@@ -1,27 +1,34 @@
2− io::{self, Stdout},
2+ io::{self, Stdout, Write},
3 path::PathBuf,
3 path::PathBuf,
7use color_eyre::Result;
6use color_eyre::Result;
8use crossterm::{
7use crossterm::{
8+ cursor::{MoveTo, Show},
10 DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream, KeyCode,
10 DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream, KeyCode,
11 KeyEvent, KeyEventKind, KeyModifiers,
11 KeyEvent, KeyEventKind, KeyModifiers,
13− execute,
13+ execute, queue,
14− terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
14+ style::{
15+ Attribute, Color as CrosstermColor, Print, SetAttribute, SetBackgroundColor,
19+ Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
16use futures::StreamExt;
23use futures::StreamExt;
17use ratatui::{
24use ratatui::{
18− backend::CrosstermBackend, buffer::Buffer,
26+ backend::CrosstermBackend,
19 layout::{Constraint, Direction, Layout, Rect},
28 layout::{Constraint, Direction, Layout, Rect},
20 style::{Color, Style},
29 style::{Color, Style},
21 text::{Line, Span, Text},
30 text::{Line, Span, Text},
22− widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget, Wrap},
31+ widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget},
26use tui_textarea::{Input, Key, TextArea};
33use tui_textarea::{Input, Key, TextArea};
27use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
34use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
@@ -36,10 +43,6 @@ const ASCII_BANNER: [&str; 7] = [
39−const STATUS_BAR_HEIGHT: u16 = 1;
40−const COMPLETIONS_MAX_HEIGHT: u16 = 6;
41−const SPINNER_FRAMES: [&str; 4] = ["|", "/", "-", "\\"];
43#[derive(Debug, Clone)]
46#[derive(Debug, Clone)]
44pub struct VmInfo {
47pub struct VmInfo {
45 pub version: String,
48 pub version: String,
@@ -53,11 +56,9 @@ pub struct AppState {
53 pub vm_info: VmInfo,
56 pub vm_info: VmInfo,
54 pub history: Vec<String>,
57 pub history: Vec<String>,
55 pub input: TextArea<'static>,
58 pub input: TextArea<'static>,
56− pub completions: CompletionState,
59+ pub commands: VibeboxCommands,
57 pub should_quit: bool,
60 pub should_quit: bool,
58 key_input_mode: KeyInputMode,
61 key_input_mode: KeyInputMode,
61 page_scroll: usize,
62 page_scroll: usize,
62 input_view_width: u16,
63 input_view_width: u16,
@@ -65,17 +66,18 @@ pub struct AppState {
65impl AppState {
66impl AppState {
66 pub fn new(cwd: PathBuf, vm_info: VmInfo) -> Self {
67 pub fn new(cwd: PathBuf, vm_info: VmInfo) -> Self {
67 let input = Self::default_input();
68 let input = Self::default_input();
69+ let mut commands = VibeboxCommands::default();
70+ commands.add_command(":new", "Create a new session.");
71+ commands.add_command(":exit", "Exit Vibebox.");
72 history: Vec::new(),
76 history: Vec::new(),
74− completions: CompletionState::default(),
78+ commands,
75 should_quit: false,
79 should_quit: false,
76 key_input_mode: KeyInputMode::Unknown,
80 key_input_mode: KeyInputMode::Unknown,
79 page_scroll: 0,
81 page_scroll: 0,
80 input_view_width: 0,
82 input_view_width: 0,
@@ -171,27 +173,26 @@ impl AppState {
174− pub fn activate_completions(&mut self, items: Vec<String>) {
176+ pub fn activate_commands(&mut self) {
175− self.completions.set_items(items);
177+ self.commands.activate();
176− self.completions.activate();
179− pub fn deactivate_completions(&mut self) {
180+ pub fn deactivate_commands(&mut self) {
180− self.completions.deactivate();
181+ self.commands.deactivate();
183− pub fn toggle_completions(&mut self, items: Vec<String>) {
184+ pub fn toggle_commands(&mut self) {
184− if self.completions.active {
185+ if self.commands.active {
185− self.deactivate_completions();
186+ self.deactivate_commands();
187− self.activate_completions(items);
188+ self.activate_commands();
192−#[derive(Debug, Default, Clone)]
193+#[derive(Debug, Clone)]
193−pub struct CompletionState {
194+pub struct VibeboxCommands {
194− items: Vec<String>,
195+ items: Vec<VibeboxCommand>,
195 selected: usize,
196 selected: usize,
196 active: bool,
197 active: bool,
@@ -203,12 +204,19 @@ enum KeyInputMode {
206−impl CompletionState {
207+impl VibeboxCommands {
207 pub fn set_items(&mut self, items: Vec<String>) {
208 pub fn set_items(&mut self, items: Vec<String>) {
208− self.items = items;
209+ self.items = items.into_iter().map(VibeboxCommand::new).collect();
209 self.selected = 0;
210 self.selected = 0;
213+ pub fn add_command(&mut self, name: impl Into<String>, description: impl Into<String>) {
214+ self.items.push(VibeboxCommand {
216+ description: description.into(),
212 pub fn activate(&mut self) {
220 pub fn activate(&mut self) {
213 self.active = !self.items.is_empty();
221 self.active = !self.items.is_empty();
214 self.selected = 0;
222 self.selected = 0;
@@ -238,13 +246,13 @@ impl CompletionState {
239 pub fn current(&self) -> Option<&str> {
247 pub fn current(&self) -> Option<&str> {
240 if self.active {
248 if self.active {
241− self.items.get(self.selected).map(|s| s.as_str())
249+ self.items.get(self.selected).map(|cmd| cmd.name.as_str())
247− pub fn items(&self) -> &[String] {
255+ pub fn items(&self) -> &[VibeboxCommand] {
248 &self.items
256 &self.items
@@ -257,6 +265,32 @@ impl CompletionState {
268+impl Default for VibeboxCommands {
269+ fn default() -> Self {
271+ items: vec![VibeboxCommand {
272+ name: ":help".to_string(),
273+ description: "Show Vibebox commands.".to_string(),
281+#[derive(Debug, Default, Clone)]
282+pub struct VibeboxCommand {
284+ pub description: String,
287+impl VibeboxCommand {
288+ pub fn new(name: String) -> Self {
289+ let description = command_description(&name).to_string();
290+ Self { name, description }
260#[allow(dead_code)]
294#[allow(dead_code)]
261#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
295#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
262pub struct LayoutAreas {
296pub struct LayoutAreas {
@@ -278,79 +312,63 @@ struct PageLayout {
280#[allow(dead_code)]
314#[allow(dead_code)]
281−pub fn compute_layout(
284− completion_items: usize,
315+pub fn compute_layout(area: Rect, input_height: u16, completion_items: usize) -> LayoutAreas {
285− completion_active: bool,
287 let header_height = header_height().min(area.height);
316 let header_height = header_height().min(area.height);
288 let mut remaining = area.height.saturating_sub(header_height);
317 let mut remaining = area.height.saturating_sub(header_height);
290 let input_height = input_height.max(1).min(remaining);
319 let input_height = input_height.max(1).min(remaining);
291 remaining = remaining.saturating_sub(input_height);
320 remaining = remaining.saturating_sub(input_height);
293− let (completion_height, status_height) = if completion_active {
322+ let completion_height = if completion_items == 0 {
294− let desired = (completion_items as u16).min(COMPLETIONS_MAX_HEIGHT);
323+ 0
295− let height = desired.min(remaining);
298− let height = STATUS_BAR_HEIGHT.min(remaining);
325+ let desired = (completion_items as u16).saturating_add(2);
299− (0, height)
326+ desired
328+ let completion_height = completion_height.min(remaining);
302− remaining = remaining.saturating_sub(completion_height + status_height);
304− let terminal_height = remaining;
330+ let terminal_height = 0;
306− let chunks = Layout::default()
332+ let mut y = 0u16;
307− .direction(Direction::Vertical)
333+ let header = Rect::new(area.x, area.y + y, area.width, header_height);
309− Constraint::Length(header_height),
334+ y = y.saturating_add(header_height);
310− Constraint::Length(terminal_height),
335+ let terminal = Rect::new(area.x, area.y + y, area.width, terminal_height);
311− Constraint::Length(input_height),
336+ let input = Rect::new(area.x, area.y + y, area.width, input_height);
312− Constraint::Length(completion_height),
337+ y = y.saturating_add(input_height);
313− Constraint::Length(status_height),
338+ let completions = Rect::new(area.x, area.y + y, area.width, completion_height);
314− ])
339+ let status = Rect::new(area.x, area.y + y, area.width, 0);
317 LayoutAreas {
341 LayoutAreas {
318− header: chunks[0],
342+ header,
319− terminal: chunks[1],
343+ terminal,
320− input: chunks[2],
344+ input,
321− completions: chunks[3],
345+ completions,
322− status: chunks[4],
346+ status,
326fn compute_page_layout(app: &AppState, width: u16) -> PageLayout {
350fn compute_page_layout(app: &AppState, width: u16) -> PageLayout {
327 let header_height = header_height();
351 let header_height = header_height();
328− let terminal_height = terminal_height(app, width);
352+ let input_height = 0;
329− let input_height = app.input_height_for_width(width);
353+ let completion_items = app.commands.items().len();
330− let (completion_height, status_height) = if app.completions.is_active() {
354+ let completion_height = if completion_items == 0 {
331− let desired = (app.completions.items().len() as u16).min(COMPLETIONS_MAX_HEIGHT);
355+ 0
332− (desired.saturating_add(2), 0)
334− (0, STATUS_BAR_HEIGHT)
357+ (completion_items as u16).saturating_add(2)
337 let total_height = header_height
359 let total_height = header_height
338− .saturating_add(terminal_height)
339 .saturating_add(input_height)
360 .saturating_add(input_height)
340 .saturating_add(completion_height)
361 .saturating_add(completion_height)
341− .saturating_add(status_height)
344 let mut y = 0u16;
364 let mut y = 0u16;
345 let header = Rect::new(0, y, width, header_height);
365 let header = Rect::new(0, y, width, header_height);
346 y = y.saturating_add(header_height);
366 y = y.saturating_add(header_height);
347− let terminal = Rect::new(0, y, width, terminal_height);
367+ let terminal = Rect::new(0, y, width, 0);
348− y = y.saturating_add(terminal_height);
349 let input = Rect::new(0, y, width, input_height);
368 let input = Rect::new(0, y, width, input_height);
350 y = y.saturating_add(input_height);
369 y = y.saturating_add(input_height);
351 let completions = Rect::new(0, y, width, completion_height);
370 let completions = Rect::new(0, y, width, completion_height);
352− y = y.saturating_add(completion_height);
353− let status = Rect::new(0, y, width, status_height);
371+ let status = Rect::new(0, y, width, 0);
355 PageLayout {
373 PageLayout {
@@ -362,23 +380,6 @@ fn compute_page_layout(app: &AppState, width: u16) -> PageLayout {
365−fn terminal_height(app: &AppState, width: u16) -> u16 {
369− let lines: Vec<Line> = app
372− .map(|line| Line::from(line.as_str()))
374− let block = Block::default().borders(Borders::ALL).title("Terminal");
375− let paragraph = Paragraph::new(Text::from_iter(lines))
377− .wrap(Wrap { trim: true });
378− let count = paragraph.line_count(width).max(1);
379− (count.min(u16::MAX as usize)) as u16
382fn header_height() -> u16 {
383fn header_height() -> u16 {
383 let banner_height = ASCII_BANNER.len() as u16;
384 let banner_height = ASCII_BANNER.len() as u16;
384 let welcome_height = 1;
385 let welcome_height = 1;
@@ -389,21 +390,12 @@ fn header_height() -> u16 {
389pub async fn run_tui(mut app: AppState) -> Result<()> {
390pub async fn run_tui(mut app: AppState) -> Result<()> {
390 let mut terminal = TerminalGuard::init()?;
391 let mut terminal = TerminalGuard::init()?;
391 let mut events = EventStream::new();
392 let mut events = EventStream::new();
392− let mut tick = tokio::time::interval(Duration::from_millis(250));
395 terminal.draw(|frame| render(frame, &mut app))?;
395 terminal.draw(|frame| render(frame, &mut app))?;
398− _ = tick.tick() => {
399− app.tick = app.tick.wrapping_add(1);
400− app.spinner = (app.spinner + 1) % SPINNER_FRAMES.len();
402− event = events.next() => {
403− if let Some(event) = event {
397+ if let Some(event) = events.next().await {
404− handle_event(event?, &mut app);
398+ handle_event(event?, &mut app);
409 if app.should_quit {
401 if app.should_quit {
@@ -414,6 +406,161 @@ pub async fn run_tui(mut app: AppState) -> Result<()> {
409+pub fn render_tui_once(app: &mut AppState) -> Result<()> {
410+ let (width, _) = crossterm::terminal::size()?;
415+ let buffer = render_static_buffer(app, width);
416+ let mut stdout = io::stdout();
417+ execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
418+ write_buffer_with_style(&buffer, &mut stdout)?;
423+pub fn render_commands_component(app: &mut AppState) -> Result<()> {
424+ let (width, _) = crossterm::terminal::size()?;
429+ let command_count = app.commands.items().len() as u16;
430+ let height = if command_count == 0 {
433+ command_count.saturating_add(2)
439+ let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
440+ let area = Rect::new(0, 0, width, height);
441+ render_completions(&mut buffer, area, app);
443+ let mut stdout = io::stdout();
444+ write_buffer_with_style(&buffer, &mut stdout)?;
449+fn render_static_buffer(app: &mut AppState, width: u16) -> Buffer {
450+ let layout = compute_page_layout(app, width);
451+ let content_height = layout.total_height.max(1);
452+ let mut buffer = Buffer::empty(Rect::new(0, 0, width, content_height));
453+ render_header(&mut buffer, layout.header, app);
454+ render_completions(&mut buffer, layout.completions, app);
458+fn write_buffer_with_style(buffer: &Buffer, out: &mut impl Write) -> io::Result<()> {
459+ let area = buffer.area;
460+ let mut current_fg: Option<CrosstermColor> = None;
461+ let mut current_bg: Option<CrosstermColor> = None;
462+ let mut current_modifier: Option<ratatui::style::Modifier> = None;
464+ for y in 0..area.height {
465+ for x in 0..area.width {
466+ let cell = &buffer[(x, y)];
471+ let fg = map_color(cell.fg);
472+ let bg = map_color(cell.bg);
473+ let modifier = cell.modifier;
474+ if current_fg != Some(fg)
475+ || current_bg != Some(bg)
476+ || current_modifier != Some(modifier)
478+ queue!(out, SetAttribute(Attribute::Reset))?;
479+ queue!(out, SetForegroundColor(fg), SetBackgroundColor(bg))?;
480+ queue_modifier(out, modifier)?;
481+ current_fg = Some(fg);
482+ current_bg = Some(bg);
483+ current_modifier = Some(modifier);
486+ let symbol = cell.symbol();
487+ if symbol.is_empty() {
488+ queue!(out, Print(" "))?;
490+ queue!(out, Print(symbol))?;
495+ SetAttribute(Attribute::Reset),
496+ SetForegroundColor(CrosstermColor::Reset),
497+ SetBackgroundColor(CrosstermColor::Reset),
502+ current_modifier = None;
508+fn map_color(color: ratatui::style::Color) -> CrosstermColor {
510+ ratatui::style::Color::Reset => CrosstermColor::Reset,
511+ ratatui::style::Color::Black => CrosstermColor::Black,
512+ ratatui::style::Color::Red => CrosstermColor::DarkRed,
513+ ratatui::style::Color::Green => CrosstermColor::DarkGreen,
514+ ratatui::style::Color::Yellow => CrosstermColor::DarkYellow,
515+ ratatui::style::Color::Blue => CrosstermColor::DarkBlue,
516+ ratatui::style::Color::Magenta => CrosstermColor::DarkMagenta,
517+ ratatui::style::Color::Cyan => CrosstermColor::DarkCyan,
518+ ratatui::style::Color::Gray => CrosstermColor::Grey,
519+ ratatui::style::Color::DarkGray => CrosstermColor::DarkGrey,
520+ ratatui::style::Color::LightRed => CrosstermColor::Red,
521+ ratatui::style::Color::LightGreen => CrosstermColor::Green,
522+ ratatui::style::Color::LightYellow => CrosstermColor::Yellow,
523+ ratatui::style::Color::LightBlue => CrosstermColor::Blue,
524+ ratatui::style::Color::LightMagenta => CrosstermColor::Magenta,
525+ ratatui::style::Color::LightCyan => CrosstermColor::Cyan,
526+ ratatui::style::Color::White => CrosstermColor::White,
527+ ratatui::style::Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
528+ ratatui::style::Color::Indexed(i) => CrosstermColor::AnsiValue(i),
532+fn queue_modifier(out: &mut impl Write, modifier: ratatui::style::Modifier) -> io::Result<()> {
533+ use ratatui::style::Modifier;
534+ if modifier.contains(Modifier::BOLD) {
535+ queue!(out, SetAttribute(Attribute::Bold))?;
537+ if modifier.contains(Modifier::DIM) {
538+ queue!(out, SetAttribute(Attribute::Dim))?;
540+ if modifier.contains(Modifier::ITALIC) {
541+ queue!(out, SetAttribute(Attribute::Italic))?;
543+ if modifier.contains(Modifier::UNDERLINED) {
544+ queue!(out, SetAttribute(Attribute::Underlined))?;
546+ if modifier.contains(Modifier::SLOW_BLINK) {
547+ queue!(out, SetAttribute(Attribute::SlowBlink))?;
549+ if modifier.contains(Modifier::RAPID_BLINK) {
550+ queue!(out, SetAttribute(Attribute::RapidBlink))?;
552+ if modifier.contains(Modifier::REVERSED) {
553+ queue!(out, SetAttribute(Attribute::Reverse))?;
555+ if modifier.contains(Modifier::HIDDEN) {
556+ queue!(out, SetAttribute(Attribute::Hidden))?;
558+ if modifier.contains(Modifier::CROSSED_OUT) {
559+ queue!(out, SetAttribute(Attribute::CrossedOut))?;
417fn handle_event(event: CrosstermEvent, app: &mut AppState) {
564fn handle_event(event: CrosstermEvent, app: &mut AppState) {
418 match event {
565 match event {
419 CrosstermEvent::Key(key) => handle_key_event(key, app),
566 CrosstermEvent::Key(key) => handle_key_event(key, app),
@@ -432,16 +579,16 @@ fn handle_key_event(key: KeyEvent, app: &mut AppState) {
435− if app.completions.is_active() {
582+ if app.commands.is_active() {
436 match key.code {
583 match key.code {
437− KeyCode::Esc => app.deactivate_completions(),
584+ KeyCode::Esc => app.deactivate_commands(),
438− KeyCode::Up => app.completions.previous(),
585+ KeyCode::Up => app.commands.previous(),
439− KeyCode::Down => app.completions.next(),
586+ KeyCode::Down => app.commands.next(),
440 KeyCode::Enter => {
587 KeyCode::Enter => {
441− if let Some(selection) = app.completions.current() {
588+ if let Some(selection) = app.commands.current() {
442 app.input.insert_str(selection);
589 app.input.insert_str(selection);
444− app.deactivate_completions();
591+ app.deactivate_commands();
@@ -464,7 +611,7 @@ fn handle_key_event(key: KeyEvent, app: &mut AppState) {
464 app.page_scroll = usize::MAX;
611 app.page_scroll = usize::MAX;
467− KeyCode::Tab => app.toggle_completions(default_completions()),
614+ KeyCode::Tab => app.toggle_commands(),
468 KeyCode::Char(c)
615 KeyCode::Char(c)
470 .modifiers
617 .modifiers
@@ -547,8 +694,13 @@ fn input_from_key_event(key: KeyEvent) -> Input {
550−fn default_completions() -> Vec<String> {
697+fn command_description(command: &str) -> &'static str {
551− vec![":help".to_string(), ":new".to_string(), ":exit".to_string()]
698+ match command {
699+ ":help" => "Show Vibebox commands.",
700+ ":new" => "Create a new session.",
701+ ":exit" => "Exit Vibebox.",
554fn render(frame: &mut Frame<'_>, app: &mut AppState) {
706fn render(frame: &mut Frame<'_>, app: &mut AppState) {
@@ -563,14 +715,7 @@ fn render(frame: &mut Frame<'_>, app: &mut AppState) {
564 let mut buffer = Buffer::empty(Rect::new(0, 0, viewport.width, content_height));
716 let mut buffer = Buffer::empty(Rect::new(0, 0, viewport.width, content_height));
565 render_header(&mut buffer, layout.header, app);
717 render_header(&mut buffer, layout.header, app);
566− render_terminal(&mut buffer, layout.terminal, app);
567− let cursor_pos = render_input(&mut buffer, layout.input, app);
569− if app.completions.is_active() {
570− render_completions(&mut buffer, layout.completions, app);
718+ render_completions(&mut buffer, layout.completions, app);
572− render_status(&mut buffer, layout.status, app);
575 let max_scroll = content_height.saturating_sub(viewport.height);
720 let max_scroll = content_height.saturating_sub(viewport.height);
576 app.page_scroll = app.page_scroll.min(max_scroll as usize);
721 app.page_scroll = app.page_scroll.min(max_scroll as usize);
@@ -586,12 +731,6 @@ fn render(frame: &mut Frame<'_>, app: &mut AppState) {
586 view[(x, y)] = buffer[(x, src_y)].clone();
731 view[(x, y)] = buffer[(x, src_y)].clone();
590− if let Some((x, y)) = cursor_pos {
591− if y >= scroll && y < scroll.saturating_add(viewport.height) {
592− frame.set_cursor_position((x, y - scroll));
597fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
736fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
@@ -645,80 +784,34 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
645 .render(header_chunks[2], buffer);
784 .render(header_chunks[2], buffer);
648−fn render_terminal(buffer: &mut Buffer, area: Rect, app: &AppState) {
649− if area.height == 0 {
652− let lines = app.history.iter().map(|line| Line::from(line.as_str()));
653− let block = Block::default().borders(Borders::ALL).title("Terminal");
654− let paragraph = Paragraph::new(Text::from_iter(lines))
656− .wrap(Wrap { trim: true });
658− paragraph.render(area, buffer);
661−fn render_input(buffer: &mut Buffer, area: Rect, app: &mut AppState) -> Option<(u16, u16)> {
662− if area.height == 0 || area.width == 0 {
665− app.input.render(area, buffer);
666− let cursor = app.input.cursor();
667− let inner = match app.input.block() {
668− Some(block) => block.inner(area),
671− let x = inner.x.saturating_add(cursor.1 as u16);
672− let y = inner.y.saturating_add(cursor.0 as u16);
673− if x < inner.x.saturating_add(inner.width) && y < inner.y.saturating_add(inner.height) {
680fn render_completions(buffer: &mut Buffer, area: Rect, app: &mut AppState) {
787fn render_completions(buffer: &mut Buffer, area: Rect, app: &mut AppState) {
681 if area.height == 0 {
788 if area.height == 0 {
685 let items: Vec<ListItem<'_>> = app
792 let items: Vec<ListItem<'_>> = app
686− .completions
793+ .commands
689− .map(|item| ListItem::new(Line::from(item.as_str())))
796+ .map(|cmd| ListItem::new(Line::from(format!("{} {}", cmd.name, cmd.description))))
690 .collect();
797 .collect();
692 let list = List::new(items)
799 let list = List::new(items)
693− .block(Block::default().borders(Borders::ALL).title("Completions"))
802+ .borders(Borders::ALL)
803+ .title("Vibebox Commands"),
694 .highlight_style(Style::default().fg(Color::Yellow));
805 .highlight_style(Style::default().fg(Color::Yellow));
696 let mut state = ListState::default();
807 let mut state = ListState::default();
697− if app.completions.is_active() && !app.completions.items().is_empty() {
808+ if app.commands.is_active() && !app.commands.items().is_empty() {
698− state.select(Some(app.completions.selected()));
809+ state.select(Some(app.commands.selected()));
701 StatefulWidget::render(list, area, buffer, &mut state);
812 StatefulWidget::render(list, area, buffer, &mut state);
704−fn render_status(buffer: &mut Buffer, area: Rect, app: &AppState) {
705− if area.height == 0 {
709− let spinner = SPINNER_FRAMES[app.spinner % SPINNER_FRAMES.len()];
710− let status = Paragraph::new(Line::from(vec![
711− Span::styled(":help", Style::default().fg(Color::DarkGray)),
714− format!("tick {} {}", app.tick, spinner),
715− Style::default().fg(Color::DarkGray),
719− status.render(area, buffer);
722struct TerminalGuard {
815struct TerminalGuard {
723 terminal: Terminal<CrosstermBackend<Stdout>>,
816 terminal: Terminal<CrosstermBackend<Stdout>>,
@@ -755,29 +848,29 @@ mod tests {
755 use super::*;
848 use super::*;
758− fn layout_without_completions_reserves_status_bar() {
851+ fn layout_without_completions_hides_status_bar() {
759 let area = Rect::new(0, 0, 80, 30);
852 let area = Rect::new(0, 0, 80, 30);
760− let layout = compute_layout(area, 3, 0, false);
853+ let layout = compute_layout(area, 3, 0);
762 assert_eq!(layout.header.height, header_height());
855 assert_eq!(layout.header.height, header_height());
763− assert_eq!(layout.status.height, STATUS_BAR_HEIGHT);
856+ assert_eq!(layout.status.height, 0);
764 assert_eq!(layout.completions.height, 0);
857 assert_eq!(layout.completions.height, 0);
765− assert!(layout.terminal.height > 0);
858+ assert_eq!(layout.terminal.height, 0);
769 fn layout_with_completions_hides_status_bar() {
862 fn layout_with_completions_hides_status_bar() {
770 let area = Rect::new(0, 0, 80, 30);
863 let area = Rect::new(0, 0, 80, 30);
771− let layout = compute_layout(area, 4, 3, true);
864+ let layout = compute_layout(area, 4, 3);
773 assert_eq!(layout.status.height, 0);
866 assert_eq!(layout.status.height, 0);
774− assert_eq!(layout.completions.height, 3);
867+ assert_eq!(layout.completions.height, 5);
778 fn layout_clamps_when_space_is_tight() {
871 fn layout_clamps_when_space_is_tight() {
779 let area = Rect::new(0, 0, 80, header_height() + 1);
872 let area = Rect::new(0, 0, 80, header_height() + 1);
780− let layout = compute_layout(area, 3, 10, true);
873+ let layout = compute_layout(area, 3, 10);
782 assert_eq!(layout.header.height, header_height());
875 assert_eq!(layout.header.height, header_height());
783 assert_eq!(layout.input.height, 1);
876 assert_eq!(layout.input.height, 1);
@@ -785,31 +878,31 @@ mod tests {
788− fn completion_state_wraps_navigation() {
881+ fn commands_wrap_navigation() {
789− let mut completions = CompletionState::default();
882+ let mut commands = VibeboxCommands::default();
790− completions.set_items(vec!["a".into(), "b".into(), "c".into()]);
883+ commands.set_items(vec![":new".into(), ":exit".into(), ":help".into()]);
791− completions.activate();
884+ commands.activate();
793− assert_eq!(completions.current(), Some("a"));
886+ assert_eq!(commands.current(), Some(":new"));
795− completions.next();
888+ commands.next();
796− assert_eq!(completions.current(), Some("b"));
889+ assert_eq!(commands.current(), Some(":exit"));
798− completions.next();
891+ commands.next();
799− completions.next();
892+ commands.next();
800− assert_eq!(completions.current(), Some("a"));
893+ assert_eq!(commands.current(), Some(":new"));
802− completions.previous();
895+ commands.previous();
803− assert_eq!(completions.current(), Some("c"));
896+ assert_eq!(commands.current(), Some(":help"));
807− fn completion_state_is_inactive_when_empty() {
900+ fn commands_inactive_when_empty() {
808− let mut completions = CompletionState::default();
901+ let mut commands = VibeboxCommands::default();
809− completions.activate();
902+ commands.activate();
811− assert!(!completions.is_active());
904+ assert!(!commands.is_active());
812− assert_eq!(completions.current(), None);
905+ assert_eq!(commands.current(), None);