diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-03-26 19:19:47 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-27 02:19:47 +0000 |
| commit | b649a7ab8de6488c1341e94c37d032c07d5b3f13 (patch) | |
| tree | ca9aadc1175b8439dd85de135f3804681b755776 /crates/atuin-ai/src/tui/component.rs | |
| parent | fix: set WorkingDirectory in PowerShell Invoke-AtuinSearch (#3351) (diff) | |
| download | atuin-b649a7ab8de6488c1341e94c37d032c07d5b3f13.zip | |
feat: Use eye-declare for more performant and flexible AI TUI (#3343)
This PR replaces the mess of custom rendering code in Atuin AI with
[eye-declare](https://github.com/BinaryMuse/eye-declare), and updates
the TUI to feel more terminal-native: output appears inline and persists
in scrollback, so you can scroll up and look at previous conversations
for reference.
The "review" state — which used to exist between the LLM generating a
response and the user either executing or following up — has been
removed; just start typing to follow up with the LLM, or press `enter`
at the empty input box to execute the suggested command.
<img width="1203" height="633" alt="image"
src="https://github.com/user-attachments/assets/159ee447-9a2a-4edd-b56e-a79bf1aaaa94"
/>
Diffstat (limited to 'crates/atuin-ai/src/tui/component.rs')
| -rw-r--r-- | crates/atuin-ai/src/tui/component.rs | 186 |
1 files changed, 0 insertions, 186 deletions
diff --git a/crates/atuin-ai/src/tui/component.rs b/crates/atuin-ai/src/tui/component.rs deleted file mode 100644 index ff20f195..00000000 --- a/crates/atuin-ai/src/tui/component.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Component-oriented rendering primitives for the TUI. -//! -//! Defines the `Component` trait and container types (`VStack`, `SymbolRow`, etc.) -//! that enable declarative, composable UI layout. - -use atuin_client::theme::{Meaning, Theme}; -use ratatui::{ - Frame, backend::FromCrossterm, layout::Rect, style::Style, text::Span, widgets::Paragraph, -}; -use tui_textarea::TextArea; - -/// Context passed through the component tree during rendering. -pub struct RenderContext<'a> { - pub theme: &'a Theme, - pub anchor_col: u16, - pub textarea: Option<&'a TextArea<'static>>, - /// Maximum viewport height (for scroll calculations) - pub max_height: u16, - /// When true, the viewport is a fixed rect already positioned for the card. - /// The card fills the entire viewport instead of positioning via anchor_col. - pub popup_mode: bool, - /// When true, blocks are rendered in reverse order so that the input field - /// appears at the bottom of the card (close to the prompt when the popup - /// is above the cursor). - pub render_above: bool, -} - -/// A renderable component with intrinsic sizing. -pub trait Component { - /// Calculate the intrinsic height at the given width. - fn height(&self, width: u16) -> u16; - - /// Render into the given area. - fn render(&self, frame: &mut Frame, area: Rect, ctx: &RenderContext); -} - -/// Vertical stack of components. -/// -/// Children are laid out top-to-bottom with optional spacing between them. -/// When `scroll_offset > 0`, content is scrolled so that only the visible -/// portion is rendered. -pub struct VStack { - pub children: Vec<Box<dyn Component>>, - pub spacing: u16, - pub scroll_offset: u16, -} - -impl VStack { - pub fn new(children: Vec<Box<dyn Component>>) -> Self { - Self { - children, - spacing: 0, - scroll_offset: 0, - } - } -} - -impl Component for VStack { - fn height(&self, width: u16) -> u16 { - if self.children.is_empty() { - return 0; - } - let content: u16 = self.children.iter().map(|c| c.height(width)).sum(); - let gaps = (self.children.len() as u16 - 1) * self.spacing; - content + gaps - } - - fn render(&self, frame: &mut Frame, area: Rect, ctx: &RenderContext) { - if self.children.is_empty() { - return; - } - - let heights: Vec<u16> = self.children.iter().map(|c| c.height(area.width)).collect(); - - let viewport_start = self.scroll_offset; - let viewport_end = self.scroll_offset + area.height; - - let mut cum: u16 = 0; - for (i, (child, &h)) in self.children.iter().zip(heights.iter()).enumerate() { - let child_start = cum; - let child_end = cum + h; - - // Render if any part of the child is within the viewport - if child_end > viewport_start && child_start < viewport_end { - let visible_start = child_start.max(viewport_start); - let visible_end = child_end.min(viewport_end); - - let child_area = Rect { - x: area.x, - y: area.y + visible_start - viewport_start, - width: area.width, - height: visible_end - visible_start, - }; - - child.render(frame, child_area, ctx); - } - - cum = child_end; - if i < self.children.len() - 1 { - cum += self.spacing; - } - } - } -} - -/// Fixed-height empty space. -pub struct Spacer(pub u16); - -impl Component for Spacer { - fn height(&self, _width: u16) -> u16 { - self.0 - } - - fn render(&self, _frame: &mut Frame, _area: Rect, _ctx: &RenderContext) {} -} - -/// A row with a symbol in column 0 and content in columns 2+. -/// -/// This is the horizontal layout primitive used by all content types that -/// display a prefix symbol (>, $, !, ?, etc.) followed by text. -pub struct SymbolRow { - pub symbol: String, - pub symbol_meaning: Meaning, - pub inner: Box<dyn Component>, -} - -impl Component for SymbolRow { - fn height(&self, width: u16) -> u16 { - self.inner.height(width.saturating_sub(2)) - } - - fn render(&self, frame: &mut Frame, area: Rect, ctx: &RenderContext) { - // Render symbol at column 0, first row only - let style = Style::from_crossterm(ctx.theme.as_style(self.symbol_meaning)); - let symbol_area = Rect { - x: area.x, - y: area.y, - width: 1, - height: 1, - }; - frame.render_widget( - Paragraph::new(self.symbol.as_str()).style(style), - symbol_area, - ); - - // Render inner content at column 2+ - let content_area = Rect { - x: area.x.saturating_add(2), - y: area.y, - width: area.width.saturating_sub(2), - height: area.height, - }; - self.inner.render(frame, content_area, ctx); - } -} - -/// Horizontal separator spanning the full card width (├───┤). -/// -/// Extends beyond its content area to overlap the card's left and right borders. -pub struct Separator { - pub card_width: u16, -} - -impl Component for Separator { - fn height(&self, _width: u16) -> u16 { - 1 - } - - fn render(&self, frame: &mut Frame, area: Rect, ctx: &RenderContext) { - let style = Style::from_crossterm(ctx.theme.as_style(Meaning::Base)); - let inner_width = self.card_width.saturating_sub(2) as usize; - let separator = format!( - "\u{251c}{}\u{2524}", // ├ ... ┤ - "\u{2500}".repeat(inner_width) // ─ - ); - - // Extend left to overlap the card border (content area is inset by border + padding) - let sep_area = Rect { - x: area.x.saturating_sub(2), - y: area.y, - width: self.card_width, - height: 1, - }; - frame.render_widget(Paragraph::new(Span::styled(separator, style)), sep_area); - } -} |
