diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-28 04:50:28 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-28 04:50:28 +0300 |
| commit | 67e48c0cc9aa1c18b743e3be54a69a4edab90680 (patch) | |
| tree | cfec8bf5046334f3a2b2b55a662c8daa1d530293 /src | |
| parent | bf9cc201244946496b4d950046bf7edcc4892994 (diff) | |
fix: route terminal replies outside pane input
refs #306
Diffstat (limited to 'src')
| -rw-r--r-- | src/pane.rs | 77 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 139 |
2 files changed, 177 insertions, 39 deletions
diff --git a/src/pane.rs b/src/pane.rs index 11d6e03c..41bb439c 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -735,6 +735,21 @@ fn write_all_nonblocking( } #[cfg(unix)] +fn write_pty_bytes_locked( + writer: &mut std::fs::File, + fd: std::os::fd::RawFd, + bytes: &[u8], + io_stop: &AtomicBool, + pty_write_lock: &Mutex<()>, +) -> std::io::Result<()> { + let _guard = pty_write_lock + .lock() + .map_err(|_| std::io::Error::other("pty write lock poisoned"))?; + write_all_nonblocking(writer, fd, bytes, io_stop)?; + writer.flush() +} + +#[cfg(unix)] fn resize_pty_fd( fd: std::os::fd::RawFd, rows: u16, @@ -1082,11 +1097,13 @@ impl PaneRuntime { set_nonblocking(master_fd.as_raw_fd())?; let reader = file_from_duplicated_fd(master_fd.as_raw_fd())?; let writer = file_from_duplicated_fd(master_fd.as_raw_fd())?; + let terminal_response_writer = file_from_duplicated_fd(master_fd.as_raw_fd())?; let force_resize_fd = duplicate_cloexec_fd(master_fd.as_raw_fd())?; let resize_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(duplicate_cloexec_fd(master_fd.as_raw_fd())?) }; let io_stop = Arc::new(AtomicBool::new(false)); + let pty_write_lock = Arc::new(Mutex::new(())); let reader_paused = Arc::new(AtomicBool::new(true)); let reader_pause_ack = Arc::new(AtomicBool::new(false)); @@ -1121,6 +1138,8 @@ impl PaneRuntime { let mut reader = reader; let reader_fd = reader.as_raw_fd(); + let mut terminal_response_writer = terminal_response_writer; + let terminal_response_fd = terminal_response_writer.as_raw_fd(); let terminal = terminal.clone(); let response_writer = input_tx.clone(); let render_notify = render_notify.clone(); @@ -1128,6 +1147,7 @@ impl PaneRuntime { let child_pid = child_pid.clone(); let events = events.clone(); let io_stop = io_stop.clone(); + let pty_write_lock = pty_write_lock.clone(); let reader_paused = reader_paused.clone(); let reader_pause_ack = reader_pause_ack.clone(); let rt = tokio::runtime::Handle::current(); @@ -1166,6 +1186,18 @@ impl PaneRuntime { &buf[..n], &response_writer, ); + for response in result.terminal_responses { + if let Err(err) = write_pty_bytes_locked( + &mut terminal_response_writer, + terminal_response_fd, + &response, + &io_stop, + &pty_write_lock, + ) { + warn!(pane = pane_id.raw(), err = %err, "handoff terminal response write failed"); + break; + } + } if result.request_render && !render_dirty.swap(true, Ordering::AcqRel) { render_notify.notify_one(); } @@ -1198,21 +1230,23 @@ impl PaneRuntime { let mut writer = writer; let writer_fd = writer.as_raw_fd(); let io_stop = io_stop.clone(); + let pty_write_lock = pty_write_lock.clone(); tokio::task::spawn_blocking(move || { let rt = tokio::runtime::Handle::current(); while let Some(bytes) = rt.block_on(input_rx.recv()) { if io_stop.load(Ordering::Acquire) { break; } - if let Err(e) = write_all_nonblocking(&mut writer, writer_fd, &bytes, &io_stop) - { + if let Err(e) = write_pty_bytes_locked( + &mut writer, + writer_fd, + &bytes, + &io_stop, + &pty_write_lock, + ) { warn!(pane = pane_id.raw(), err = %e, "handoff pty write failed"); break; } - if let Err(e) = writer.flush() { - warn!(pane = pane_id.raw(), err = %e, "handoff pty flush failed"); - break; - } } debug!(pane = pane_id.raw(), "handoff writer task exiting"); }); @@ -1321,9 +1355,11 @@ impl PaneRuntime { set_nonblocking(master_fd)?; let reader = file_from_duplicated_fd(master_fd)?; let writer = file_from_duplicated_fd(master_fd)?; + let terminal_response_writer = file_from_duplicated_fd(master_fd)?; let force_resize_fd = duplicate_cloexec_fd(master_fd)?; let resize_fd = duplicate_cloexec_fd(master_fd)?; let io_stop = Arc::new(AtomicBool::new(false)); + let pty_write_lock = Arc::new(Mutex::new(())); let reader_paused = Arc::new(AtomicBool::new(false)); let reader_pause_ack = Arc::new(AtomicBool::new(false)); let (reader_stopped_tx, reader_stopped_rx) = std::sync::mpsc::channel(); @@ -1367,6 +1403,8 @@ impl PaneRuntime { let mut reader = reader; let reader_fd = reader.as_raw_fd(); + let mut terminal_response_writer = terminal_response_writer; + let terminal_response_fd = terminal_response_writer.as_raw_fd(); let terminal = terminal.clone(); let response_writer = input_tx.clone(); let render_notify = render_notify.clone(); @@ -1374,6 +1412,7 @@ impl PaneRuntime { let child_pid = child_pid.clone(); let events = events.clone(); let io_stop = io_stop.clone(); + let pty_write_lock = pty_write_lock.clone(); let reader_paused = reader_paused.clone(); let reader_pause_ack = reader_pause_ack.clone(); let rt = tokio::runtime::Handle::current(); @@ -1412,6 +1451,18 @@ impl PaneRuntime { &buf[..n], &response_writer, ); + for response in result.terminal_responses { + if let Err(err) = write_pty_bytes_locked( + &mut terminal_response_writer, + terminal_response_fd, + &response, + &io_stop, + &pty_write_lock, + ) { + warn!(pane = pane_id.raw(), err = %err, "terminal response write failed"); + break; + } + } if result.request_render && !render_dirty.swap(true, Ordering::AcqRel) { render_notify.notify_one(); } @@ -1720,21 +1771,23 @@ impl PaneRuntime { let mut writer = writer; let writer_fd = writer.as_raw_fd(); let io_stop = io_stop.clone(); + let pty_write_lock = pty_write_lock.clone(); tokio::task::spawn_blocking(move || { let rt = tokio::runtime::Handle::current(); while let Some(bytes) = rt.block_on(input_rx.recv()) { if io_stop.load(Ordering::Acquire) { break; } - if let Err(e) = write_all_nonblocking(&mut writer, writer_fd, &bytes, &io_stop) - { + if let Err(e) = write_pty_bytes_locked( + &mut writer, + writer_fd, + &bytes, + &io_stop, + &pty_write_lock, + ) { warn!(pane = pane_id.raw(), err = %e, "pty write failed"); break; } - if let Err(e) = writer.flush() { - warn!(pane = pane_id.raw(), err = %e, "pty flush failed"); - break; - } } debug!(pane = pane_id.raw(), "writer task exiting"); }); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index c248bded..cc316bf7 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use std::time::Duration; use bytes::Bytes; @@ -87,11 +87,13 @@ pub(crate) struct ProcessBytesResult { pub request_render: bool, pub render_delay: Option<Duration>, pub clipboard_writes: Vec<Vec<u8>>, + pub terminal_responses: Vec<Bytes>, } pub(crate) struct GhosttyPaneTerminal { pub core: Mutex<GhosttyPaneCore>, key_encoder: Mutex<crate::ghostty::KeyEncoder>, + pending_pty_responses: Arc<Mutex<Vec<Bytes>>>, } pub(crate) struct GhosttyPaneCore { @@ -273,11 +275,15 @@ impl PaneTerminal { impl GhosttyPaneTerminal { pub fn new( mut terminal: crate::ghostty::Terminal, - response_writer: mpsc::Sender<Bytes>, + _response_writer: mpsc::Sender<Bytes>, ) -> std::io::Result<Self> { + let pending_pty_responses = Arc::new(Mutex::new(Vec::new())); + let callback_responses = pending_pty_responses.clone(); terminal .set_write_pty_callback(move |bytes| { - let _ = response_writer.try_send(Bytes::copy_from_slice(bytes)); + if let Ok(mut responses) = callback_responses.lock() { + responses.push(Bytes::copy_from_slice(bytes)); + } }) .map_err(|e| std::io::Error::other(e.to_string()))?; @@ -308,6 +314,7 @@ impl GhosttyPaneTerminal { osc52_forwarder: Osc52Forwarder::default(), }), key_encoder: Mutex::new(key_encoder), + pending_pty_responses, }) } @@ -362,7 +369,7 @@ impl GhosttyPaneTerminal { pane_id: PaneId, shell_pid: u32, bytes: &[u8], - response_writer: &mpsc::Sender<Bytes>, + _response_writer: &mpsc::Sender<Bytes>, ) -> ProcessBytesResult { let Ok(mut core) = self.core.lock() else { error!(pane = pane_id.raw(), "ghostty core lock poisoned in reader"); @@ -370,6 +377,7 @@ impl GhosttyPaneTerminal { request_render: false, render_delay: None, clipboard_writes: Vec::new(), + terminal_responses: Vec::new(), }; }; @@ -412,10 +420,12 @@ impl GhosttyPaneTerminal { } core.kitty_keyboard.observe(filtered_bytes.as_ref()); - core.terminal.write(filtered_bytes.as_ref()); + let mut terminal_responses = Vec::new(); core.default_color_event_tracker .observe(filtered_bytes.as_ref()); - respond_to_default_color_events(&mut core, response_writer); + respond_to_default_color_events(&mut core, &mut terminal_responses); + core.terminal.write(filtered_bytes.as_ref()); + terminal_responses.extend(self.drain_pending_pty_responses()); let has_kitty_graphics_sequence = crate::kitty_graphics::is_enabled() && contains_kitty_graphics_sequence(filtered_bytes.as_ref()); @@ -434,9 +444,17 @@ impl GhosttyPaneTerminal { render_delay: (!synchronized_output && has_kitty_graphics_sequence) .then_some(KITTY_GRAPHICS_REDRAW_SETTLE), clipboard_writes, + terminal_responses, } } + fn drain_pending_pty_responses(&self) -> Vec<Bytes> { + self.pending_pty_responses + .lock() + .map(|mut responses| std::mem::take(&mut *responses)) + .unwrap_or_default() + } + pub fn seed_history_ansi(&self, ansi: &str) { if ansi.is_empty() { return; @@ -1278,13 +1296,13 @@ fn ghostty_cell_style( fn respond_to_default_color_events( core: &mut GhosttyPaneCore, - response_writer: &mpsc::Sender<Bytes>, + terminal_responses: &mut Vec<Bytes>, ) { for event in core.default_color_event_tracker.drain_pending() { match event { DefaultColorEvent::Query(query) => { if let Some(response) = default_color_query_response(query, core) { - let _ = response_writer.try_send(response); + terminal_responses.push(response); } } DefaultColorEvent::Set(query) => mark_child_default_color_changed(core, query, true), @@ -2260,7 +2278,71 @@ mod tests { } #[test] - fn process_pty_bytes_responds_to_default_color_queries() { + fn process_pty_bytes_returns_libghostty_query_responses_without_queuing_input() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b[6n", &tx); + + assert_eq!(result.terminal_responses.len(), 1); + assert!(String::from_utf8_lossy(&result.terminal_responses[0]).contains('R')); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_orders_default_color_reply_before_following_device_attribute_reply() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.apply_host_terminal_theme(crate::terminal_theme::TerminalTheme { + foreground: None, + background: Some(crate::terminal_theme::RgbColor { + r: 0x00, + g: 0x2b, + b: 0x36, + }), + }); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07\x1b[c", &tx); + + assert_eq!(result.terminal_responses.len(), 2); + assert_eq!( + result.terminal_responses[0], + Bytes::from_static(b"\x1b]11;rgb:0000/2b2b/3636\x1b\\") + ); + assert!(String::from_utf8_lossy(&result.terminal_responses[1]).contains('c')); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_returns_default_color_query_responses_without_queuing_input() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.apply_host_terminal_theme(crate::terminal_theme::TerminalTheme { + foreground: None, + background: Some(crate::terminal_theme::RgbColor { + r: 0x00, + g: 0x2b, + b: 0x36, + }), + }); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07", &tx); + + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]11;rgb:0000/2b2b/3636\x1b\\")] + ); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_returns_default_color_query_responses_in_order() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); @@ -2278,21 +2360,20 @@ mod tests { }), }); - pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x07\x1b]11;?\x07", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x07\x1b]11;?\x07", &tx); assert_eq!( - rx.try_recv().unwrap().as_ref(), - b"\x1b]10;rgb:6565/7b7b/8383\x1b\\" - ); - assert_eq!( - rx.try_recv().unwrap().as_ref(), - b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\" + result.terminal_responses, + vec![ + Bytes::from_static(b"\x1b]10;rgb:6565/7b7b/8383\x1b\\"), + Bytes::from_static(b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\"), + ] ); assert!(rx.try_recv().is_err()); } #[test] - fn process_pty_bytes_responds_to_split_default_color_query() { + fn process_pty_bytes_returns_split_default_color_query_response() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); @@ -2306,21 +2387,23 @@ mod tests { }), }); - pane.process_pty_bytes(pane_id, 0, b"\x1b]11", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11", &tx); + assert!(result.terminal_responses.is_empty()); assert!(rx.try_recv().is_err()); - pane.process_pty_bytes(pane_id, 0, b";?\x1b", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b";?\x1b", &tx); + assert!(result.terminal_responses.is_empty()); assert!(rx.try_recv().is_err()); - pane.process_pty_bytes(pane_id, 0, b"\\", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\\", &tx); assert_eq!( - rx.try_recv().unwrap().as_ref(), - b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\" + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\")] ); assert!(rx.try_recv().is_err()); } #[test] - fn process_pty_bytes_stops_replying_after_child_default_color_set() { + fn process_pty_bytes_tracks_default_color_set_and_reset_before_replying() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); @@ -2334,13 +2417,15 @@ mod tests { }), }); - pane.process_pty_bytes(pane_id, 0, b"\x1b]11;rgb:11/22/33\x07\x1b]11;?\x07", &tx); + let result = + pane.process_pty_bytes(pane_id, 0, b"\x1b]11;rgb:11/22/33\x07\x1b]11;?\x07", &tx); + assert!(result.terminal_responses.is_empty()); assert!(rx.try_recv().is_err()); - pane.process_pty_bytes(pane_id, 0, b"\x1b]111\x07\x1b]11;?\x07", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]111\x07\x1b]11;?\x07", &tx); assert_eq!( - rx.try_recv().unwrap().as_ref(), - b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\" + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\")] ); assert!(rx.try_recv().is_err()); } |
