Cleanup
This commit is contained in:
parent
7b9525ef95
commit
b043fed596
3 changed files with 73 additions and 93 deletions
30
PLAN.md
30
PLAN.md
|
|
@ -1,35 +1,5 @@
|
||||||
# Implementation Plan
|
# Implementation Plan
|
||||||
|
|
||||||
## Phase 2: Vim Modes and Navigation
|
|
||||||
|
|
||||||
**Done when:** Fluid mode switching and scrolling feels vim-native
|
|
||||||
|
|
||||||
### 2.3 Command Parser + Execution
|
|
||||||
- Parse command buffer on enter: `:quit` -> `Quit`, `:clear` -> clear messages and history, `:q` alias for `:quit`
|
|
||||||
- Return appropriate `LoopControl` or mutate `AppState` directly for display-only commands
|
|
||||||
- Unknown commands -> show error in a transient status area
|
|
||||||
|
|
||||||
### 2.4 Status Bar + Command Overlay
|
|
||||||
- Add a status bar row at the bottom of the layout (below input pane)
|
|
||||||
- Status bar shows: current mode (`-- NORMAL --`, `-- INSERT --`), token totals (placeholder for now)
|
|
||||||
- Style: bold, color-coded per mode (following vim convention)
|
|
||||||
- Command input renders as a floating bar near the top of the screen (overlay on the output pane), not in the status bar
|
|
||||||
- Command overlay: shows `:` prefix + partial command buffer, only visible in Command mode
|
|
||||||
- Overlay dismisses on Esc or Enter
|
|
||||||
|
|
||||||
### 2.6 Input Pane Behavior by Mode
|
|
||||||
- Normal mode: input pane shows last draft (unsent) message or is empty, not editable
|
|
||||||
- Insert mode: input pane is editable (current behavior)
|
|
||||||
- Command mode: floating overlay near top shows `:` prefix + command buffer; input pane unchanged
|
|
||||||
- Cursor visibility: show cursor in Insert and Command modes, hide in Normal
|
|
||||||
|
|
||||||
### 2.7 Phase 2 Unit Tests
|
|
||||||
- Mode transitions: Normal->Insert->Normal, Normal->Command->Normal, Command execute
|
|
||||||
- Key dispatch: correct handler called per mode
|
|
||||||
- Command parser: `:quit`, `:clear`, `:q`, unknown command
|
|
||||||
- Scroll clamping: j/k at boundaries, G/gg
|
|
||||||
- Existing Phase 1 input tests still pass (insert mode behavior unchanged)
|
|
||||||
|
|
||||||
## Phase 3: Tool Execution
|
## Phase 3: Tool Execution
|
||||||
- `Tool` trait, `ToolRegistry`, core tools (`read_file`, `write_file`, `shell_exec`)
|
- `Tool` trait, `ToolRegistry`, core tools (`read_file`, `write_file`, `shell_exec`)
|
||||||
- Tool definitions in API requests, parse tool-use responses
|
- Tool definitions in API requests, parse tool-use responses
|
||||||
|
|
|
||||||
4
TODO.md
4
TODO.md
|
|
@ -2,3 +2,7 @@
|
||||||
|
|
||||||
- Move keyboard/event reads in the TUI to a separate thread or async/io loop
|
- Move keyboard/event reads in the TUI to a separate thread or async/io loop
|
||||||
- Keep UI and orchestrator in sync (i.e. messages display out of order if you queue up many.)
|
- Keep UI and orchestrator in sync (i.e. messages display out of order if you queue up many.)
|
||||||
|
- `update_scroll` auto-follows in Insert mode, yanking viewport to bottom on mode switch. Only auto-follow when new content arrives (in `drain_ui_events`), not every frame.
|
||||||
|
- `G` sets scroll to `u16::MAX` and relies on `update_scroll` clamping. Compute actual max_scroll inline, or document the contract that `update_scroll` must always run before render.
|
||||||
|
- `:clear` clears TUI state immediately but sends `ClearHistory` to orchestrator async. A mid-stream response can ghost back in after clear. Need synchronization (e.g. clear on `TurnComplete`, or have orchestrator ack the clear).
|
||||||
|
- Command overlay width: `(out.width / 2).max(80)` makes overlay full-bleed on 80-col terminals. Consider `.max(40)` or a different minimum.
|
||||||
|
|
|
||||||
132
src/tui/mod.rs
132
src/tui/mod.rs
|
|
@ -22,7 +22,7 @@ use ratatui::backend::CrosstermBackend;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Clear, Paragraph, Wrap};
|
||||||
use ratatui::{Frame, Terminal};
|
use ratatui::{Frame, Terminal};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
@ -43,17 +43,16 @@ pub enum TuiError {
|
||||||
/// Mode transitions:
|
/// Mode transitions:
|
||||||
/// Insert --Esc--> Normal
|
/// Insert --Esc--> Normal
|
||||||
/// Normal --i--> Insert
|
/// Normal --i--> Insert
|
||||||
/// Normal --:--> Command(buffer)
|
/// Normal --:--> Command
|
||||||
/// Command --Esc/Enter--> Normal
|
/// Command --Esc/Enter--> Normal
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
/// Navigation and command entry. Keys are interpreted as commands, not text.
|
/// Navigation and command entry. Keys are interpreted as commands, not text.
|
||||||
Normal,
|
Normal,
|
||||||
/// Text input. Printable keys append to the input buffer.
|
/// Text input. Printable keys append to the input buffer.
|
||||||
Insert,
|
Insert,
|
||||||
/// Ex-style command entry. The `String` holds the partial command buffer
|
/// Ex-style command entry. The command buffer lives in `AppState::command_buffer`.
|
||||||
/// (without the leading `:`).
|
Command,
|
||||||
Command(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The UI-layer view of a conversation: rendered messages and the current input buffer.
|
/// The UI-layer view of a conversation: rendered messages and the current input buffer.
|
||||||
|
|
@ -66,6 +65,8 @@ pub struct AppState {
|
||||||
pub scroll: u16,
|
pub scroll: u16,
|
||||||
/// Current vim-style editing mode.
|
/// Current vim-style editing mode.
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
|
/// Partial command text in Command mode (without the leading `:`).
|
||||||
|
pub command_buffer: String,
|
||||||
/// Buffered keystrokes for multi-key chord resolution in Normal mode (e.g. `gg`).
|
/// Buffered keystrokes for multi-key chord resolution in Normal mode (e.g. `gg`).
|
||||||
pub pending_keys: Vec<char>,
|
pub pending_keys: Vec<char>,
|
||||||
/// Last-known viewport height (output pane lines). Updated each frame by `update_scroll`.
|
/// Last-known viewport height (output pane lines). Updated each frame by `update_scroll`.
|
||||||
|
|
@ -81,6 +82,7 @@ impl AppState {
|
||||||
input: String::new(),
|
input: String::new(),
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
mode: Mode::Insert,
|
mode: Mode::Insert,
|
||||||
|
command_buffer: String::new(),
|
||||||
pending_keys: Vec::new(),
|
pending_keys: Vec::new(),
|
||||||
viewport_height: 0,
|
viewport_height: 0,
|
||||||
status_error: None,
|
status_error: None,
|
||||||
|
|
@ -154,7 +156,7 @@ fn handle_key(key: Option<KeyEvent>, state: &mut AppState) -> Option<LoopControl
|
||||||
match &state.mode {
|
match &state.mode {
|
||||||
Mode::Normal => handle_normal_key(key, state),
|
Mode::Normal => handle_normal_key(key, state),
|
||||||
Mode::Insert => handle_insert_key(key, state),
|
Mode::Insert => handle_insert_key(key, state),
|
||||||
Mode::Command(_) => handle_command_key(key, state),
|
Mode::Command => handle_command_key(key, state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +210,8 @@ fn handle_normal_key(key: KeyEvent, state: &mut AppState) -> Option<LoopControl>
|
||||||
state.mode = Mode::Insert;
|
state.mode = Mode::Insert;
|
||||||
}
|
}
|
||||||
KeyCode::Char(':') if !is_ctrl && state.pending_keys.is_empty() => {
|
KeyCode::Char(':') if !is_ctrl && state.pending_keys.is_empty() => {
|
||||||
state.mode = Mode::Command(String::new());
|
state.command_buffer.clear();
|
||||||
|
state.mode = Mode::Command;
|
||||||
}
|
}
|
||||||
KeyCode::Char('j') if !is_ctrl && state.pending_keys.is_empty() => {
|
KeyCode::Char('j') if !is_ctrl && state.pending_keys.is_empty() => {
|
||||||
state.scroll = state.scroll.saturating_add(1);
|
state.scroll = state.scroll.saturating_add(1);
|
||||||
|
|
@ -255,7 +258,7 @@ fn handle_insert_key(key: KeyEvent, state: &mut AppState) -> Option<LoopControl>
|
||||||
state.mode = Mode::Normal;
|
state.mode = Mode::Normal;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
state.input.push(c);
|
state.input.push(c);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -307,30 +310,21 @@ fn execute_command(buf: &str, state: &mut AppState) -> Option<LoopControl> {
|
||||||
/// | Backspace | Pop from buffer; if empty, return to Normal |
|
/// | Backspace | Pop from buffer; if empty, return to Normal |
|
||||||
/// | Char(c) | Append to command buffer |
|
/// | Char(c) | Append to command buffer |
|
||||||
fn handle_command_key(key: KeyEvent, state: &mut AppState) -> Option<LoopControl> {
|
fn handle_command_key(key: KeyEvent, state: &mut AppState) -> Option<LoopControl> {
|
||||||
// Extract the command buffer. We need ownership to mutate it.
|
|
||||||
let mut buf = match std::mem::replace(&mut state.mode, Mode::Normal) {
|
|
||||||
Mode::Command(b) => b,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
// Already set to Normal above.
|
state.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
return execute_command(&buf, state);
|
state.mode = Mode::Normal;
|
||||||
|
return execute_command(&state.command_buffer.clone(), state);
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
buf.pop();
|
state.command_buffer.pop();
|
||||||
state.mode = Mode::Command(buf);
|
|
||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
buf.push(c);
|
state.command_buffer.push(c);
|
||||||
state.mode = Mode::Command(buf);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
state.mode = Mode::Command(buf);
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -407,6 +401,20 @@ fn update_scroll(state: &mut AppState, area: Rect) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute the overlay rectangle for the command palette, centered on `output_area`.
|
||||||
|
fn command_overlay_rect(output_area: Rect) -> Rect {
|
||||||
|
let overlay_w = (output_area.width / 2).max(80).min(output_area.width);
|
||||||
|
let overlay_h: u16 = 3; // border + content + border
|
||||||
|
let overlay_x = output_area.x + (output_area.width.saturating_sub(overlay_w)) / 2;
|
||||||
|
let overlay_y = output_area.y + (output_area.height.saturating_sub(overlay_h)) / 2;
|
||||||
|
Rect {
|
||||||
|
x: overlay_x,
|
||||||
|
y: overlay_y,
|
||||||
|
width: overlay_w,
|
||||||
|
height: overlay_h.min(output_area.height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Render the full TUI into `frame`.
|
/// Render the full TUI into `frame`.
|
||||||
///
|
///
|
||||||
/// Layout (top to bottom):
|
/// Layout (top to bottom):
|
||||||
|
|
@ -452,22 +460,11 @@ fn render(frame: &mut Frame, state: &AppState) {
|
||||||
frame.render_widget(output, chunks[0]);
|
frame.render_widget(output, chunks[0]);
|
||||||
|
|
||||||
// --- Command overlay (floating box centered on output pane) ---
|
// --- Command overlay (floating box centered on output pane) ---
|
||||||
if let Mode::Command(ref buf) = state.mode {
|
if state.mode == Mode::Command {
|
||||||
let out = chunks[0];
|
let overlay_area = command_overlay_rect(chunks[0]);
|
||||||
let overlay_w = (out.width / 2).max(80).min(out.width);
|
|
||||||
let overlay_h = 3; // border + content + border
|
|
||||||
let overlay_x = out.x + (out.width.saturating_sub(overlay_w)) / 2;
|
|
||||||
let overlay_y = out.y + (out.height.saturating_sub(overlay_h)) / 2;
|
|
||||||
let overlay_area = Rect {
|
|
||||||
x: overlay_x,
|
|
||||||
y: overlay_y,
|
|
||||||
width: overlay_w,
|
|
||||||
height: overlay_h.min(out.height),
|
|
||||||
};
|
|
||||||
// Clear the area behind the overlay so it appears floating.
|
// Clear the area behind the overlay so it appears floating.
|
||||||
let clear = Paragraph::new("");
|
frame.render_widget(Clear, overlay_area);
|
||||||
frame.render_widget(clear, overlay_area);
|
let overlay = Paragraph::new(format!(":{}", state.command_buffer)).block(
|
||||||
let overlay = Paragraph::new(format!(":{buf}")).block(
|
|
||||||
Block::bordered()
|
Block::bordered()
|
||||||
.border_style(Style::default().fg(Color::Yellow))
|
.border_style(Style::default().fg(Color::Yellow))
|
||||||
.title("Command"),
|
.title("Command"),
|
||||||
|
|
@ -477,7 +474,7 @@ fn render(frame: &mut Frame, state: &AppState) {
|
||||||
|
|
||||||
// --- Input pane ---
|
// --- Input pane ---
|
||||||
let (input_title, input_style) = match state.mode {
|
let (input_title, input_style) = match state.mode {
|
||||||
Mode::Normal | Mode::Command(_) => (
|
Mode::Normal | Mode::Command => (
|
||||||
"Input (normal)",
|
"Input (normal)",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::DarkGray)
|
.fg(Color::DarkGray)
|
||||||
|
|
@ -499,15 +496,12 @@ fn render(frame: &mut Frame, state: &AppState) {
|
||||||
let cursor_y = chunks[1].y + 1; // inside the border
|
let cursor_y = chunks[1].y + 1; // inside the border
|
||||||
frame.set_cursor_position((cursor_x, cursor_y));
|
frame.set_cursor_position((cursor_x, cursor_y));
|
||||||
}
|
}
|
||||||
Mode::Command(ref buf) => {
|
Mode::Command => {
|
||||||
// Cursor in the floating overlay: recalculate overlay position
|
// Cursor in the floating overlay
|
||||||
let out = chunks[0];
|
let overlay = command_overlay_rect(chunks[0]);
|
||||||
let overlay_w = (out.width / 2).max(80).min(out.width);
|
|
||||||
let overlay_x = out.x + (out.width.saturating_sub(overlay_w)) / 2;
|
|
||||||
let overlay_y = out.y + (out.height.saturating_sub(3)) / 2;
|
|
||||||
// border(1) + ":" (1) + buf len
|
// border(1) + ":" (1) + buf len
|
||||||
let cursor_x = overlay_x + 1 + 1 + buf.len() as u16;
|
let cursor_x = overlay.x + 1 + 1 + state.command_buffer.len() as u16;
|
||||||
let cursor_y = overlay_y + 1; // inside the border
|
let cursor_y = overlay.y + 1; // inside the border
|
||||||
frame.set_cursor_position((cursor_x, cursor_y));
|
frame.set_cursor_position((cursor_x, cursor_y));
|
||||||
}
|
}
|
||||||
Mode::Normal => {} // no cursor
|
Mode::Normal => {} // no cursor
|
||||||
|
|
@ -523,7 +517,7 @@ fn render(frame: &mut Frame, state: &AppState) {
|
||||||
" INSERT ",
|
" INSERT ",
|
||||||
Style::default().bg(Color::Green).fg(Color::White),
|
Style::default().bg(Color::Green).fg(Color::White),
|
||||||
),
|
),
|
||||||
Mode::Command(_) => (
|
Mode::Command => (
|
||||||
" COMMAND ",
|
" COMMAND ",
|
||||||
Style::default().bg(Color::Yellow).fg(Color::Black),
|
Style::default().bg(Color::Yellow).fg(Color::Black),
|
||||||
),
|
),
|
||||||
|
|
@ -687,6 +681,13 @@ mod tests {
|
||||||
assert!(matches!(result, Some(LoopControl::Quit)));
|
assert!(matches!(result, Some(LoopControl::Quit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_ctrl_char_is_noop() {
|
||||||
|
let mut state = AppState::new();
|
||||||
|
handle_key(ctrl_key('a'), &mut state);
|
||||||
|
assert_eq!(state.input, "", "Ctrl+A should not insert 'a'");
|
||||||
|
}
|
||||||
|
|
||||||
// --- drain_ui_events tests ---
|
// --- drain_ui_events tests ---
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -801,13 +802,14 @@ mod tests {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Normal;
|
state.mode = Mode::Normal;
|
||||||
handle_key(make_key(KeyCode::Char(':')), &mut state);
|
handle_key(make_key(KeyCode::Char(':')), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Command(String::new()));
|
assert_eq!(state.mode, Mode::Command);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_esc_enters_normal() {
|
fn command_esc_enters_normal() {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command("q".to_string());
|
state.mode = Mode::Command;
|
||||||
|
state.command_buffer = "q".to_string();
|
||||||
handle_key(make_key(KeyCode::Esc), &mut state);
|
handle_key(make_key(KeyCode::Esc), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Normal);
|
assert_eq!(state.mode, Mode::Normal);
|
||||||
}
|
}
|
||||||
|
|
@ -815,7 +817,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn command_enter_enters_normal() {
|
fn command_enter_enters_normal() {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command("q".to_string());
|
state.mode = Mode::Command;
|
||||||
handle_key(make_key(KeyCode::Enter), &mut state);
|
handle_key(make_key(KeyCode::Enter), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Normal);
|
assert_eq!(state.mode, Mode::Normal);
|
||||||
}
|
}
|
||||||
|
|
@ -823,23 +825,25 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn command_chars_append_to_buffer() {
|
fn command_chars_append_to_buffer() {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command(String::new());
|
state.mode = Mode::Command;
|
||||||
handle_key(make_key(KeyCode::Char('q')), &mut state);
|
handle_key(make_key(KeyCode::Char('q')), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Command("q".to_string()));
|
assert_eq!(state.mode, Mode::Command);
|
||||||
|
assert_eq!(state.command_buffer, "q");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_backspace_empty_stays_in_command() {
|
fn command_backspace_empty_stays_in_command() {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command("ab".to_string());
|
state.mode = Mode::Command;
|
||||||
|
state.command_buffer = "ab".to_string();
|
||||||
handle_key(make_key(KeyCode::Backspace), &mut state);
|
handle_key(make_key(KeyCode::Backspace), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Command("a".to_string()));
|
assert_eq!(state.command_buffer, "a");
|
||||||
handle_key(make_key(KeyCode::Backspace), &mut state);
|
handle_key(make_key(KeyCode::Backspace), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Command(String::new()));
|
assert_eq!(state.command_buffer, "");
|
||||||
handle_key(make_key(KeyCode::Backspace), &mut state);
|
handle_key(make_key(KeyCode::Backspace), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Command(String::new()));
|
assert_eq!(state.command_buffer, "");
|
||||||
handle_key(make_key(KeyCode::Backspace), &mut state);
|
handle_key(make_key(KeyCode::Backspace), &mut state);
|
||||||
assert_eq!(state.mode, Mode::Command(String::new()));
|
assert_eq!(state.command_buffer, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -900,7 +904,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ctrl_c_quits_from_any_mode() {
|
fn ctrl_c_quits_from_any_mode() {
|
||||||
for mode in [Mode::Normal, Mode::Insert, Mode::Command("q".to_string())] {
|
for mode in [Mode::Normal, Mode::Insert, Mode::Command] {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = mode;
|
state.mode = mode;
|
||||||
let result = handle_key(ctrl_key('c'), &mut state);
|
let result = handle_key(ctrl_key('c'), &mut state);
|
||||||
|
|
@ -993,7 +997,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn command_enter_executes_quit() {
|
fn command_enter_executes_quit() {
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command("q".to_string());
|
state.mode = Mode::Command;
|
||||||
|
state.command_buffer = "q".to_string();
|
||||||
let result = handle_key(make_key(KeyCode::Enter), &mut state);
|
let result = handle_key(make_key(KeyCode::Enter), &mut state);
|
||||||
assert!(matches!(result, Some(LoopControl::Quit)));
|
assert!(matches!(result, Some(LoopControl::Quit)));
|
||||||
assert_eq!(state.mode, Mode::Normal);
|
assert_eq!(state.mode, Mode::Normal);
|
||||||
|
|
@ -1041,7 +1046,7 @@ mod tests {
|
||||||
let backend = TestBackend::new(80, 24);
|
let backend = TestBackend::new(80, 24);
|
||||||
let mut terminal = Terminal::new(backend).unwrap();
|
let mut terminal = Terminal::new(backend).unwrap();
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command("quit".to_string());
|
state.mode = Mode::Command;
|
||||||
terminal.draw(|frame| render(frame, &state)).unwrap();
|
terminal.draw(|frame| render(frame, &state)).unwrap();
|
||||||
let buf = terminal.backend().buffer().clone();
|
let buf = terminal.backend().buffer().clone();
|
||||||
let all_text: String = buf
|
let all_text: String = buf
|
||||||
|
|
@ -1057,7 +1062,8 @@ mod tests {
|
||||||
let backend = TestBackend::new(80, 24);
|
let backend = TestBackend::new(80, 24);
|
||||||
let mut terminal = Terminal::new(backend).unwrap();
|
let mut terminal = Terminal::new(backend).unwrap();
|
||||||
let mut state = AppState::new();
|
let mut state = AppState::new();
|
||||||
state.mode = Mode::Command("quit".to_string());
|
state.mode = Mode::Command;
|
||||||
|
state.command_buffer = "quit".to_string();
|
||||||
terminal.draw(|frame| render(frame, &state)).unwrap();
|
terminal.draw(|frame| render(frame, &state)).unwrap();
|
||||||
let buf = terminal.backend().buffer().clone();
|
let buf = terminal.backend().buffer().clone();
|
||||||
let all_text: String = buf
|
let all_text: String = buf
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue