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) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 20:04:12 +04:00
parent a4405b4976
commit 741e6fbcfd
2 changed files with 75 additions and 5 deletions

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "0.0.17"
version = "0.0.18"
edition = "2021"
license = "MIT"
rust-version = "1.75"

View File

@@ -52,6 +52,7 @@ pub struct App {
pub peer_fp: Option<String>,
pub server_url: String,
pub should_quit: bool,
pub cursor_pos: usize,
pub last_dm_peer: Arc<Mutex<Option<String>>>,
/// Track receipt status for messages we sent, keyed by message ID.
pub receipts: Arc<Mutex<HashMap<String, ReceiptStatus>>>,
@@ -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;