diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 00:19:36 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 00:19:36 +0300 |
| commit | 8b1d77acf6059e6688fb6a875c3176c95602b6c4 (patch) | |
| tree | cb9bf5e138a4cfe098726cdaf3b06e0ef458288f /src/pane | |
| parent | 1db9695eecece3ddd2bcf43ac33c961daffd6004 (diff) | |
fix: keep powershell panes alive after agent ctrl-c
refs #860
Diffstat (limited to 'src/pane')
| -rw-r--r-- | src/pane/terminal.rs | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index a479e5ad..b36ae0ee 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -133,6 +133,7 @@ pub(crate) struct GhosttyPaneCore { pub xtgettcap_query_tracker: XtgettcapQueryTracker, decscusr_tracker: DecscusrTracker, cursor_settle_state: CursorPositionSettleState, + windows_powershell_prompt_cwd_reporting: bool, } pub(crate) struct PaneTerminal { @@ -391,12 +392,19 @@ impl GhosttyPaneTerminal { xtgettcap_query_tracker: XtgettcapQueryTracker::default(), decscusr_tracker: DecscusrTracker::default(), cursor_settle_state: CursorPositionSettleState::default(), + windows_powershell_prompt_cwd_reporting: false, }), key_encoder: Mutex::new(key_encoder), pending_pty_responses, }) } + pub(super) fn set_windows_powershell_prompt_cwd_reporting(&self, enabled: bool) { + if let Ok(mut core) = self.core.lock() { + core.windows_powershell_prompt_cwd_reporting = enabled; + } + } + pub fn apply_host_terminal_theme(&self, theme: crate::terminal_theme::TerminalTheme) { if let Ok(mut core) = self.core.lock() { let foreground_unowned = !core.child_default_foreground_changed; @@ -585,6 +593,13 @@ impl GhosttyPaneTerminal { core.cursor_settle_state .observe(cursor_after_write, Instant::now()); } + #[cfg(windows)] + let reported_cwd = if core.windows_powershell_prompt_cwd_reporting { + reported_cwd.or_else(|| windows_powershell_current_prompt_cwd(&mut core)) + } else { + reported_cwd + }; + let request_render = !synchronized_output && !has_kitty_graphics_sequence; let render_delay = render_delay_after_pty_write( synchronized_output, @@ -1624,6 +1639,69 @@ fn ghostty_detection_text(core: &GhosttyPaneCore) -> Result<String, crate::ghost ghostty_recent_text(core, lines) } +#[cfg(windows)] +fn windows_powershell_current_prompt_cwd(core: &mut GhosttyPaneCore) -> Option<std::path::PathBuf> { + if core.terminal.active_screen().ok()? != crate::ghostty::ActiveScreen::Primary { + return None; + } + let cursor = current_cursor_state(core)?; + let rows = core.terminal.rows().ok()?; + let cols = core.terminal.cols().ok()?; + if rows == 0 || cols == 0 || cursor.y >= rows { + return None; + } + let total_rows = core.terminal.total_rows().ok()?; + let viewport_start = total_rows.saturating_sub(usize::from(rows)); + let cursor_row = viewport_start + usize::from(cursor.y); + if core + .terminal + .read_text_screen( + (0, cursor_row as u32), + (cols.saturating_sub(1), cursor_row as u32), + false, + ) + .ok()? + .trim() + .is_empty() + { + return None; + } + let text = core + .terminal + .read_text_screen( + (0, viewport_start as u32), + (cols.saturating_sub(1), cursor_row as u32), + false, + ) + .ok()?; + windows_powershell_prompt_cwd(&text) +} + +#[cfg(windows)] +fn windows_powershell_prompt_cwd(text: &str) -> Option<std::path::PathBuf> { + text.lines() + .rev() + .find(|line| !line.trim().is_empty()) + .and_then(windows_powershell_prompt_line_cwd) +} + +#[cfg(windows)] +fn windows_powershell_prompt_line_cwd(line: &str) -> Option<std::path::PathBuf> { + let line = line.trim_end(); + let rest = line.strip_prefix("PS ")?; + let marker = rest.find('>')?; + let mut raw_cwd = rest[..marker].trim_end(); + let suffix = rest[marker..].trim_end(); + if !suffix.chars().all(|ch| ch == '>') { + return None; + } + if let Some((_, filesystem_path)) = raw_cwd.rsplit_once("::") { + raw_cwd = filesystem_path; + } + let cwd = std::path::PathBuf::from(raw_cwd); + (cwd.is_absolute() && cwd.is_dir()).then_some(cwd) +} + fn ghostty_recent_text( core: &GhosttyPaneCore, lines: usize, @@ -2202,6 +2280,102 @@ mod tests { Bytes::from(format!("\x1b]{command};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\")) } + #[cfg(windows)] + #[test] + fn windows_powershell_prompt_cwd_uses_latest_default_prompt_path() { + let cwd = std::env::current_dir().unwrap(); + let text = format!("PS C:\\old> cd {}\nPS {}>", cwd.display(), cwd.display()); + + assert_eq!(windows_powershell_prompt_cwd(&text).as_ref(), Some(&cwd)); + } + + #[cfg(windows)] + #[test] + fn windows_powershell_prompt_cwd_requires_current_prompt_line() { + let cwd = std::env::current_dir().unwrap(); + let text = format!("PS {}>\ncommand output", cwd.display()); + + assert_eq!(windows_powershell_prompt_cwd(&text), None); + } + + #[cfg(windows)] + #[test] + fn windows_powershell_prompt_cwd_ignores_command_echo() { + let cwd = std::env::current_dir().unwrap(); + let text = format!("PS {}> echo hi", cwd.display()); + + assert_eq!(windows_powershell_prompt_cwd(&text), None); + } + + #[cfg(windows)] + fn process_windows_powershell_prompt_bytes( + bytes: &[u8], + cols: u16, + rows: u16, + enabled: bool, + ) -> ProcessBytesResult { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(cols, rows, 100).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + pane.set_windows_powershell_prompt_cwd_reporting(enabled); + pane.process_pty_bytes(PaneId::from_raw(1), 0, bytes, &tx) + } + + #[cfg(windows)] + #[test] + fn process_pty_bytes_reports_windows_powershell_prompt_cwd() { + let cwd = std::env::current_dir().unwrap(); + let bytes = format!("PS C:\\old> cd {}\r\nPS {}>", cwd.display(), cwd.display()); + + let result = process_windows_powershell_prompt_bytes(bytes.as_bytes(), 80, 24, true); + + assert_eq!(result.reported_cwd.as_ref(), Some(&cwd)); + } + + #[cfg(windows)] + #[test] + fn process_pty_bytes_reports_wrapped_windows_powershell_prompt_cwd() { + let cwd = std::env::current_dir().unwrap(); + let bytes = format!("PS {}>", cwd.display()); + + let result = process_windows_powershell_prompt_bytes(bytes.as_bytes(), 12, 8, true); + + assert_eq!(result.reported_cwd.as_ref(), Some(&cwd)); + } + + #[cfg(windows)] + #[test] + fn process_pty_bytes_ignores_prompt_like_output_on_previous_line() { + let cwd = std::env::current_dir().unwrap(); + let bytes = format!("PS {}>\r\n", cwd.display()); + + let result = process_windows_powershell_prompt_bytes(bytes.as_bytes(), 80, 24, true); + + assert_eq!(result.reported_cwd, None); + } + + #[cfg(windows)] + #[test] + fn process_pty_bytes_ignores_windows_powershell_prompt_cwd_on_alternate_screen() { + let cwd = std::env::current_dir().unwrap(); + let bytes = format!("\x1b[?1049hPS {}>", cwd.display()); + + let result = process_windows_powershell_prompt_bytes(bytes.as_bytes(), 80, 24, true); + + assert_eq!(result.reported_cwd, None); + } + + #[cfg(windows)] + #[test] + fn process_pty_bytes_skips_windows_powershell_prompt_cwd_when_disabled() { + let cwd = std::env::current_dir().unwrap(); + let bytes = format!("PS {}>", cwd.display()); + + let result = process_windows_powershell_prompt_bytes(bytes.as_bytes(), 80, 24, false); + + assert_eq!(result.reported_cwd, None); + } + fn expected_xtgettcap_response(cap_hex: &str, value: Option<&[u8]>) -> Bytes { let mut response = format!("\x1bP1+r{cap_hex}").into_bytes(); if let Some(value) = value { |
