aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-28 04:50:28 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-28 04:50:28 +0300
commit67e48c0cc9aa1c18b743e3be54a69a4edab90680 (patch)
treecfec8bf5046334f3a2b2b55a662c8daa1d530293 /src
parentbf9cc201244946496b4d950046bf7edcc4892994 (diff)
fix: route terminal replies outside pane input
refs #306
Diffstat (limited to 'src')
-rw-r--r--src/pane.rs77
-rw-r--r--src/pane/terminal.rs139
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());
}