From c6e50e579aa6caa5e0c2ac6905776a9f7f037d20 Mon Sep 17 00:00:00 2001 From: Nelo Puchades Date: Thu, 16 Jul 2026 09:26:05 +0200 Subject: [PATCH 1/2] fix: reset cursor to column 0 after raw-mode sub-prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sub-prompt readers emitted a bare `\n` on Enter (and on Ctrl+C/Ctrl+D) while still in raw mode, which line-feeds without a carriage return and leaves the cursor in the input's last column. The first output printed afterwards (e.g. the `✓` confirmation or a picker's first row) started at that column, appearing shifted far to the right. Emit `\r\n` instead so the next line begins at column 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/input.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/input.rs b/src/input.rs index 06ba272..dc305fa 100644 --- a/src/input.rs +++ b/src/input.rs @@ -353,16 +353,20 @@ fn read_plain_outcome( continue; } if let Some(outcome) = control_key_outcome(code, modifiers) { + // See the Enter branch: `\r\n`, not `\n`, while raw mode is on. redraw(&mut out, &chars, cursor, sel, true)?; - writeln!(out)?; + write!(out, "\r\n")?; out.flush()?; return Ok(outcome); } match code { KeyCode::Enter => { - // Clear any open menu, then drop to the next line. + // Clear any open menu, then drop to the next line. Raw mode + // is still active here, so a bare `\n` only line-feeds and + // leaves the cursor in the input's last column — `\r` returns + // it to column 0 so the caller's output isn't shifted right. redraw(&mut out, &chars, cursor, sel, true)?; - writeln!(out)?; + write!(out, "\r\n")?; out.flush()?; return Ok(ReadLineOutcome::Line(chars.iter().collect())); } From 00b1ae6079b84e91a564d704d4fbee383a0e6c3e Mon Sep 17 00:00:00 2001 From: Nelo Puchades Date: Thu, 16 Jul 2026 09:41:20 +0200 Subject: [PATCH 2/2] style: add breathing room around /backend and /model pickers Print a blank line between the option list and the "Select" prompt, and another between the typed selection and the response line, so the picker output no longer crowds together. Pairs with the raw-mode cursor reset so the response lands aligned at column 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commands/config_cmds.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/config_cmds.rs b/src/commands/config_cmds.rs index b5b89a5..d435a88 100644 --- a/src/commands/config_cmds.rs +++ b/src/commands/config_cmds.rs @@ -510,8 +510,12 @@ pub(super) async fn cmd_backend(args: &str, state: &mut AppState) -> Result<()> for (i, b) in BackendName::all().iter().enumerate() { println!(" {DIM}{}){RESET} {}", i + 1, b.as_str()); } + // Blank line so the list and the prompt don't crowd each other. + println!(); let prompt = format!(" {DIM}Select (1-{}):{RESET} ", BackendName::all().len()); let pick = plain_read_line(prompt).await?.trim().to_string(); + // Blank line between the typed selection and the response below. + println!(); pick.parse::() .ok() .and_then(|n| n.checked_sub(1)) @@ -633,8 +637,12 @@ pub(super) async fn cmd_model(args: &str, state: &mut AppState) -> Result<()> { if total > shown.len() { println!(" {DIM}…and {} more{RESET}", total - shown.len()); } + // Blank line so the list and the prompt don't crowd each other. + println!(); let prompt = format!(" {DIM}Select (1-{}):{RESET} ", shown.len()); let pick = plain_read_line(prompt).await?.trim().to_string(); + // Blank line between the typed selection and the response below. + println!(); if let Some(idx) = pick.parse::().ok().and_then(|n| n.checked_sub(1)) { if let Some(m) = shown.get(idx) { state.config.model_override = Some(m.clone());