From 3f7236f81735e338e2ed3ecbf0f1fed26392acba Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sun, 7 Jun 2026 17:01:27 +0300 Subject: [PATCH] fix: drop empty pty mouse writes refs #496 --- src/pane/terminal.rs | 32 +++++++++++++++++++++++++--- src/pty/actor/unix.rs | 49 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 9bfcf57f..5f44854d 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -923,7 +923,10 @@ impl GhosttyPaneTerminal { }; let mut encoder = ghostty_mouse_encoder_for_terminal(&core.terminal)?; let event = ghostty_mouse_event_from_button_kind(kind, column, row, modifiers)?; - encoder.encode(&event).ok() + encoder + .encode(&event) + .ok() + .filter(|bytes| !bytes.is_empty()) } pub fn encode_mouse_motion( @@ -941,7 +944,10 @@ impl GhosttyPaneTerminal { } let mut encoder = ghostty_mouse_encoder_for_terminal(&core.terminal)?; let event = ghostty_mouse_event_from_motion_kind(kind, column, row, modifiers)?; - encoder.encode(&event).ok() + encoder + .encode(&event) + .ok() + .filter(|bytes| !bytes.is_empty()) } pub fn encode_mouse_wheel( @@ -956,7 +962,10 @@ impl GhosttyPaneTerminal { }; let mut encoder = ghostty_mouse_encoder_for_terminal(&core.terminal)?; let event = ghostty_mouse_event_from_wheel_kind(kind, column, row, modifiers)?; - encoder.encode(&event).ok() + encoder + .encode(&event) + .ok() + .filter(|bytes| !bytes.is_empty()) } pub fn visible_text(&self) -> String { @@ -2410,6 +2419,23 @@ mod tests { assert_eq!(encoded.as_deref(), Some(&b"\x1b[<36;5;7M"[..])); } + #[test] + fn ghostty_mouse_drag_without_motion_reporting_is_not_forwarded() { + let (tx, _rx) = mpsc::channel(4); + let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + terminal.write(b"\x1b[?1000h\x1b[?1006h"); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + + let encoded = pane.encode_mouse_button( + crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left), + 4, + 6, + crossterm::event::KeyModifiers::empty(), + ); + + assert_eq!(encoded, None); + } + #[test] fn ghostty_mouse_moved_encoding_uses_any_motion_state() { let (tx, _rx) = mpsc::channel(4); diff --git a/src/pty/actor/unix.rs b/src/pty/actor/unix.rs index 70634475..e25d3dcb 100644 --- a/src/pty/actor/unix.rs +++ b/src/pty/actor/unix.rs @@ -408,6 +408,12 @@ struct PtyIoActorRunner { } impl PtyIoActorRunner { + fn enqueue_write(&mut self, bytes: Bytes) { + if !bytes.is_empty() { + self.pending_writes.push_back(bytes); + } + } + fn run(&mut self) { let mut should_exit = false; while !should_exit { @@ -515,7 +521,7 @@ impl PtyIoActorRunner { match command { PtyIoDataCommand::WriteUserInput(bytes) => { if self.state == ActorState::Running { - self.pending_writes.push_back(bytes); + self.enqueue_write(bytes); } } } @@ -592,7 +598,7 @@ impl PtyIoActorRunner { fn drain_pre_quiesce_commands(&mut self) { while let Ok(PtyIoDataCommand::WriteUserInput(bytes)) = self.data_rx.try_recv() { if self.state != ActorState::Released { - self.pending_writes.push_back(bytes); + self.enqueue_write(bytes); } } } @@ -639,7 +645,9 @@ impl PtyIoActorRunner { if self.state == ActorState::Released { return; } - self.pending_writes.extend(terminal_responses); + for bytes in terminal_responses { + self.enqueue_write(bytes); + } } fn flush_pending_writes_once(&mut self) { @@ -789,6 +797,41 @@ mod tests { (handle, peer, read_rx) } + fn actor_runner_for_unit_test() -> (PtyIoActorRunner, UnixStream) { + let (actor_socket, peer) = UnixStream::pair().expect("socket pair"); + actor_socket + .set_nonblocking(true) + .expect("actor socket nonblocking"); + let owned = unsafe { OwnedFd::from_raw_fd(actor_socket.into_raw_fd()) }; + let (_data_tx, data_rx) = mpsc::channel(ACTOR_COMMAND_BUFFER); + let (_control_tx, control_rx) = std_mpsc::channel(); + let wake_pipe = fd::create_wake_pipe().expect("wake pipe"); + let runner = PtyIoActorRunner { + pane_id: 1, + file: std::fs::File::from(owned), + data_rx, + control_rx, + state: ActorState::Running, + pending_writes: VecDeque::new(), + current_write_offset: 0, + wake_read_fd: wake_pipe.read_fd, + controls: Arc::new(Mutex::new(SharedPtyControls::default())), + on_read: Box::new(|_| PtyReadResult::empty()), + on_reader_exit: None, + poll_observer: None, + }; + (runner, peer) + } + + #[test] + fn actor_ignores_empty_user_input_write() { + let (mut runner, _peer) = actor_runner_for_unit_test(); + + assert!(!runner.handle_data_command(PtyIoDataCommand::WriteUserInput(Bytes::new()))); + + assert!(runner.pending_writes.is_empty()); + } + #[test] fn actor_writes_user_input_to_owned_fd() { let (handle, mut peer, _read_rx) = actor_with_socket_pair(false);