From 741e6fbcfdedfbc24d312cca33a66afffebcb4c3 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 27 Mar 2026 20:04:12 +0400 Subject: [PATCH] v0.0.18: proper line editing in TUI input Keyboard shortcuts: - Left/Right: move cursor - Home / Ctrl+A: beginning of line - End / Ctrl+E: end of line - Alt+Left/Right: word jump - Alt+Backspace: delete word back - Ctrl+W: delete word back - Ctrl+U: clear entire line - Ctrl+K: kill to end of line - Delete: delete char at cursor - Backspace: delete char before cursor Cursor position tracked, chars insert at cursor (not just append). Co-Authored-By: Claude Opus 4.6 (1M context) --- warzone/Cargo.toml | 2 +- warzone/crates/warzone-client/src/tui/app.rs | 78 +++++++++++++++++++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/warzone/Cargo.toml b/warzone/Cargo.toml index d22c4bc..5a44501 100644 --- a/warzone/Cargo.toml +++ b/warzone/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.0.17" +version = "0.0.18" edition = "2021" license = "MIT" rust-version = "1.75" diff --git a/warzone/crates/warzone-client/src/tui/app.rs b/warzone/crates/warzone-client/src/tui/app.rs index aeb0d0e..667aa14 100644 --- a/warzone/crates/warzone-client/src/tui/app.rs +++ b/warzone/crates/warzone-client/src/tui/app.rs @@ -52,6 +52,7 @@ pub struct App { pub peer_fp: Option, pub server_url: String, pub should_quit: bool, + pub cursor_pos: usize, pub last_dm_peer: Arc>>, /// Track receipt status for messages we sent, keyed by message ID. pub receipts: Arc>>, @@ -113,6 +114,7 @@ impl App { server_url, should_quit: false, last_dm_peer: Arc::new(Mutex::new(None)), + cursor_pos: 0, receipts: Arc::new(Mutex::new(HashMap::new())), pending_files: Arc::new(Mutex::new(HashMap::new())), } @@ -227,7 +229,7 @@ impl App { frame.render_widget(input_widget, chunks[2]); // Cursor - let x = (self.input.len() as u16 + 1).min(chunks[2].width - 2); + let x = (self.cursor_pos as u16 + 1).min(chunks[2].width - 2); frame.set_cursor_position((chunks[2].x + x, chunks[2].y + 1)); } @@ -239,6 +241,7 @@ impl App { ) { let text = self.input.trim().to_string(); self.input.clear(); + self.cursor_pos = 0; if text.is_empty() { return; @@ -1520,11 +1523,78 @@ pub async fn run_tui( KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { app.should_quit = true; } - KeyCode::Backspace => { - app.input.pop(); + // Alt+Backspace / Ctrl+W: delete word before cursor + KeyCode::Backspace if key.modifiers.contains(KeyModifiers::ALT) => { + if app.cursor_pos > 0 { + let before = &app.input[..app.cursor_pos]; + let new_pos = before.trim_end().rfind(' ').map(|i| i + 1).unwrap_or(0); + app.input.drain(new_pos..app.cursor_pos); + app.cursor_pos = new_pos; + } } + // Backspace: delete char before cursor + KeyCode::Backspace => { + if app.cursor_pos > 0 { + app.input.remove(app.cursor_pos - 1); + app.cursor_pos -= 1; + } + } + // Delete: delete char at cursor + KeyCode::Delete => { + if app.cursor_pos < app.input.len() { + app.input.remove(app.cursor_pos); + } + } + // Left arrow + KeyCode::Left => { + if key.modifiers.contains(KeyModifiers::ALT) { + // Alt+Left: word left + let before = &app.input[..app.cursor_pos]; + app.cursor_pos = before.rfind(' ').map(|i| i).unwrap_or(0); + } else if app.cursor_pos > 0 { + app.cursor_pos -= 1; + } + } + // Right arrow + KeyCode::Right => { + if key.modifiers.contains(KeyModifiers::ALT) { + // Alt+Right: word right + let after = &app.input[app.cursor_pos..]; + app.cursor_pos += after.find(' ').map(|i| i + 1).unwrap_or(after.len()); + } else if app.cursor_pos < app.input.len() { + app.cursor_pos += 1; + } + } + // Home / Ctrl+A + KeyCode::Home => { app.cursor_pos = 0; } + KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => { + app.cursor_pos = 0; + } + // End / Ctrl+E + KeyCode::End => { app.cursor_pos = app.input.len(); } + KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { + app.cursor_pos = app.input.len(); + } + // Ctrl+U: clear line + KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + app.input.clear(); + app.cursor_pos = 0; + } + // Ctrl+K: kill to end of line + KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { + app.input.truncate(app.cursor_pos); + } + // Ctrl+W: delete word back + KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => { + let before = &app.input[..app.cursor_pos]; + let new_pos = before.trim_end().rfind(' ').map(|i| i + 1).unwrap_or(0); + app.input.drain(new_pos..app.cursor_pos); + app.cursor_pos = new_pos; + } + // Regular char: insert at cursor KeyCode::Char(c) => { - app.input.push(c); + app.input.insert(app.cursor_pos, c); + app.cursor_pos += 1; } KeyCode::Esc => { app.should_quit = true;