aboutsummaryrefslogtreecommitdiff
path: root/src/pane
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-09 02:04:39 +0300
committerGitHub <noreply@github.com>2026-08-09 02:04:39 +0300
commit1777e9bba32b953ed1ad203b4a16d01105539000 (patch)
tree22096973812c9baba822453d8470bdcfe64fc2a6 /src/pane
parent36a8d36a7258e08b40c01bf521b1f4006525f143 (diff)
feat(graphics): add direct pane frame streaming (#2523)
* feat(graphics): add direct pane frame streaming * feat(graphics): expose pane visibility * test(graphics): preserve legacy terminal image output * fix(graphics): harden streaming lifecycle * fix(input): preserve mouse releases outside panes
Diffstat (limited to 'src/pane')
-rw-r--r--src/pane/input.rs38
-rw-r--r--src/pane/terminal.rs140
2 files changed, 105 insertions, 73 deletions
diff --git a/src/pane/input.rs b/src/pane/input.rs
index 2e60bfc8..2ff299c9 100644
--- a/src/pane/input.rs
+++ b/src/pane/input.rs
@@ -60,23 +60,43 @@ pub(super) fn ghostty_mods_from_key_modifiers(modifiers: crossterm::event::KeyMo
pub(super) fn ghostty_mouse_encoder_for_terminal(
terminal: &crate::ghostty::Terminal,
+ position: crate::input::mouse::Position,
) -> Option<crate::ghostty::MouseEncoder> {
let mut encoder = crate::ghostty::MouseEncoder::new().ok()?;
encoder.set_from_terminal(terminal);
- if terminal
- .mode_get(crate::ghostty::MODE_MOUSE_SGR_PIXELS)
- .ok()?
- {
- // Herdr receives host mouse positions in terminal cells. Downgrade
- // SGR-pixels to normal SGR so forwarded coordinates stay cell-local.
- encoder.set_format(crate::ghostty::MOUSE_FORMAT_SGR);
- }
let cols = terminal.cols().ok()? as u32;
let rows = terminal.rows().ok()? as u32;
- encoder.set_size(cols, rows, 1, 1);
+ match position {
+ crate::input::mouse::Position::Cell { .. } => {
+ if terminal
+ .mode_get(crate::ghostty::MODE_MOUSE_SGR_PIXELS)
+ .ok()?
+ {
+ encoder.set_format(crate::ghostty::MOUSE_FORMAT_SGR);
+ }
+ encoder.set_size(cols, rows, 1, 1);
+ }
+ crate::input::mouse::Position::Pixels { .. } => {
+ let width_px = terminal.width_px().ok()?;
+ let height_px = terminal.height_px().ok()?;
+ if width_px == 0 || height_px == 0 || cols == 0 || rows == 0 {
+ return None;
+ }
+ encoder.set_size(width_px, height_px, width_px / cols, height_px / rows);
+ }
+ }
Some(encoder)
}
+pub(super) fn ghostty_mouse_position_for_terminal(
+ position: crate::input::mouse::Position,
+) -> Option<(f32, f32)> {
+ match position {
+ crate::input::mouse::Position::Pixels { x, y } => Some((x as f32, y as f32)),
+ crate::input::mouse::Position::Cell { column, row } => Some((column as f32, row as f32)),
+ }
+}
+
pub(super) fn ghostty_mouse_event_from_button_kind(
kind: crossterm::event::MouseEventKind,
column: u16,
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 8de84af3..91fcb4fe 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -23,7 +23,8 @@ use super::{
input::{
ghostty_key_event_from_terminal_key, ghostty_mouse_encoder_for_terminal,
ghostty_mouse_event_from_button_kind, ghostty_mouse_event_from_motion_kind,
- ghostty_mouse_event_from_wheel_kind, ghostty_prefers_herdr_text_encoding,
+ ghostty_mouse_event_from_wheel_kind, ghostty_mouse_position_for_terminal,
+ ghostty_prefers_herdr_text_encoding,
},
kitty_keyboard::KittyKeyboardTracker,
osc::{
@@ -537,37 +538,31 @@ impl PaneTerminal {
self.ghostty.encode_terminal_key(key, protocol)
}
- pub fn encode_mouse_button(
+ pub(crate) fn encode_mouse_button(
&self,
kind: crossterm::event::MouseEventKind,
- column: u16,
- row: u16,
+ position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
- self.ghostty
- .encode_mouse_button(kind, column, row, modifiers)
+ self.ghostty.encode_mouse_button(kind, position, modifiers)
}
- pub fn encode_mouse_motion(
+ pub(crate) fn encode_mouse_motion(
&self,
kind: crossterm::event::MouseEventKind,
- column: u16,
- row: u16,
+ position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
- self.ghostty
- .encode_mouse_motion(kind, column, row, modifiers)
+ self.ghostty.encode_mouse_motion(kind, position, modifiers)
}
- pub fn encode_mouse_wheel(
+ pub(crate) fn encode_mouse_wheel(
&self,
kind: crossterm::event::MouseEventKind,
- column: u16,
- row: u16,
+ position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
- self.ghostty
- .encode_mouse_wheel(kind, column, row, modifiers)
+ self.ghostty.encode_mouse_wheel(kind, position, modifiers)
}
}
@@ -1488,6 +1483,9 @@ impl GhosttyPaneTerminal {
let _ = core
.terminal
.mode_set(crate::ghostty::MODE_MOUSE_SGR, false);
+ let _ = core
+ .terminal
+ .mode_set(crate::ghostty::MODE_MOUSE_SGR_PIXELS, false);
match input_state.mouse_protocol_encoding {
crate::input::MouseProtocolEncoding::Default => {}
crate::input::MouseProtocolEncoding::Utf8 => {
@@ -1498,6 +1496,11 @@ impl GhosttyPaneTerminal {
crate::input::MouseProtocolEncoding::Sgr => {
let _ = core.terminal.mode_set(crate::ghostty::MODE_MOUSE_SGR, true);
}
+ crate::input::MouseProtocolEncoding::SgrPixels => {
+ let _ = core
+ .terminal
+ .mode_set(crate::ghostty::MODE_MOUSE_SGR_PIXELS, true);
+ }
}
if input_state.modify_other_keys {
@@ -1688,6 +1691,10 @@ impl GhosttyPaneTerminal {
.terminal
.mode_get(crate::ghostty::MODE_MOUSE_UTF8)
.ok()?;
+ let mouse_sgr_pixels = core
+ .terminal
+ .mode_get(crate::ghostty::MODE_MOUSE_SGR_PIXELS)
+ .ok()?;
let mouse_alternate_scroll = core
.terminal
.mode_get(crate::ghostty::MODE_MOUSE_ALTERNATE_SCROLL)
@@ -1703,7 +1710,9 @@ impl GhosttyPaneTerminal {
} else {
crate::input::MouseProtocolMode::None
};
- let mouse_protocol_encoding = if mouse_sgr {
+ let mouse_protocol_encoding = if mouse_sgr_pixels {
+ crate::input::MouseProtocolEncoding::SgrPixels
+ } else if mouse_sgr {
crate::input::MouseProtocolEncoding::Sgr
} else if mouse_utf8 {
crate::input::MouseProtocolEncoding::Utf8
@@ -1838,57 +1847,58 @@ impl GhosttyPaneTerminal {
}
}
- pub fn encode_mouse_button(
+ pub(crate) fn encode_mouse_button(
&self,
kind: crossterm::event::MouseEventKind,
- column: u16,
- row: u16,
+ position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
- let Ok(core) = self.core.lock() else {
- return None;
- };
- 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()
- .filter(|bytes| !bytes.is_empty())
+ self.encode_mouse_event(
+ ghostty_mouse_event_from_button_kind(kind, 0, 0, modifiers)?,
+ position,
+ false,
+ )
}
- pub fn encode_mouse_motion(
+ pub(crate) fn encode_mouse_motion(
&self,
kind: crossterm::event::MouseEventKind,
- column: u16,
- row: u16,
+ position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
- let Ok(core) = self.core.lock() else {
- return None;
- };
- if !core.terminal.mode_get(MODE_MOUSE_ANY_MOTION).ok()? {
- return None;
- }
- 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()
- .filter(|bytes| !bytes.is_empty())
+ self.encode_mouse_event(
+ ghostty_mouse_event_from_motion_kind(kind, 0, 0, modifiers)?,
+ position,
+ true,
+ )
}
- pub fn encode_mouse_wheel(
+ pub(crate) fn encode_mouse_wheel(
&self,
kind: crossterm::event::MouseEventKind,
- column: u16,
- row: u16,
+ position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
- let Ok(core) = self.core.lock() else {
+ self.encode_mouse_event(
+ ghostty_mouse_event_from_wheel_kind(kind, 0, 0, modifiers)?,
+ position,
+ false,
+ )
+ }
+
+ fn encode_mouse_event(
+ &self,
+ mut event: crate::ghostty::MouseEvent,
+ position: crate::input::mouse::Position,
+ require_any_motion: bool,
+ ) -> Option<Vec<u8>> {
+ let core = self.core.lock().ok()?;
+ if require_any_motion && !core.terminal.mode_get(MODE_MOUSE_ANY_MOTION).ok()? {
return None;
- };
- let mut encoder = ghostty_mouse_encoder_for_terminal(&core.terminal)?;
- let event = ghostty_mouse_event_from_wheel_kind(kind, column, row, modifiers)?;
+ }
+ let mut encoder = ghostty_mouse_encoder_for_terminal(&core.terminal, position)?;
+ let (x, y) = ghostty_mouse_position_for_terminal(position)?;
+ event.set_position(x, y);
encoder
.encode(&event)
.ok()
@@ -4670,8 +4680,7 @@ mod tests {
let encoded = pane.encode_mouse_button(
crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Left),
- 11,
- 9,
+ crate::input::mouse::Position::Cell { column: 11, row: 9 },
crossterm::event::KeyModifiers::empty(),
);
@@ -4687,8 +4696,7 @@ mod tests {
let encoded = pane.encode_mouse_button(
crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left),
- 4,
- 6,
+ crate::input::mouse::Position::Cell { column: 4, row: 6 },
crossterm::event::KeyModifiers::SHIFT,
);
@@ -4704,8 +4712,7 @@ mod tests {
let encoded = pane.encode_mouse_button(
crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left),
- 4,
- 6,
+ crate::input::mouse::Position::Cell { column: 4, row: 6 },
crossterm::event::KeyModifiers::empty(),
);
@@ -4721,8 +4728,7 @@ mod tests {
let encoded = pane.encode_mouse_motion(
crossterm::event::MouseEventKind::Moved,
- 4,
- 6,
+ crate::input::mouse::Position::Cell { column: 4, row: 6 },
crossterm::event::KeyModifiers::empty(),
);
@@ -4730,20 +4736,26 @@ mod tests {
}
#[test]
- fn ghostty_mouse_sgr_pixels_downgrades_to_cell_coordinates() {
+ fn ghostty_mouse_sgr_pixels_preserves_exact_and_downgrades_cell_input() {
let (tx, _rx) = mpsc::channel(4);
let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ terminal.resize(80, 24, 10, 20).unwrap();
terminal.write(b"\x1b[?1003h\x1b[?1006h\x1b[?1016h");
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
- let encoded = pane.encode_mouse_motion(
+ let exact = pane.encode_mouse_motion(
crossterm::event::MouseEventKind::Moved,
- 4,
- 6,
+ crate::input::mouse::Position::Pixels { x: 48, y: 139 },
+ crossterm::event::KeyModifiers::empty(),
+ );
+ let fallback = pane.encode_mouse_motion(
+ crossterm::event::MouseEventKind::Moved,
+ crate::input::mouse::Position::Cell { column: 4, row: 6 },
crossterm::event::KeyModifiers::empty(),
);
- assert_eq!(encoded.as_deref(), Some(&b"\x1b[<35;5;7M"[..]));
+ assert_eq!(exact.as_deref(), Some(&b"\x1b[<35;48;139M"[..]));
+ assert_eq!(fallback.as_deref(), Some(&b"\x1b[<35;5;7M"[..]));
}
#[test]