aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/commands
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2026-03-09 14:28:32 -0700
committerGitHub <noreply@github.com>2026-03-09 14:28:32 -0700
commitb4a17e4346c97d837d0ee3a3a55c5ceca789a3e8 (patch)
tree4be327a9f902455a870232d36e2cd4fb4206804d /crates/atuin-ai/src/commands
parentchore: update to Rust 1.94 (#3247) (diff)
downloadatuin-b4a17e4346c97d837d0ee3a3a55c5ceca789a3e8.zip
feat: use pty proxy for rendering tui popups without clearing the terminal (#3234)
It feels much, much nicer this way. This has also been asked for pretty consistently since we made inline rendering the default. Now we can have everything :) Maintains a shadow vt100 renderer so that we can restore the terminal state upon popup close. This happens on a background thread, so our impact on terminal performance should still be super minimal, if anything ## Checks - [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [ ] I have checked that there are no existing pull requests for the same thing
Diffstat (limited to 'crates/atuin-ai/src/commands')
-rw-r--r--crates/atuin-ai/src/commands/debug_render.rs8
-rw-r--r--crates/atuin-ai/src/commands/inline.rs65
2 files changed, 69 insertions, 4 deletions
diff --git a/crates/atuin-ai/src/commands/debug_render.rs b/crates/atuin-ai/src/commands/debug_render.rs
index e78a418a..b35d73c9 100644
--- a/crates/atuin-ai/src/commands/debug_render.rs
+++ b/crates/atuin-ai/src/commands/debug_render.rs
@@ -219,6 +219,8 @@ pub async fn run(input_file: Option<String>, format: OutputFormat) -> Result<()>
anchor_col: 0,
textarea: Some(&state.textarea),
max_height: debug_input.height,
+ popup_mode: false,
+ render_above: false,
};
terminal.draw(|frame| {
@@ -245,7 +247,11 @@ fn blocks_to_json(blocks: &Blocks) -> serde_json::Value {
"title": block.title,
"content": block.content.iter().map(content_to_json).collect::<Vec<_>>()
})
- }).collect::<Vec<_>>()
+ }).collect::<Vec<_>>(),
+ "status_bar": blocks.status_bar.as_ref().map(|sb| serde_json::json!({
+ "frame": sb.frame,
+ "text": sb.text
+ }))
})
}
diff --git a/crates/atuin-ai/src/commands/inline.rs b/crates/atuin-ai/src/commands/inline.rs
index 67241574..cd670bf8 100644
--- a/crates/atuin-ai/src/commands/inline.rs
+++ b/crates/atuin-ai/src/commands/inline.rs
@@ -375,7 +375,7 @@ impl DebugStateLogger {
.unwrap_or(0);
// Calculate the actual content height needed for this state
- let content_height = calculate_needed_height(state);
+ let content_height = calculate_needed_height(state, 0);
let mut state_json = state_to_json(state);
// Add dimensions for accurate replay
@@ -405,7 +405,22 @@ async fn run_inline_tui(
debug_state_file: Option<String>,
settings: &atuin_client::settings::Settings,
) -> Result<(Action, String)> {
- // Initialize terminal guard and app state
+ // Detect popup mode (only on Unix where atuin-hex socket is available)
+ #[cfg(unix)]
+ let mut popup_state = crate::tui::popup::try_setup_popup();
+ #[cfg(not(unix))]
+ let mut popup_state: Option<()> = None;
+
+ let popup_mode = popup_state.is_some();
+
+ // Initialize terminal guard: popup mode uses Fixed viewport, inline uses Inline
+ #[cfg(unix)]
+ let mut guard = if let Some(ref ps) = popup_state {
+ TerminalGuard::new_popup(ps.current_rect, ps.saved_screen.cursor_col)?
+ } else {
+ TerminalGuard::new(keep_output)?
+ };
+ #[cfg(not(unix))]
let mut guard = TerminalGuard::new(keep_output)?;
let mut app = App::new();
if let Some(prompt) = initial_prompt {
@@ -451,16 +466,54 @@ async fn run_inline_tui(
loop {
// Ensure viewport is large enough for current content (capped at terminal height)
- let needed_height = calculate_needed_height(&app.state);
+ // In popup mode, use the actual popup width for accurate height calculation
+ let card_width = if popup_mode {
+ #[cfg(unix)]
+ {
+ popup_state
+ .as_ref()
+ .map(|ps| {
+ ps.current_rect
+ .width
+ .saturating_sub(crate::tui::popup::POPUP_MARGIN * 2)
+ })
+ .unwrap_or(0)
+ }
+ #[cfg(not(unix))]
+ {
+ 0
+ }
+ } else {
+ 0
+ };
+ let needed_height = calculate_needed_height(&app.state, card_width);
+
+ // Grow popup dynamically as content arrives
+ #[cfg(unix)]
+ if let Some(ref mut ps) = popup_state {
+ // Add vertical margin for visual separation from terminal content
+ let popup_height = needed_height.saturating_add(crate::tui::popup::POPUP_MARGIN * 2);
+ if let Some(new_rect) = ps.fit_to(popup_height) {
+ guard.resize_popup(new_rect)?;
+ }
+ }
+
let actual_height = guard.ensure_height(needed_height)?;
// Render current state
let anchor_col = guard.anchor_col();
+ #[cfg(unix)]
+ let render_above = popup_state.as_ref().is_some_and(|ps| ps.render_above);
+ #[cfg(not(unix))]
+ let render_above = false;
+
let ctx = RenderContext {
theme,
anchor_col,
textarea: Some(&app.state.textarea),
max_height: actual_height,
+ popup_mode,
+ render_above,
};
// Handle draw errors gracefully - cursor position reads can fail during resize
if let Err(e) = guard.terminal().draw(|frame| {
@@ -597,6 +650,12 @@ async fn run_inline_tui(
}
}
+ // Restore popup area before guard drops (guard skips cleanup in popup mode)
+ #[cfg(unix)]
+ if let Some(ref ps) = popup_state {
+ crate::tui::popup::restore(ps);
+ }
+
// Map exit action to return value
let result = match app.state.exit_action {
Some(ExitAction::Execute(cmd)) => (Action::Execute, cmd),