aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/tui/terminal.rs
diff options
context:
space:
mode:
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();