fix: forward resize terminal responses

refs #471
This commit is contained in:
Ogulcan Celik 2026-06-04 22:43:06 +03:00
parent 648afa1fd5
commit 670656b26f
4 changed files with 117 additions and 28 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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()
}
}
@ -2621,6 +2637,21 @@ mod tests {
assert!(pane.recent_text(3).trim().is_empty());
}
#[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);

View File

@ -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);