diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 50e6478c25..353eb10dd5 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -3304,7 +3304,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt } if crate::settings::detected_legacy_windows_console_host() { println!( - " {} legacy Windows console host → low_motion + fancy_animations=false + synchronized_output=off (auto)", + " {} legacy Windows console host → low_motion + fancy_animations=false + bracketed_paste=false + synchronized_output=off (auto)", "•".truecolor(sky_r, sky_g, sky_b) ); any_quirk = true; @@ -6026,8 +6026,8 @@ async fn run_interactive( let use_alt_screen = should_use_alt_screen(cli, config); let use_mouse_capture = should_use_mouse_capture(cli, config, use_alt_screen); let use_bracketed_paste = crate::settings::Settings::load() - .map(|s| s.bracketed_paste) - .unwrap_or(true); + .map(|s| s.effective_bracketed_paste()) + .unwrap_or_else(|_| !crate::settings::detected_legacy_windows_console_host()); // Auto-install bundled system skills (e.g. skill-creator) on first launch. // Errors are non-fatal: log a warning and continue. diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index 7c377f2e5b..f05c92ae53 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -1105,6 +1105,18 @@ impl Settings { pub fn synchronized_output_enabled(&self) -> bool { !self.synchronized_output.eq_ignore_ascii_case("off") } + + /// Runtime bracketed-paste mode after terminal-host quirks are applied. + /// + /// This deliberately does not mutate [`Settings::bracketed_paste`]: + /// `apply_env_overrides()` can run before saving settings, and a legacy + /// conhost runtime fallback must not permanently disable bracketed paste + /// when the same config is later used in Windows Terminal or another + /// modern terminal. + #[must_use] + pub fn effective_bracketed_paste(&self) -> bool { + self.bracketed_paste && !detected_legacy_windows_console_host() + } } fn resolve_settings_path_from_candidates( @@ -2222,6 +2234,14 @@ mod tests { settings.apply_env_overrides(); assert!(settings.low_motion); assert!(!settings.fancy_animations); + assert!( + settings.bracketed_paste, + "env-only conhost fallback must not persistently mutate bracketed_paste (#1102)" + ); + assert!( + !settings.effective_bracketed_paste(), + "legacy Windows console hosts do not support crossterm bracketed paste (#1102)" + ); assert_eq!(settings.synchronized_output, "off"); // SAFETY: cleanup under the guard. diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 7f78223d46..4901fce610 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -872,7 +872,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> { execute!(terminal.backend_mut(), DisableMouseCapture)?; } if use_bracketed_paste { - execute!(terminal.backend_mut(), DisableBracketedPaste)?; + disable_bracketed_paste_mode(terminal.backend_mut()); } terminal.show_cursor()?; drop(terminal); @@ -1061,7 +1061,7 @@ impl Drop for TerminalCleanupGuard { let _ = execute!(stdout, DisableMouseCapture); } if self.use_bracketed_paste { - let _ = execute!(stdout, DisableBracketedPaste); + disable_bracketed_paste_mode(&mut stdout); } let _ = execute!(stdout, crossterm::cursor::Show); } @@ -9766,7 +9766,7 @@ fn pause_terminal( execute!(terminal.backend_mut(), DisableMouseCapture)?; } if use_bracketed_paste { - execute!(terminal.backend_mut(), DisableBracketedPaste)?; + disable_bracketed_paste_mode(terminal.backend_mut()); } Ok(()) } @@ -9928,7 +9928,7 @@ pub fn emergency_restore_terminal() { pop_keyboard_enhancement_flags(&mut stdout); disable_alternate_scroll_mode(&mut stdout); let _ = execute!(stdout, DisableFocusChange); - let _ = execute!(stdout, DisableBracketedPaste); + disable_bracketed_paste_mode(&mut stdout); let _ = execute!(stdout, DisableMouseCapture); let _ = disable_raw_mode(); let _ = execute!(stdout, LeaveAlternateScreen); @@ -9991,14 +9991,30 @@ fn recover_terminal_modes( if use_mouse_capture && let Err(err) = execute!(writer, EnableMouseCapture) { tracing::debug!(?err, "EnableMouseCapture ignored"); } - if use_bracketed_paste && let Err(err) = execute!(writer, EnableBracketedPaste) { - tracing::debug!(?err, "EnableBracketedPaste ignored"); + if use_bracketed_paste { + try_enable_bracketed_paste_mode(writer); } if let Err(err) = execute!(writer, EnableFocusChange) { tracing::debug!(?err, "EnableFocusChange ignored"); } } +fn try_enable_bracketed_paste_mode(writer: &mut W) -> bool { + match execute!(writer, EnableBracketedPaste) { + Ok(()) => true, + Err(err) => { + tracing::debug!(?err, "EnableBracketedPaste ignored"); + false + } + } +} + +fn disable_bracketed_paste_mode(writer: &mut W) { + if let Err(err) = execute!(writer, DisableBracketedPaste) { + tracing::debug!(?err, "DisableBracketedPaste ignored"); + } +} + fn terminal_event_needs_viewport_recapture(evt: &Event) -> bool { matches!(evt, Event::FocusGained) } diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 44a5bef308..587965cd62 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -241,6 +241,30 @@ fn recover_terminal_modes_emits_expected_csi_sequences_with_gating() { ); } +#[cfg(not(windows))] +#[test] +fn bracketed_paste_mode_helpers_ignore_writer_errors() { + struct FailingWriter; + + impl std::io::Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> std::io::Result { + Err(std::io::Error::other("terminal mode unsupported")) + } + + fn flush(&mut self) -> std::io::Result<()> { + Err(std::io::Error::other("terminal mode unsupported")) + } + } + + let mut writer = FailingWriter; + + assert!( + !try_enable_bracketed_paste_mode(&mut writer), + "unsupported bracketed paste must be reported without bubbling an error" + ); + disable_bracketed_paste_mode(&mut writer); +} + #[cfg(windows)] #[test] fn recover_terminal_modes_runs_without_panic_on_windows() {