aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/tui/terminal.rs
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/tui/terminal.rs
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/tui/terminal.rs')
-rw-r--r--crates/atuin-ai/src/tui/terminal.rs77
1 files changed, 76 insertions, 1 deletions
diff --git a/crates/atuin-ai/src/tui/terminal.rs b/crates/atuin-ai/src/tui/terminal.rs
index 75bfd6e6..f8089323 100644
--- a/crates/atuin-ai/src/tui/terminal.rs
+++ b/crates/atuin-ai/src/tui/terminal.rs
@@ -3,7 +3,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode},
};
use eyre::{Context, Result, bail};
-use ratatui::{Terminal, TerminalOptions, Viewport, backend::CrosstermBackend};
+use ratatui::{Terminal, TerminalOptions, Viewport, backend::CrosstermBackend, layout::Rect};
use std::io::{IsTerminal, Stdout, stdout};
/// Install a panic hook that ensures the terminal is restored to a usable state
@@ -65,6 +65,7 @@ pub struct TerminalGuard {
anchor_col: u16,
keep_output: bool,
viewport_height: u16,
+ popup_mode: bool,
}
impl TerminalGuard {
@@ -122,6 +123,56 @@ impl TerminalGuard {
anchor_col,
keep_output,
viewport_height,
+ popup_mode: false,
+ })
+ }
+
+ /// Create a new TerminalGuard for popup overlay mode.
+ ///
+ /// In popup mode:
+ /// - Raw mode is not managed (atuin-hex owns it)
+ /// - The viewport is a fixed rect positioned over existing terminal content
+ /// - The popup area is pre-cleared to prevent background bleed-through
+ /// - Drop does not clear the viewport or disable raw mode
+ pub fn new_popup(popup_rect: Rect, anchor_col: u16) -> Result<Self> {
+ // Pre-clear the popup area before creating the ratatui terminal.
+ // Ratatui's diff-based rendering won't write "default" (space) cells on
+ // the first frame because its previous buffer is also all-default. By
+ // writing spaces to the terminal now, we ensure those positions are
+ // visually blank even if ratatui skips them.
+ {
+ use crossterm::cursor::MoveTo;
+ use crossterm::execute;
+ use crossterm::style::{Attribute, SetAttribute};
+ use std::io::Write;
+
+ let mut out = stdout();
+ for row in popup_rect.y..popup_rect.y.saturating_add(popup_rect.height) {
+ let _ = execute!(
+ out,
+ MoveTo(popup_rect.x, row),
+ SetAttribute(Attribute::Reset)
+ );
+ let _ = write!(out, "{:width$}", "", width = popup_rect.width as usize);
+ }
+ let _ = out.flush();
+ }
+
+ let backend = CrosstermBackend::new(stdout());
+ let terminal = Terminal::with_options(
+ backend,
+ TerminalOptions {
+ viewport: Viewport::Fixed(popup_rect),
+ },
+ )
+ .context("failed to create terminal with fixed viewport")?;
+
+ Ok(Self {
+ terminal,
+ anchor_col,
+ keep_output: false,
+ viewport_height: popup_rect.height,
+ popup_mode: true,
})
}
@@ -149,6 +200,24 @@ impl TerminalGuard {
&mut self.terminal
}
+ /// Resize the popup viewport to a new rect.
+ ///
+ /// Creates a fresh terminal with the updated Fixed viewport. The caller
+ /// is responsible for pre-clearing any newly exposed rows before calling
+ /// this (see `PopupState::grow_to`).
+ pub fn resize_popup(&mut self, new_rect: Rect) -> Result<()> {
+ self.viewport_height = new_rect.height;
+ let backend = CrosstermBackend::new(stdout());
+ self.terminal = Terminal::with_options(
+ backend,
+ TerminalOptions {
+ viewport: Viewport::Fixed(new_rect),
+ },
+ )
+ .context("failed to resize popup terminal")?;
+ Ok(())
+ }
+
/// Get the anchor column where the inline UI should be positioned.
///
/// This is the column position where the cursor was located when
@@ -173,6 +242,12 @@ impl TerminalGuard {
/// - The panic hook provides a second layer of safety for abnormal exits
impl Drop for TerminalGuard {
fn drop(&mut self) {
+ if self.popup_mode {
+ // Popup mode: screen restoration handled by caller before drop.
+ // Raw mode is owned by atuin-hex, don't touch it.
+ return;
+ }
+
// Clear terminal content only if keep_output is false - ignore errors (best-effort)
if !self.keep_output {
let _ = self.terminal.clear();