aboutsummaryrefslogtreecommitdiff
path: root/src/pane
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:28:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:29:28 +0300
commit4a3302d19a2baa3a33e910e0088c24e780905b90 (patch)
treedcaf76fae1c82102d933c9225b17ca7ac5d3a847 /src/pane
parent550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff)
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'src/pane')
-rw-r--r--src/pane/osc.rs90
-rw-r--r--src/pane/terminal.rs252
2 files changed, 295 insertions, 47 deletions
diff --git a/src/pane/osc.rs b/src/pane/osc.rs
index 838515e0..f7cf9eb9 100644
--- a/src/pane/osc.rs
+++ b/src/pane/osc.rs
@@ -144,10 +144,9 @@ impl DefaultColorOscTracker {
}
fn is_default_color_set_osc(body: &[u8]) -> bool {
- matches!(
- parse_default_color_event(body),
- Some(DefaultColorEvent::Set(_))
- )
+ parse_default_color_events(body)
+ .iter()
+ .any(|event| matches!(event, DefaultColorEvent::Set(_)))
}
#[derive(Debug, Default)]
@@ -233,27 +232,43 @@ impl DefaultColorEventTracker {
}
fn finalize(&mut self, end_offset: usize) {
- if let Some(event) = parse_default_color_event(&self.body) {
- self.pending
- .push(DefaultColorTrackedEvent { end_offset, event });
- }
+ self.pending.extend(
+ parse_default_color_events(&self.body)
+ .into_iter()
+ .map(|event| DefaultColorTrackedEvent { end_offset, event }),
+ );
self.body.clear();
}
+ pub(super) fn in_progress_event(&self) -> Option<DefaultColorEvent> {
+ if !matches!(
+ self.state,
+ DefaultColorOscTrackerState::OscBody | DefaultColorOscTrackerState::OscEscape
+ ) {
+ return None;
+ }
+ let mut events = parse_default_color_events(&self.body);
+ (events.len() == 1).then(|| events.remove(0))
+ }
+
pub(super) fn drain_pending(&mut self) -> Vec<DefaultColorTrackedEvent> {
std::mem::take(&mut self.pending)
}
}
-fn parse_default_color_event(body: &[u8]) -> Option<DefaultColorEvent> {
- match body {
+fn parse_default_color_events(body: &[u8]) -> Vec<DefaultColorEvent> {
+ let single = match body {
b"10;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Foreground)),
b"11;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Background)),
b"12;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Cursor)),
b"110" | b"110;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Foreground)),
b"111" | b"111;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Background)),
- _ => parse_palette_color_query(body).or_else(|| parse_default_color_set_event(body)),
+ _ => parse_palette_color_query(body),
+ };
+ if let Some(event) = single {
+ return vec![event];
}
+ parse_default_color_set_events(body)
}
fn parse_palette_color_query(body: &[u8]) -> Option<DefaultColorEvent> {
@@ -270,15 +285,33 @@ fn parse_palette_color_query(body: &[u8]) -> Option<DefaultColorEvent> {
.map(DefaultColorEvent::PaletteQuery)
}
-fn parse_default_color_set_event(body: &[u8]) -> Option<DefaultColorEvent> {
- let separator = body.iter().position(|byte| *byte == b';')?;
- let query = match &body[..separator] {
- b"10" => DefaultColorQuery::Foreground,
- b"11" => DefaultColorQuery::Background,
- _ => return None,
+fn parse_default_color_set_events(body: &[u8]) -> Vec<DefaultColorEvent> {
+ let Some(separator) = body.iter().position(|byte| *byte == b';') else {
+ return Vec::new();
+ };
+ let start = match &body[..separator] {
+ b"10" => 10,
+ b"11" => 11,
+ b"12" => 12,
+ _ => return Vec::new(),
};
- let value = &body[separator + 1..];
- (!value.is_empty() && value != b"?").then_some(DefaultColorEvent::Set(query))
+ body[separator + 1..]
+ .split(|byte| *byte == b';')
+ .filter(|value| !value.is_empty())
+ .enumerate()
+ .filter_map(|(offset, value)| {
+ if value == b"?" {
+ return None;
+ }
+ let query = match start + offset {
+ 10 => DefaultColorQuery::Foreground,
+ 11 => DefaultColorQuery::Background,
+ 12 => DefaultColorQuery::Cursor,
+ _ => return None,
+ };
+ Some(DefaultColorEvent::Set(query))
+ })
+ .collect()
}
/// 256 KiB of base64 ≈ 192 KiB of text — enough for real source-file copies
@@ -1359,6 +1392,25 @@ mod tests {
}
#[test]
+ fn default_color_event_tracker_tracks_each_multi_value_set() {
+ let mut tracker = DefaultColorEventTracker::default();
+
+ tracker.observe(
+ b"\x1b]10;rgb:11/22/33;rgb:44/55/66\x1b\\\x1b]10;?;rgb:77/88/99\x1b\\\x1b]10;;rgb:aa/bb/cc\x1b\\",
+ );
+
+ assert_eq!(
+ tracked_default_color_events(tracker.drain_pending()),
+ vec![
+ DefaultColorEvent::Set(DefaultColorQuery::Foreground),
+ DefaultColorEvent::Set(DefaultColorQuery::Background),
+ DefaultColorEvent::Set(DefaultColorQuery::Background),
+ DefaultColorEvent::Set(DefaultColorQuery::Foreground),
+ ]
+ );
+ }
+
+ #[test]
fn default_color_event_tracker_handles_split_default_color_queries() {
let mut tracker = DefaultColorEventTracker::default();
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 06cdea35..cf7a82f4 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -1107,6 +1107,7 @@ impl GhosttyPaneTerminal {
core.xtgettcap_query_tracker
.observe(filtered_bytes.as_ref());
core.decscusr_tracker.observe(filtered_bytes.as_ref());
+ let in_progress_default_color_event = core.default_color_event_tracker.in_progress_event();
let default_color_events = core.default_color_event_tracker.drain_pending();
let xtgettcap_responses = core.xtgettcap_query_tracker.drain_pending();
let write_started = crate::render_prof::timer();
@@ -1114,6 +1115,7 @@ impl GhosttyPaneTerminal {
&mut core,
filtered_bytes.as_ref(),
default_color_events,
+ in_progress_default_color_event,
xtgettcap_responses,
&mut terminal_responses,
);
@@ -1177,6 +1179,7 @@ impl GhosttyPaneTerminal {
core: &mut GhosttyPaneCore,
bytes: &[u8],
default_color_events: Vec<DefaultColorTrackedEvent>,
+ in_progress_default_color_event: Option<DefaultColorEvent>,
xtgettcap_responses: Vec<XtgettcapResponse>,
terminal_responses: &mut Vec<Bytes>,
) {
@@ -1196,16 +1199,26 @@ impl GhosttyPaneTerminal {
let mut written = 0;
for event in events {
let end_offset = event.end_offset().min(bytes.len());
+ let mut libghostty_responses = Vec::new();
if end_offset > written {
core.terminal.write(&bytes[written..end_offset]);
- terminal_responses.extend(self.drain_pending_pty_responses());
+ libghostty_responses = self.drain_pending_pty_responses();
written = end_offset;
}
match event {
OrderedPtyResponseEvent::DefaultColor(event) => {
- respond_to_default_color_event(core, terminal_responses, event.event);
+ let replacement = respond_to_default_color_event(core, event.event);
+ if replacement.is_some() {
+ remove_last_matching_libghostty_color_reply(
+ &mut libghostty_responses,
+ event.event,
+ );
+ }
+ terminal_responses.extend(libghostty_responses);
+ terminal_responses.extend(replacement);
}
OrderedPtyResponseEvent::Xtgettcap(response) => {
+ terminal_responses.extend(libghostty_responses);
terminal_responses.push(response.bytes);
}
}
@@ -1213,7 +1226,13 @@ impl GhosttyPaneTerminal {
if written < bytes.len() {
core.terminal.write(&bytes[written..]);
- terminal_responses.extend(self.drain_pending_pty_responses());
+ let mut libghostty_responses = self.drain_pending_pty_responses();
+ if let Some(event) = in_progress_default_color_event {
+ if default_color_event_response(core, event).is_some() {
+ remove_last_matching_libghostty_color_reply(&mut libghostty_responses, event);
+ }
+ }
+ terminal_responses.extend(libghostty_responses);
}
if !core.child_default_foreground_changed && !core.child_default_background_changed {
@@ -1777,7 +1796,7 @@ impl GhosttyPaneTerminal {
Ok(rows) => rows,
Err(_) => return,
};
- let mut grapheme_codepoints = Vec::new();
+ let mut grapheme_bytes = Vec::new();
let mut symbol_scratch = String::new();
let mut y = 0u16;
while y < area.height && rows.next() {
@@ -1800,7 +1819,7 @@ impl GhosttyPaneTerminal {
&cells,
basic.wide,
hide_kitty_placeholders,
- &mut grapheme_codepoints,
+ &mut grapheme_bytes,
&mut symbol_scratch,
) {
Ok(symbol) => symbol,
@@ -2031,7 +2050,7 @@ fn ghostty_collect_dirty_patch(
let Ok(mut rows) = render_state.populate_row_iterator(&mut row_iterator) else {
fallback!("populate_rows_error");
};
- let mut grapheme_codepoints = Vec::new();
+ let mut grapheme_bytes = Vec::new();
let mut symbol_scratch = String::new();
let mut patch_rows = Vec::new();
let mut y = 0u16;
@@ -2069,7 +2088,7 @@ fn ghostty_collect_dirty_patch(
&cells,
basic.wide,
hide_kitty_placeholders,
- &mut grapheme_codepoints,
+ &mut grapheme_bytes,
&mut symbol_scratch,
) {
Ok(symbol) => symbol.to_owned(),
@@ -2475,7 +2494,7 @@ fn ghostty_buffer_symbol_into<'a>(
cells: &crate::ghostty::RowCellIter<'_>,
wide: crate::ghostty::CellWide,
hide_kitty_placeholders: bool,
- grapheme_codepoints: &mut Vec<u32>,
+ grapheme_bytes: &mut Vec<u8>,
symbol_scratch: &'a mut String,
) -> Result<&'a str, crate::ghostty::Error> {
symbol_scratch.clear();
@@ -2483,7 +2502,7 @@ fn ghostty_buffer_symbol_into<'a>(
crate::ghostty::CellWide::SpacerTail => {}
crate::ghostty::CellWide::SpacerHead => symbol_scratch.push(' '),
crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::Wide => {
- cells.grapheme_text_into(grapheme_codepoints, symbol_scratch)?;
+ cells.grapheme_text_into(grapheme_bytes, symbol_scratch)?;
let hidden_kitty_placeholder = hide_kitty_placeholders
&& symbol_scratch.chars().next().map(u32::from)
== Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER);
@@ -2640,30 +2659,59 @@ impl OrderedPtyResponseEvent {
}
}
+fn remove_last_matching_libghostty_color_reply(
+ responses: &mut Vec<Bytes>,
+ event: DefaultColorEvent,
+) {
+ if let Some(index) = responses
+ .iter()
+ .rposition(|response| is_matching_libghostty_color_reply(response, event))
+ {
+ responses.remove(index);
+ }
+}
+
+fn is_matching_libghostty_color_reply(response: &Bytes, event: DefaultColorEvent) -> bool {
+ let prefix = match event {
+ DefaultColorEvent::Query(query) => format!("\x1b]{};rgb:", query.osc_number()),
+ DefaultColorEvent::PaletteQuery(index) => format!("\x1b]4;{index};rgb:"),
+ DefaultColorEvent::Set(_) | DefaultColorEvent::Reset(_) => return false,
+ };
+ response.starts_with(prefix.as_bytes())
+ && (response.ends_with(b"\x07") || response.ends_with(b"\x1b\\"))
+}
+
fn respond_to_default_color_event(
core: &mut GhosttyPaneCore,
- terminal_responses: &mut Vec<Bytes>,
event: DefaultColorEvent,
-) {
+) -> Option<Bytes> {
match event {
- DefaultColorEvent::Query(query) => {
- if let Some(response) = default_color_query_response(query, core) {
- terminal_responses.push(response);
- }
+ DefaultColorEvent::Query(_) | DefaultColorEvent::PaletteQuery(_) => {
+ default_color_event_response(core, event)
}
- DefaultColorEvent::PaletteQuery(index) => {
- if let Some(response) = palette_color_query_response(index, core) {
- terminal_responses.push(response);
- }
+ DefaultColorEvent::Set(query) => {
+ mark_child_default_color_changed(core, query, true);
+ None
}
- DefaultColorEvent::Set(query) => mark_child_default_color_changed(core, query, true),
DefaultColorEvent::Reset(query) => {
mark_child_default_color_changed(core, query, false);
apply_cached_host_default_color(core, query);
+ None
}
}
}
+fn default_color_event_response(
+ core: &mut GhosttyPaneCore,
+ event: DefaultColorEvent,
+) -> Option<Bytes> {
+ match event {
+ DefaultColorEvent::Query(query) => default_color_query_response(query, core),
+ DefaultColorEvent::PaletteQuery(index) => palette_color_query_response(index, core),
+ DefaultColorEvent::Set(_) | DefaultColorEvent::Reset(_) => None,
+ }
+}
+
fn default_color_query_response(
query: DefaultColorQuery,
core: &mut GhosttyPaneCore,
@@ -4021,7 +4069,7 @@ mod tests {
.populate_row_iterator(&mut row_iterator)
.unwrap();
let mut row_cells = crate::ghostty::RowCells::new().unwrap();
- let mut grapheme_codepoints = Vec::new();
+ let mut grapheme_bytes = Vec::new();
let mut symbol_scratch = String::new();
let mut out = Vec::new();
@@ -4033,7 +4081,7 @@ mod tests {
&cells,
wide,
false,
- &mut grapheme_codepoints,
+ &mut grapheme_bytes,
&mut symbol_scratch,
)
.unwrap()
@@ -4048,7 +4096,6 @@ mod tests {
#[test]
fn grapheme_cluster_mode_renders_flag_emoji_in_single_wide_cell() {
let mut terminal = crate::ghostty::Terminal::new(40, 1, 0).unwrap();
- terminal.enable_grapheme_cluster_mode().unwrap();
terminal.write("🇧🇷".as_bytes());
let cells = render_cells_to_symbols(&mut terminal);
@@ -4064,7 +4111,6 @@ mod tests {
#[test]
fn grapheme_cluster_mode_renders_zwj_family_in_single_wide_cell() {
let mut terminal = crate::ghostty::Terminal::new(40, 1, 0).unwrap();
- terminal.enable_grapheme_cluster_mode().unwrap();
terminal.write("👨\u{200d}👩\u{200d}👧".as_bytes());
let cells = render_cells_to_symbols(&mut terminal);
@@ -4188,6 +4234,27 @@ mod tests {
}
#[test]
+ fn resize_shrinks_both_axes_with_cursor_at_old_bottom() {
+ let (tx, _rx) = mpsc::channel(4);
+ let mut terminal = crate::ghostty::Terminal::new(8, 4, 10_000).unwrap();
+ terminal.write(b"alpha\r\nbeta\r\ngamma\r\ndelta");
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+
+ pane.resize(3, 7, 8, 16);
+
+ assert_eq!(pane.visible_text(), "beta\ngamma\ndelta\n");
+ assert_eq!(pane.detection_text(), "beta\ngamma\ndelta\n");
+ assert_eq!(
+ pane.scroll_metrics(),
+ Some(ScrollMetrics {
+ offset_from_bottom: 0,
+ max_offset_from_bottom: 1,
+ viewport_rows: 3,
+ })
+ );
+ }
+
+ #[test]
fn resize_reflow_keeps_scrolled_viewport_and_bottom_detection_sane() {
let (tx, _rx) = mpsc::channel(4);
let mut terminal = crate::ghostty::Terminal::new(12, 4, 10_000).unwrap();
@@ -4508,6 +4575,19 @@ mod tests {
}
#[test]
+ fn process_pty_bytes_does_not_advertise_unsupported_glyph_protocol() {
+ 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_25a1;s\x1b\\", &tx);
+
+ assert!(result.terminal_responses.is_empty());
+ assert!(rx.try_recv().is_err());
+ }
+
+ #[test]
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();
@@ -4655,7 +4735,10 @@ mod tests {
});
let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]11;?\x07", &tx);
- assert!(result.terminal_responses.is_empty());
+ assert_eq!(
+ result.terminal_responses,
+ vec![Bytes::from_static(b"\x1b]11;rgb:1111/2222/3333\x07")]
+ );
assert!(rx.try_recv().is_err());
}
@@ -4884,7 +4967,7 @@ mod tests {
}
#[test]
- fn process_pty_bytes_ignores_malformed_palette_color_queries() {
+ fn process_pty_bytes_ignores_malformed_and_preserves_multi_palette_queries() {
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();
@@ -4897,7 +4980,15 @@ mod tests {
&tx,
);
- assert!(result.terminal_responses.is_empty());
+ assert_eq!(result.terminal_responses.len(), 1);
+ assert!(result.terminal_responses[0].starts_with(b"\x1b]4;0;rgb:"));
+ assert_eq!(
+ result.terminal_responses[0]
+ .windows(4)
+ .filter(|window| *window == b"rgb:")
+ .count(),
+ 2
+ );
assert!(rx.try_recv().is_err());
}
@@ -4957,6 +5048,108 @@ mod tests {
}
#[test]
+ fn process_pty_bytes_preserves_untracked_multi_color_query_responses() {
+ 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: Some(crate::terminal_theme::RgbColor {
+ r: 0x65,
+ g: 0x7b,
+ b: 0x83,
+ }),
+ background: Some(crate::terminal_theme::RgbColor {
+ r: 0xfd,
+ g: 0xf6,
+ b: 0xe3,
+ }),
+ });
+
+ let palette = pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?;1;?\x1b\\", &tx);
+ let palette_response = palette.terminal_responses.concat();
+ assert!(palette_response.starts_with(b"\x1b]4;0;rgb:"));
+ assert_eq!(
+ palette_response
+ .windows(4)
+ .filter(|window| *window == b"rgb:")
+ .count(),
+ 2
+ );
+
+ let defaults = pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?;?;?\x1b\\", &tx);
+ let default_response = defaults.terminal_responses.concat();
+ assert!(
+ default_response.starts_with(b"\x1b]10;rgb:"),
+ "unexpected default-color report: {:?}",
+ String::from_utf8_lossy(&default_response)
+ );
+ assert_eq!(
+ default_response
+ .windows(4)
+ .filter(|window| *window == b"rgb:")
+ .count(),
+ 3
+ );
+ let core = pane.core.lock().unwrap();
+ assert!(!core.child_default_foreground_changed);
+ assert!(!core.child_default_background_changed);
+ drop(core);
+ assert!(rx.try_recv().is_err());
+ }
+
+ #[test]
+ fn process_pty_bytes_preserves_earlier_aggregate_palette_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);
+
+ let result =
+ pane.process_pty_bytes(pane_id, 0, b"\x1b]4;0;?;1;?\x1b\\\x1b]4;0;?\x1b\\", &tx);
+
+ assert_eq!(result.terminal_responses.len(), 2);
+ assert_eq!(
+ result.terminal_responses[0]
+ .windows(4)
+ .filter(|window| *window == b"rgb:")
+ .count(),
+ 2
+ );
+ assert!(result.terminal_responses[1].starts_with(b"\x1b]4;0;rgb:"));
+ assert!(rx.try_recv().is_err());
+ }
+
+ #[test]
+ fn process_pty_bytes_preserves_libghostty_reply_for_child_color_override() {
+ 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.process_pty_bytes(pane_id, 0, b"\x1b]10;rgb:11/22/33\x07", &tx);
+ let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x1b\\", &tx);
+
+ assert_eq!(result.terminal_responses.len(), 1);
+ assert!(result.terminal_responses[0].starts_with(b"\x1b]10;rgb:1111/2222/3333"));
+ assert!(rx.try_recv().is_err());
+ }
+
+ #[test]
+ fn process_pty_bytes_tracks_later_multi_value_color_set() {
+ let (tx, _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.process_pty_bytes(pane_id, 0, b"\x1b]10;?;rgb:44/55/66\x1b\\", &tx);
+
+ let core = pane.core.lock().unwrap();
+ assert!(!core.child_default_foreground_changed);
+ assert!(core.child_default_background_changed);
+ }
+
+ #[test]
fn process_pty_bytes_returns_cursor_color_query_response_from_foreground_fallback() {
let (tx, mut rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();
@@ -5140,7 +5333,10 @@ mod tests {
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_eq!(
+ result.terminal_responses,
+ vec![Bytes::from_static(b"\x1b]11;rgb:1111/2222/3333\x07")]
+ );
assert!(rx.try_recv().is_err());
let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]111\x07\x1b]11;?\x07", &tx);