aboutsummaryrefslogtreecommitdiff
path: root/src/pane
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-30 00:19:36 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-30 00:19:36 +0300
commit8b1d77acf6059e6688fb6a875c3176c95602b6c4 (patch)
treecb9bf5e138a4cfe098726cdaf3b06e0ef458288f /src/pane
parent1db9695eecece3ddd2bcf43ac33c961daffd6004 (diff)
fix: keep powershell panes alive after agent ctrl-c
refs #860
Diffstat (limited to 'src/pane')
-rw-r--r--src/pane/terminal.rs174
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 {