aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-04 22:43:06 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-04 22:43:06 +0300
commit670656b26f1aeadc3f18e6692a9fdf1b02da799c (patch)
tree7401a16884f8c4088bff920435dce1e0f7b401de
parent648afa1fd5b4c2eb74b4292b4eceeeb6d93016c1 (diff)
fix: forward resize terminal responses
refs #471
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/pane.rs28
-rw-r--r--src/pane/terminal.rs37
-rw-r--r--src/pty/actor.rs79
4 files changed, 117 insertions, 28 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index c1ef0939..824e52c5 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -4,6 +4,7 @@
### Fixed
- Resizing restored panes no longer aborts the server when libghostty-vt reflows a terminal whose pre-resize cursor row is past the new height. (#465)
+- Full-screen TUIs such as Neovim now receive resize-generated terminal responses after Herdr internal pane resizes, so grown panes redraw without waiting for extra input. (#471)
## [0.6.8] - 2026-06-04
diff --git a/src/pane.rs b/src/pane.rs
index 248f5bb1..19556ffe 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -722,11 +722,24 @@ impl PaneRuntimeIo {
}
}
- fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) {
+ fn resize(
+ &self,
+ rows: u16,
+ cols: u16,
+ cell_width_px: u32,
+ cell_height_px: u32,
+ terminal_responses: Vec<Bytes>,
+ ) {
match self {
#[cfg(unix)]
PaneRuntimeIo::Actor(actor) => {
- actor.resize(rows, cols, cell_width_px, cell_height_px);
+ actor.resize(
+ rows,
+ cols,
+ cell_width_px,
+ cell_height_px,
+ terminal_responses,
+ );
}
#[cfg(test)]
PaneRuntimeIo::TestChannel { resize_tx, .. } => {
@@ -1814,9 +1827,16 @@ impl PaneRuntime {
return;
}
self.current_size.set(size);
- self.terminal
+ let terminal_responses = self
+ .terminal
.resize(rows, cols, cell_width_px, cell_height_px);
- self.io.resize(rows, cols, cell_width_px, cell_height_px);
+ self.io.resize(
+ rows,
+ cols,
+ cell_width_px,
+ cell_height_px,
+ terminal_responses,
+ );
}
pub fn nudge_child_redraw_after_handoff(&self) {
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 88924d36..f2fceaa8 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -146,9 +146,15 @@ impl PaneTerminal {
.process_pty_bytes(pane_id, shell_pid, bytes, response_writer)
}
- pub fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) {
+ pub fn resize(
+ &self,
+ rows: u16,
+ cols: u16,
+ cell_width_px: u32,
+ cell_height_px: u32,
+ ) -> Vec<Bytes> {
self.ghostty
- .resize(rows, cols, cell_width_px, cell_height_px);
+ .resize(rows, cols, cell_width_px, cell_height_px)
}
pub fn scroll_up(&self, lines: usize) {
@@ -662,7 +668,13 @@ impl GhosttyPaneTerminal {
}
}
- pub fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) {
+ pub fn resize(
+ &self,
+ rows: u16,
+ cols: u16,
+ cell_width_px: u32,
+ cell_height_px: u32,
+ ) -> Vec<Bytes> {
if let Ok(mut core) = self.core.lock() {
let offset_from_bottom = core
.terminal
@@ -694,6 +706,7 @@ impl GhosttyPaneTerminal {
let _ = core
.terminal
.resize(cols, rows, cell_width_px, cell_height_px);
+ let terminal_responses = self.drain_pending_pty_responses();
let bottom_is_blank = ghostty_detection_text(&core)
.map(|text| text.trim().is_empty())
@@ -716,6 +729,9 @@ impl GhosttyPaneTerminal {
remaining -= 1;
}
}
+ terminal_responses
+ } else {
+ Vec::new()
}
}
@@ -2622,6 +2638,21 @@ mod tests {
}
#[test]
+ fn resize_returns_in_band_size_report_response() {
+ let (tx, _rx) = mpsc::channel(4);
+ let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ terminal.mode_set(2048, true).unwrap();
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+
+ let responses = pane.resize(40, 100, 9, 18);
+
+ assert_eq!(
+ responses,
+ vec![Bytes::from_static(b"\x1B[48;40;100;720;900t")]
+ );
+ }
+
+ #[test]
fn synchronized_output_suppresses_intermediate_render_requests_until_batch_ends() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
diff --git a/src/pty/actor.rs b/src/pty/actor.rs
index cce58a75..70634475 100644
--- a/src/pty/actor.rs
+++ b/src/pty/actor.rs
@@ -50,9 +50,15 @@ struct PtyResize {
cell_height_px: u32,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct PtyResizeRequest {
+ resize: PtyResize,
+ terminal_responses: Vec<Bytes>,
+}
+
#[derive(Default)]
struct SharedPtyControls {
- resize: Option<PtyResize>,
+ resize: Option<PtyResizeRequest>,
nudge: Option<PtyResize>,
}
@@ -151,17 +157,27 @@ impl PtyIoActorHandle {
}
}
- pub(crate) fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) {
+ pub(crate) fn resize(
+ &self,
+ rows: u16,
+ cols: u16,
+ cell_width_px: u32,
+ cell_height_px: u32,
+ terminal_responses: Vec<Bytes>,
+ ) {
{
let mut controls = self
.controls
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
- controls.resize = Some(PtyResize {
- rows,
- cols,
- cell_width_px,
- cell_height_px,
+ controls.resize = Some(PtyResizeRequest {
+ resize: PtyResize {
+ rows,
+ cols,
+ cell_width_px,
+ cell_height_px,
+ },
+ terminal_responses,
});
}
self.wake_actor();
@@ -592,8 +608,9 @@ impl PtyIoActorRunner {
if self.state == ActorState::Released {
return;
}
- if let Some(resize) = resize {
- self.resize(resize);
+ if let Some(request) = resize {
+ self.resize(request.resize);
+ self.enqueue_terminal_responses(request.terminal_responses);
}
if let Some(nudge) = nudge {
self.nudge(nudge);
@@ -612,16 +629,19 @@ impl PtyIoActorRunner {
}
Ok(n) => {
let result = (self.on_read)(&buf[..n]);
- for response in result.terminal_responses {
- if self.state != ActorState::Released {
- self.pending_writes.push_back(response);
- }
- }
+ self.enqueue_terminal_responses(result.terminal_responses);
true
}
}
}
+ fn enqueue_terminal_responses(&mut self, terminal_responses: Vec<Bytes>) {
+ if self.state == ActorState::Released {
+ return;
+ }
+ self.pending_writes.extend(terminal_responses);
+ }
+
fn flush_pending_writes_once(&mut self) {
while let Some(bytes) = self.pending_writes.front() {
let chunk = &bytes[self.current_write_offset..];
@@ -922,18 +942,21 @@ mod tests {
controls: Arc::clone(&controls),
};
- handle.resize(20, 80, 8, 16);
- handle.resize(40, 120, 9, 18);
+ handle.resize(20, 80, 8, 16, vec![Bytes::from_static(b"old")]);
+ handle.resize(40, 120, 9, 18, vec![Bytes::from_static(b"new")]);
handle.nudge_child_redraw_after_handoff(41, 121, 10, 20);
let controls = controls.lock().expect("controls lock");
assert_eq!(
controls.resize,
- Some(PtyResize {
- rows: 40,
- cols: 120,
- cell_width_px: 9,
- cell_height_px: 18,
+ Some(PtyResizeRequest {
+ resize: PtyResize {
+ rows: 40,
+ cols: 120,
+ cell_width_px: 9,
+ cell_height_px: 18,
+ },
+ terminal_responses: vec![Bytes::from_static(b"new")],
})
);
assert_eq!(
@@ -947,6 +970,20 @@ mod tests {
);
}
+ #[test]
+ fn resize_writes_terminal_responses_after_applying_resize() {
+ let (handle, mut peer, _read_rx) = actor_with_socket_pair(false);
+ let response = Bytes::from_static(b"\x1B[48;40;100;720;900t");
+
+ handle.resize(40, 100, 9, 18, vec![response.clone()]);
+
+ let mut buf = vec![0; response.len()];
+ peer.read_exact(&mut buf)
+ .expect("peer receives resize response");
+ assert_eq!(Bytes::from(buf), response);
+ handle.shutdown();
+ }
+
#[tokio::test]
async fn async_user_input_waits_for_queue_capacity() {
let (data_tx, mut data_rx) = mpsc::channel(1);