From d710d39c279655b55446e3ca5263cf2647a974fd Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:00:33 -0500 Subject: [PATCH] feat: now fixed --- docs/tui.md | 18 +- src/bin/vibebox-tui.rs | 44 +++- src/tui.rs | 507 ++++++++++++++++++++++++----------------- 3 files changed, 345 insertions(+), 224 deletions(-) diff --git a/docs/tui.md b/docs/tui.md index 8e09fba..a695230 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -4,6 +4,7 @@ The end-user ui for the vibebox ## TUI header +- One time, not fixed - This is a welcome header. - Shows text: "Welcome to Vibebox vX.X.XX" - Shows the ASCII banner @@ -12,17 +13,12 @@ The end-user ui for the vibebox - Shows current vm version and max memory, cpu cores - The position is flex, so this will move with the VM terminal history. -## Terminal Area +## Vibebox Commands -- Shows all the VM terminal history +- One time, not fixed +- Prints all commands. +- Each command includes a description. -## Vibebox input area +## Normal terminal -- A text input area, user can input text in it, it can vertically expand depending on the text length, by default it - is a line high. -- it should be able to switch to auto-completion mode, which will display a list of available commands. When in this mode, the bottom - status bar will disappear. The auto completions are displayed right below the text input area. - -## Bottom status bar - -- Display texts in gray, a line high. on the left it shows `:help` for help. \ No newline at end of file +- Then the tui should end. diff --git a/src/bin/vibebox-tui.rs b/src/bin/vibebox-tui.rs index 027ada8..b318c8f 100644 --- a/src/bin/vibebox-tui.rs +++ b/src/bin/vibebox-tui.rs @@ -1,4 +1,9 @@ -use std::{env, ffi::OsString, path::PathBuf}; +use std::{ + env, + ffi::OsString, + io::{self, Read, Write}, + path::PathBuf, +}; use color_eyre::Result; use lexopt::prelude::*; @@ -33,8 +38,7 @@ enum CliError { Io(#[from] std::io::Error), } -#[tokio::main] -async fn main() -> Result<()> { +fn main() -> Result<()> { color_eyre::install()?; let command = parse_args(env::args_os())?; @@ -52,15 +56,43 @@ async fn main() -> Result<()> { cpu_cores: config.cpu_cores, }; let mut app = AppState::new(config.cwd, vm_info); - app.push_history("VM output will appear here."); - app.push_history("TODO: wire VM IO into the TUI event loop."); - tui::run_tui(app).await?; + tui::render_tui_once(&mut app)?; + { + let mut stdout = io::stdout().lock(); + writeln!(stdout)?; + stdout.flush()?; + } + passthrough_stdio(&mut app)?; } } Ok(()) } +fn passthrough_stdio(app: &mut AppState) -> io::Result<()> { + let mut stdin = io::stdin().lock(); + let mut buf = [0u8; 8192]; + let mut line_buf: Vec = Vec::new(); + loop { + let n = stdin.read(&mut buf)?; + if n == 0 { + break; + } + for &b in &buf[..n] { + line_buf.push(b); + if b == b'\n' { + let line = String::from_utf8_lossy(&line_buf); + let trimmed = line.trim_end_matches(&['\r', '\n'][..]); + if trimmed == ":help" { + let _ = tui::render_commands_component(app); + } + line_buf.clear(); + } + } + } + Ok(()) +} + fn print_help() { println!( "vibebox-tui\n\nUsage:\n vibebox-tui [options]\n\nOptions:\n --help, -h Show this help\n --version Show version\n --cwd Working directory for the session header\n --vm-version VM version string for the header\n --max-memory Max memory in MB (default 2048)\n --cpu-cores CPU core count (default 2)\n" diff --git a/src/tui.rs b/src/tui.rs index 272a5bb..2044ced 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,27 +1,34 @@ use std::{ - io::{self, Stdout}, + io::{self, Stdout, Write}, path::PathBuf, - time::Duration, }; use color_eyre::Result; use crossterm::{ + cursor::{MoveTo, Show}, event::{ DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + execute, queue, + style::{ + Attribute, Color as CrosstermColor, Print, SetAttribute, SetBackgroundColor, + SetForegroundColor, + }, + terminal::{ + Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, + enable_raw_mode, + }, }; use futures::StreamExt; use ratatui::{ - backend::CrosstermBackend, buffer::Buffer, + Frame, Terminal, + backend::CrosstermBackend, + buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::{Line, Span, Text}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget, Wrap}, - Frame, - Terminal, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}, }; use tui_textarea::{Input, Key, TextArea}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -36,10 +43,6 @@ const ASCII_BANNER: [&str; 7] = [ "", ]; -const STATUS_BAR_HEIGHT: u16 = 1; -const COMPLETIONS_MAX_HEIGHT: u16 = 6; -const SPINNER_FRAMES: [&str; 4] = ["|", "/", "-", "\\"]; - #[derive(Debug, Clone)] pub struct VmInfo { pub version: String, @@ -53,11 +56,9 @@ pub struct AppState { pub vm_info: VmInfo, pub history: Vec, pub input: TextArea<'static>, - pub completions: CompletionState, + pub commands: VibeboxCommands, pub should_quit: bool, key_input_mode: KeyInputMode, - tick: u64, - spinner: usize, page_scroll: usize, input_view_width: u16, } @@ -65,17 +66,18 @@ pub struct AppState { impl AppState { pub fn new(cwd: PathBuf, vm_info: VmInfo) -> Self { let input = Self::default_input(); + let mut commands = VibeboxCommands::default(); + commands.add_command(":new", "Create a new session."); + commands.add_command(":exit", "Exit Vibebox."); Self { cwd, vm_info, history: Vec::new(), input, - completions: CompletionState::default(), + commands, should_quit: false, key_input_mode: KeyInputMode::Unknown, - tick: 0, - spinner: 0, page_scroll: 0, input_view_width: 0, } @@ -171,27 +173,26 @@ impl AppState { } } - pub fn activate_completions(&mut self, items: Vec) { - self.completions.set_items(items); - self.completions.activate(); + pub fn activate_commands(&mut self) { + self.commands.activate(); } - pub fn deactivate_completions(&mut self) { - self.completions.deactivate(); + pub fn deactivate_commands(&mut self) { + self.commands.deactivate(); } - pub fn toggle_completions(&mut self, items: Vec) { - if self.completions.active { - self.deactivate_completions(); + pub fn toggle_commands(&mut self) { + if self.commands.active { + self.deactivate_commands(); } else { - self.activate_completions(items); + self.activate_commands(); } } } -#[derive(Debug, Default, Clone)] -pub struct CompletionState { - items: Vec, +#[derive(Debug, Clone)] +pub struct VibeboxCommands { + items: Vec, selected: usize, active: bool, } @@ -203,12 +204,19 @@ enum KeyInputMode { Release, } -impl CompletionState { +impl VibeboxCommands { pub fn set_items(&mut self, items: Vec) { - self.items = items; + self.items = items.into_iter().map(VibeboxCommand::new).collect(); self.selected = 0; } + pub fn add_command(&mut self, name: impl Into, description: impl Into) { + self.items.push(VibeboxCommand { + name: name.into(), + description: description.into(), + }); + } + pub fn activate(&mut self) { self.active = !self.items.is_empty(); self.selected = 0; @@ -238,13 +246,13 @@ impl CompletionState { pub fn current(&self) -> Option<&str> { if self.active { - self.items.get(self.selected).map(|s| s.as_str()) + self.items.get(self.selected).map(|cmd| cmd.name.as_str()) } else { None } } - pub fn items(&self) -> &[String] { + pub fn items(&self) -> &[VibeboxCommand] { &self.items } @@ -257,6 +265,32 @@ impl CompletionState { } } +impl Default for VibeboxCommands { + fn default() -> Self { + Self { + items: vec![VibeboxCommand { + name: ":help".to_string(), + description: "Show Vibebox commands.".to_string(), + }], + selected: 0, + active: false, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct VibeboxCommand { + pub name: String, + pub description: String, +} + +impl VibeboxCommand { + pub fn new(name: String) -> Self { + let description = command_description(&name).to_string(); + Self { name, description } + } +} + #[allow(dead_code)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct LayoutAreas { @@ -278,79 +312,63 @@ struct PageLayout { } #[allow(dead_code)] -pub fn compute_layout( - area: Rect, - input_height: u16, - completion_items: usize, - completion_active: bool, -) -> LayoutAreas { +pub fn compute_layout(area: Rect, input_height: u16, completion_items: usize) -> LayoutAreas { let header_height = header_height().min(area.height); let mut remaining = area.height.saturating_sub(header_height); let input_height = input_height.max(1).min(remaining); remaining = remaining.saturating_sub(input_height); - let (completion_height, status_height) = if completion_active { - let desired = (completion_items as u16).min(COMPLETIONS_MAX_HEIGHT); - let height = desired.min(remaining); - (height, 0) + let completion_height = if completion_items == 0 { + 0 } else { - let height = STATUS_BAR_HEIGHT.min(remaining); - (0, height) + let desired = (completion_items as u16).saturating_add(2); + desired }; + let completion_height = completion_height.min(remaining); - remaining = remaining.saturating_sub(completion_height + status_height); + let terminal_height = 0; - let terminal_height = remaining; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(header_height), - Constraint::Length(terminal_height), - Constraint::Length(input_height), - Constraint::Length(completion_height), - Constraint::Length(status_height), - ]) - .split(area); + let mut y = 0u16; + let header = Rect::new(area.x, area.y + y, area.width, header_height); + y = y.saturating_add(header_height); + let terminal = Rect::new(area.x, area.y + y, area.width, terminal_height); + let input = Rect::new(area.x, area.y + y, area.width, input_height); + y = y.saturating_add(input_height); + let completions = Rect::new(area.x, area.y + y, area.width, completion_height); + let status = Rect::new(area.x, area.y + y, area.width, 0); LayoutAreas { - header: chunks[0], - terminal: chunks[1], - input: chunks[2], - completions: chunks[3], - status: chunks[4], + header, + terminal, + input, + completions, + status, } } fn compute_page_layout(app: &AppState, width: u16) -> PageLayout { let header_height = header_height(); - let terminal_height = terminal_height(app, width); - let input_height = app.input_height_for_width(width); - let (completion_height, status_height) = if app.completions.is_active() { - let desired = (app.completions.items().len() as u16).min(COMPLETIONS_MAX_HEIGHT); - (desired.saturating_add(2), 0) + let input_height = 0; + let completion_items = app.commands.items().len(); + let completion_height = if completion_items == 0 { + 0 } else { - (0, STATUS_BAR_HEIGHT) + (completion_items as u16).saturating_add(2) }; - let total_height = header_height - .saturating_add(terminal_height) .saturating_add(input_height) .saturating_add(completion_height) - .saturating_add(status_height) .max(1); let mut y = 0u16; let header = Rect::new(0, y, width, header_height); y = y.saturating_add(header_height); - let terminal = Rect::new(0, y, width, terminal_height); - y = y.saturating_add(terminal_height); + let terminal = Rect::new(0, y, width, 0); let input = Rect::new(0, y, width, input_height); y = y.saturating_add(input_height); let completions = Rect::new(0, y, width, completion_height); - y = y.saturating_add(completion_height); - let status = Rect::new(0, y, width, status_height); + let status = Rect::new(0, y, width, 0); PageLayout { header, @@ -362,23 +380,6 @@ fn compute_page_layout(app: &AppState, width: u16) -> PageLayout { } } -fn terminal_height(app: &AppState, width: u16) -> u16 { - if width == 0 { - return 0; - } - let lines: Vec = app - .history - .iter() - .map(|line| Line::from(line.as_str())) - .collect(); - let block = Block::default().borders(Borders::ALL).title("Terminal"); - let paragraph = Paragraph::new(Text::from_iter(lines)) - .block(block) - .wrap(Wrap { trim: true }); - let count = paragraph.line_count(width).max(1); - (count.min(u16::MAX as usize)) as u16 -} - fn header_height() -> u16 { let banner_height = ASCII_BANNER.len() as u16; let welcome_height = 1; @@ -389,21 +390,12 @@ fn header_height() -> u16 { pub async fn run_tui(mut app: AppState) -> Result<()> { let mut terminal = TerminalGuard::init()?; let mut events = EventStream::new(); - let mut tick = tokio::time::interval(Duration::from_millis(250)); loop { terminal.draw(|frame| render(frame, &mut app))?; - tokio::select! { - _ = tick.tick() => { - app.tick = app.tick.wrapping_add(1); - app.spinner = (app.spinner + 1) % SPINNER_FRAMES.len(); - }, - event = events.next() => { - if let Some(event) = event { - handle_event(event?, &mut app); - } - } + if let Some(event) = events.next().await { + handle_event(event?, &mut app); } if app.should_quit { @@ -414,6 +406,161 @@ pub async fn run_tui(mut app: AppState) -> Result<()> { Ok(()) } +pub fn render_tui_once(app: &mut AppState) -> Result<()> { + let (width, _) = crossterm::terminal::size()?; + if width == 0 { + return Ok(()); + } + + let buffer = render_static_buffer(app, width); + let mut stdout = io::stdout(); + execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?; + write_buffer_with_style(&buffer, &mut stdout)?; + stdout.flush()?; + Ok(()) +} + +pub fn render_commands_component(app: &mut AppState) -> Result<()> { + let (width, _) = crossterm::terminal::size()?; + if width == 0 { + return Ok(()); + } + + let command_count = app.commands.items().len() as u16; + let height = if command_count == 0 { + 0 + } else { + command_count.saturating_add(2) + }; + if height == 0 { + return Ok(()); + } + + let mut buffer = Buffer::empty(Rect::new(0, 0, width, height)); + let area = Rect::new(0, 0, width, height); + render_completions(&mut buffer, area, app); + + let mut stdout = io::stdout(); + write_buffer_with_style(&buffer, &mut stdout)?; + stdout.flush()?; + Ok(()) +} + +fn render_static_buffer(app: &mut AppState, width: u16) -> Buffer { + let layout = compute_page_layout(app, width); + let content_height = layout.total_height.max(1); + let mut buffer = Buffer::empty(Rect::new(0, 0, width, content_height)); + render_header(&mut buffer, layout.header, app); + render_completions(&mut buffer, layout.completions, app); + buffer +} + +fn write_buffer_with_style(buffer: &Buffer, out: &mut impl Write) -> io::Result<()> { + let area = buffer.area; + let mut current_fg: Option = None; + let mut current_bg: Option = None; + let mut current_modifier: Option = None; + + for y in 0..area.height { + for x in 0..area.width { + let cell = &buffer[(x, y)]; + if cell.skip { + continue; + } + + let fg = map_color(cell.fg); + let bg = map_color(cell.bg); + let modifier = cell.modifier; + if current_fg != Some(fg) + || current_bg != Some(bg) + || current_modifier != Some(modifier) + { + queue!(out, SetAttribute(Attribute::Reset))?; + queue!(out, SetForegroundColor(fg), SetBackgroundColor(bg))?; + queue_modifier(out, modifier)?; + current_fg = Some(fg); + current_bg = Some(bg); + current_modifier = Some(modifier); + } + + let symbol = cell.symbol(); + if symbol.is_empty() { + queue!(out, Print(" "))?; + } else { + queue!(out, Print(symbol))?; + } + } + queue!( + out, + SetAttribute(Attribute::Reset), + SetForegroundColor(CrosstermColor::Reset), + SetBackgroundColor(CrosstermColor::Reset), + Print("\n") + )?; + current_fg = None; + current_bg = None; + current_modifier = None; + } + + Ok(()) +} + +fn map_color(color: ratatui::style::Color) -> CrosstermColor { + match color { + ratatui::style::Color::Reset => CrosstermColor::Reset, + ratatui::style::Color::Black => CrosstermColor::Black, + ratatui::style::Color::Red => CrosstermColor::DarkRed, + ratatui::style::Color::Green => CrosstermColor::DarkGreen, + ratatui::style::Color::Yellow => CrosstermColor::DarkYellow, + ratatui::style::Color::Blue => CrosstermColor::DarkBlue, + ratatui::style::Color::Magenta => CrosstermColor::DarkMagenta, + ratatui::style::Color::Cyan => CrosstermColor::DarkCyan, + ratatui::style::Color::Gray => CrosstermColor::Grey, + ratatui::style::Color::DarkGray => CrosstermColor::DarkGrey, + ratatui::style::Color::LightRed => CrosstermColor::Red, + ratatui::style::Color::LightGreen => CrosstermColor::Green, + ratatui::style::Color::LightYellow => CrosstermColor::Yellow, + ratatui::style::Color::LightBlue => CrosstermColor::Blue, + ratatui::style::Color::LightMagenta => CrosstermColor::Magenta, + ratatui::style::Color::LightCyan => CrosstermColor::Cyan, + ratatui::style::Color::White => CrosstermColor::White, + ratatui::style::Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b }, + ratatui::style::Color::Indexed(i) => CrosstermColor::AnsiValue(i), + } +} + +fn queue_modifier(out: &mut impl Write, modifier: ratatui::style::Modifier) -> io::Result<()> { + use ratatui::style::Modifier; + if modifier.contains(Modifier::BOLD) { + queue!(out, SetAttribute(Attribute::Bold))?; + } + if modifier.contains(Modifier::DIM) { + queue!(out, SetAttribute(Attribute::Dim))?; + } + if modifier.contains(Modifier::ITALIC) { + queue!(out, SetAttribute(Attribute::Italic))?; + } + if modifier.contains(Modifier::UNDERLINED) { + queue!(out, SetAttribute(Attribute::Underlined))?; + } + if modifier.contains(Modifier::SLOW_BLINK) { + queue!(out, SetAttribute(Attribute::SlowBlink))?; + } + if modifier.contains(Modifier::RAPID_BLINK) { + queue!(out, SetAttribute(Attribute::RapidBlink))?; + } + if modifier.contains(Modifier::REVERSED) { + queue!(out, SetAttribute(Attribute::Reverse))?; + } + if modifier.contains(Modifier::HIDDEN) { + queue!(out, SetAttribute(Attribute::Hidden))?; + } + if modifier.contains(Modifier::CROSSED_OUT) { + queue!(out, SetAttribute(Attribute::CrossedOut))?; + } + Ok(()) +} + fn handle_event(event: CrosstermEvent, app: &mut AppState) { match event { CrosstermEvent::Key(key) => handle_key_event(key, app), @@ -432,16 +579,16 @@ fn handle_key_event(key: KeyEvent, app: &mut AppState) { return; } - if app.completions.is_active() { + if app.commands.is_active() { match key.code { - KeyCode::Esc => app.deactivate_completions(), - KeyCode::Up => app.completions.previous(), - KeyCode::Down => app.completions.next(), + KeyCode::Esc => app.deactivate_commands(), + KeyCode::Up => app.commands.previous(), + KeyCode::Down => app.commands.next(), KeyCode::Enter => { - if let Some(selection) = app.completions.current() { + if let Some(selection) = app.commands.current() { app.input.insert_str(selection); } - app.deactivate_completions(); + app.deactivate_commands(); } _ => {} } @@ -464,7 +611,7 @@ fn handle_key_event(key: KeyEvent, app: &mut AppState) { app.page_scroll = usize::MAX; } } - KeyCode::Tab => app.toggle_completions(default_completions()), + KeyCode::Tab => app.toggle_commands(), KeyCode::Char(c) if !key .modifiers @@ -547,8 +694,13 @@ fn input_from_key_event(key: KeyEvent) -> Input { } } -fn default_completions() -> Vec { - vec![":help".to_string(), ":new".to_string(), ":exit".to_string()] +fn command_description(command: &str) -> &'static str { + match command { + ":help" => "Show Vibebox commands.", + ":new" => "Create a new session.", + ":exit" => "Exit Vibebox.", + _ => "", + } } fn render(frame: &mut Frame<'_>, app: &mut AppState) { @@ -563,14 +715,7 @@ fn render(frame: &mut Frame<'_>, app: &mut AppState) { let mut buffer = Buffer::empty(Rect::new(0, 0, viewport.width, content_height)); render_header(&mut buffer, layout.header, app); - render_terminal(&mut buffer, layout.terminal, app); - let cursor_pos = render_input(&mut buffer, layout.input, app); - - if app.completions.is_active() { - render_completions(&mut buffer, layout.completions, app); - } else { - render_status(&mut buffer, layout.status, app); - } + render_completions(&mut buffer, layout.completions, app); let max_scroll = content_height.saturating_sub(viewport.height); app.page_scroll = app.page_scroll.min(max_scroll as usize); @@ -586,12 +731,6 @@ fn render(frame: &mut Frame<'_>, app: &mut AppState) { view[(x, y)] = buffer[(x, src_y)].clone(); } } - - if let Some((x, y)) = cursor_pos { - if y >= scroll && y < scroll.saturating_add(viewport.height) { - frame.set_cursor_position((x, y - scroll)); - } - } } fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) { @@ -645,80 +784,34 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) { .render(header_chunks[2], buffer); } -fn render_terminal(buffer: &mut Buffer, area: Rect, app: &AppState) { - if area.height == 0 { - return; - } - let lines = app.history.iter().map(|line| Line::from(line.as_str())); - let block = Block::default().borders(Borders::ALL).title("Terminal"); - let paragraph = Paragraph::new(Text::from_iter(lines)) - .block(block) - .wrap(Wrap { trim: true }); - - paragraph.render(area, buffer); -} - -fn render_input(buffer: &mut Buffer, area: Rect, app: &mut AppState) -> Option<(u16, u16)> { - if area.height == 0 || area.width == 0 { - return None; - } - app.input.render(area, buffer); - let cursor = app.input.cursor(); - let inner = match app.input.block() { - Some(block) => block.inner(area), - None => area, - }; - let x = inner.x.saturating_add(cursor.1 as u16); - let y = inner.y.saturating_add(cursor.0 as u16); - if x < inner.x.saturating_add(inner.width) && y < inner.y.saturating_add(inner.height) { - Some((x, y)) - } else { - None - } -} - fn render_completions(buffer: &mut Buffer, area: Rect, app: &mut AppState) { if area.height == 0 { return; } let items: Vec> = app - .completions + .commands .items() .iter() - .map(|item| ListItem::new(Line::from(item.as_str()))) + .map(|cmd| ListItem::new(Line::from(format!("{} {}", cmd.name, cmd.description)))) .collect(); let list = List::new(items) - .block(Block::default().borders(Borders::ALL).title("Completions")) + .block( + Block::default() + .borders(Borders::ALL) + .title("Vibebox Commands"), + ) .highlight_style(Style::default().fg(Color::Yellow)); let mut state = ListState::default(); - if app.completions.is_active() && !app.completions.items().is_empty() { - state.select(Some(app.completions.selected())); + if app.commands.is_active() && !app.commands.items().is_empty() { + state.select(Some(app.commands.selected())); } StatefulWidget::render(list, area, buffer, &mut state); } -fn render_status(buffer: &mut Buffer, area: Rect, app: &AppState) { - if area.height == 0 { - return; - } - - let spinner = SPINNER_FRAMES[app.spinner % SPINNER_FRAMES.len()]; - let status = Paragraph::new(Line::from(vec![ - Span::styled(":help", Style::default().fg(Color::DarkGray)), - Span::raw(" "), - Span::styled( - format!("tick {} {}", app.tick, spinner), - Style::default().fg(Color::DarkGray), - ), - ])); - - status.render(area, buffer); -} - struct TerminalGuard { terminal: Terminal>, } @@ -755,29 +848,29 @@ mod tests { use super::*; #[test] - fn layout_without_completions_reserves_status_bar() { + fn layout_without_completions_hides_status_bar() { let area = Rect::new(0, 0, 80, 30); - let layout = compute_layout(area, 3, 0, false); + let layout = compute_layout(area, 3, 0); assert_eq!(layout.header.height, header_height()); - assert_eq!(layout.status.height, STATUS_BAR_HEIGHT); + assert_eq!(layout.status.height, 0); assert_eq!(layout.completions.height, 0); - assert!(layout.terminal.height > 0); + assert_eq!(layout.terminal.height, 0); } #[test] fn layout_with_completions_hides_status_bar() { let area = Rect::new(0, 0, 80, 30); - let layout = compute_layout(area, 4, 3, true); + let layout = compute_layout(area, 4, 3); assert_eq!(layout.status.height, 0); - assert_eq!(layout.completions.height, 3); + assert_eq!(layout.completions.height, 5); } #[test] fn layout_clamps_when_space_is_tight() { let area = Rect::new(0, 0, 80, header_height() + 1); - let layout = compute_layout(area, 3, 10, true); + let layout = compute_layout(area, 3, 10); assert_eq!(layout.header.height, header_height()); assert_eq!(layout.input.height, 1); @@ -785,31 +878,31 @@ mod tests { } #[test] - fn completion_state_wraps_navigation() { - let mut completions = CompletionState::default(); - completions.set_items(vec!["a".into(), "b".into(), "c".into()]); - completions.activate(); + fn commands_wrap_navigation() { + let mut commands = VibeboxCommands::default(); + commands.set_items(vec![":new".into(), ":exit".into(), ":help".into()]); + commands.activate(); - assert_eq!(completions.current(), Some("a")); + assert_eq!(commands.current(), Some(":new")); - completions.next(); - assert_eq!(completions.current(), Some("b")); + commands.next(); + assert_eq!(commands.current(), Some(":exit")); - completions.next(); - completions.next(); - assert_eq!(completions.current(), Some("a")); + commands.next(); + commands.next(); + assert_eq!(commands.current(), Some(":new")); - completions.previous(); - assert_eq!(completions.current(), Some("c")); + commands.previous(); + assert_eq!(commands.current(), Some(":help")); } #[test] - fn completion_state_is_inactive_when_empty() { - let mut completions = CompletionState::default(); - completions.activate(); + fn commands_inactive_when_empty() { + let mut commands = VibeboxCommands::default(); + commands.activate(); - assert!(!completions.is_active()); - assert_eq!(completions.current(), None); + assert!(!commands.is_active()); + assert_eq!(commands.current(), None); } #[test]