aboutsummaryrefslogtreecommitdiff
path: root/src/pane/terminal.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/pane/terminal.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff)
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations
Diffstat (limited to 'src/pane/terminal.rs')
-rw-r--r--src/pane/terminal.rs499
1 files changed, 260 insertions, 239 deletions
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 37de333b..df84e5cb 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -69,6 +69,20 @@ pub(crate) struct TerminalTextMatch {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum TerminalSearchDirection {
+ Forward,
+ Backward,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct TerminalSearchWindow {
+ pub matches: Vec<TerminalTextMatch>,
+ pub current: Option<usize>,
+ pub current_global: Option<usize>,
+ pub total: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TerminalWordMotion {
NextStart,
PreviousStart,
@@ -241,69 +255,32 @@ impl PaneTerminal {
self.ghostty.scroll_metrics()
}
- pub(crate) fn search_text_matches(
+ pub(crate) fn search_text_window(
&self,
query: &str,
case_sensitive: bool,
- ) -> Vec<TerminalTextMatch> {
+ direction: TerminalSearchDirection,
+ cursor: TerminalTextPoint,
+ previous: Option<(TerminalTextPoint, TerminalTextPoint)>,
+ limit: usize,
+ ) -> TerminalSearchWindow {
let Some((buffer, active_screen)) = self.retained_text_buffer() else {
- return Vec::new();
- };
- buffer.search(query, case_sensitive, active_screen)
- }
-
- pub(crate) fn text_match_is_current(&self, text_match: TerminalTextMatch) -> bool {
- self.text_matches_are_current(&[text_match])
- .first()
- .copied()
- .unwrap_or(false)
- }
-
- pub(crate) fn text_matches_are_current(&self, text_matches: &[TerminalTextMatch]) -> Vec<bool> {
- if text_matches.is_empty() {
- return Vec::new();
- }
- let Ok(core) = self.ghostty.core.lock() else {
- return vec![false; text_matches.len()];
- };
- let Some(cols) = core.terminal.cols().ok() else {
- return vec![false; text_matches.len()];
- };
- let Some(active_screen) = core.terminal.active_screen().ok() else {
- return vec![false; text_matches.len()];
- };
- let row_range = text_matches
- .iter()
- .filter(|text_match| {
- text_match.scan_cols == cols && text_match.scan_screen == active_screen
- })
- .fold(None::<(u32, u32)>, |range, text_match| {
- Some(match range {
- Some((start_row, end_row)) => (
- start_row.min(text_match.start.row),
- end_row.max(text_match.end.row),
- ),
- None => (text_match.start.row, text_match.end.row),
- })
- });
- let Some((start_row, end_row)) = row_range else {
- return vec![false; text_matches.len()];
- };
- let Ok(rows) = core
- .terminal
- .screen_text_rows_range(start_row as usize, end_row.saturating_add(1) as usize)
- else {
- return vec![false; text_matches.len()];
+ return TerminalSearchWindow {
+ matches: Vec::new(),
+ current: None,
+ current_global: None,
+ total: 0,
+ };
};
- let buffer = RetainedTextBuffer::new_search(cols, rows, start_row);
- text_matches
- .iter()
- .map(|text_match| {
- text_match.scan_cols == cols
- && text_match.scan_screen == active_screen
- && buffer.contains_match(*text_match)
- })
- .collect()
+ buffer.search_window(
+ query,
+ case_sensitive,
+ active_screen,
+ direction,
+ cursor,
+ previous,
+ limit,
+ )
}
pub(crate) fn word_motion_target(
@@ -377,6 +354,53 @@ impl PaneTerminal {
}
}
+ pub(crate) fn dimensions(&self) -> Option<(u16, u16)> {
+ let core = self.ghostty.core.lock().ok()?;
+ Some((core.terminal.cols().ok()?, core.terminal.rows().ok()?))
+ }
+
+ pub(crate) fn paragraph_motion_target(
+ &self,
+ row: u32,
+ direction: i8,
+ ) -> Option<TerminalTextPoint> {
+ let core = self.ghostty.core.lock().ok()?;
+ let total_rows = core.terminal.total_rows().ok()?;
+ let current = usize::try_from(row).ok()?;
+ if current >= total_rows || direction == 0 {
+ return None;
+ }
+ let limit = total_rows.min(1000);
+ for distance in 1..limit {
+ let candidate = if direction < 0 {
+ current.checked_sub(distance)?
+ } else {
+ let candidate = current.saturating_add(distance);
+ if candidate >= total_rows {
+ return None;
+ }
+ candidate
+ };
+ let rows = core
+ .terminal
+ .screen_text_rows_range(candidate, candidate.saturating_add(1))
+ .ok()?;
+ let row = rows.first()?;
+ let is_blank = row.cells.iter().all(|cell| {
+ terminal_cell_text(&cell.graphemes)
+ .chars()
+ .all(char::is_whitespace)
+ });
+ if is_blank {
+ return Some(TerminalTextPoint {
+ row: u32::try_from(candidate).ok()?,
+ col: 0,
+ });
+ }
+ }
+ None
+ }
+
fn retained_text_buffer(&self) -> Option<(RetainedTextBuffer, crate::ghostty::ActiveScreen)> {
let (cols, rows, active_screen) = {
let core = self.ghostty.core.lock().ok()?;
@@ -393,10 +417,6 @@ impl PaneTerminal {
self.ghostty.input_state()
}
- pub fn keyboard_report_all_requested(&self) -> bool {
- self.ghostty.keyboard_report_all_requested()
- }
-
pub fn bracketed_paste_enabled(&self) -> bool {
self.ghostty.bracketed_paste_enabled()
}
@@ -409,6 +429,10 @@ impl PaneTerminal {
self.ghostty.mouse_reporting_enabled()
}
+ pub fn modify_other_keys_level(&self) -> u8 {
+ self.ghostty.modify_other_keys_level()
+ }
+
pub fn sgr_pixel_mouse_enabled(&self) -> bool {
self.ghostty.sgr_pixel_mouse_enabled()
}
@@ -530,12 +554,10 @@ impl PaneTerminal {
self.ghostty.terminal_title()
}
- #[allow(dead_code)] // exposed for Stage C (detection loop wiring)
pub fn agent_osc_title(&self) -> String {
self.ghostty.agent_osc_title()
}
- #[allow(dead_code)] // exposed for Stage C (detection loop wiring)
pub fn agent_osc_progress(&self) -> String {
self.ghostty.agent_osc_progress()
}
@@ -742,70 +764,122 @@ impl RetainedTextBuffer {
Self { cols, lines, atoms }
}
- fn search(
+ fn search_window(
&self,
query: &str,
case_sensitive: bool,
active_screen: crate::ghostty::ActiveScreen,
- ) -> Vec<TerminalTextMatch> {
- if query.is_empty() {
- return Vec::new();
+ direction: TerminalSearchDirection,
+ cursor: TerminalTextPoint,
+ previous: Option<(TerminalTextPoint, TerminalTextPoint)>,
+ limit: usize,
+ ) -> TerminalSearchWindow {
+ if query.is_empty() || limit == 0 {
+ return TerminalSearchWindow {
+ matches: Vec::new(),
+ current: None,
+ current_global: None,
+ total: 0,
+ };
}
let Ok(regex) = regex::RegexBuilder::new(&regex::escape(query))
.case_insensitive(!case_sensitive)
.build()
else {
- return Vec::new();
+ return TerminalSearchWindow {
+ matches: Vec::new(),
+ current: None,
+ current_global: None,
+ total: 0,
+ };
+ };
+ let to_match = |line: &LogicalTextLine, found: regex::Match<'_>| {
+ let start_index = line
+ .spans
+ .binary_search_by_key(&found.start(), |span| span.byte_start)
+ .ok()?;
+ let end_index = line
+ .spans
+ .binary_search_by_key(&found.end(), |span| span.byte_end)
+ .ok()?;
+ let start_span = &line.spans[start_index];
+ let end_span = &line.spans[end_index];
+ Some(TerminalTextMatch {
+ start: start_span.start,
+ end: end_span.end,
+ source_fingerprint: text_fingerprint(found.as_str()),
+ scan_cols: self.cols,
+ scan_screen: active_screen,
+ })
};
- let mut matches = Vec::new();
+
+ let origin = match direction {
+ TerminalSearchDirection::Forward => previous.map_or(cursor, |(_, end)| end),
+ TerminalSearchDirection::Backward => previous.map_or(cursor, |(start, _)| start),
+ };
+ let mut total = 0usize;
+ let mut target = None;
for line in &self.lines {
for found in regex.find_iter(&line.text) {
- let Ok(start_index) = line
- .spans
- .binary_search_by_key(&found.start(), |span| span.byte_start)
- else {
+ let Some(text_match) = to_match(line, found) else {
continue;
};
- let Ok(end_index) = line
- .spans
- .binary_search_by_key(&found.end(), |span| span.byte_end)
- else {
+ match direction {
+ TerminalSearchDirection::Forward
+ if target.is_none() && text_match.start > origin =>
+ {
+ target = Some(total);
+ }
+ TerminalSearchDirection::Backward if text_match.end < origin => {
+ target = Some(total);
+ }
+ _ => {}
+ }
+ total = total.saturating_add(1);
+ }
+ }
+ if total == 0 {
+ return TerminalSearchWindow {
+ matches: Vec::new(),
+ current: None,
+ current_global: None,
+ total: 0,
+ };
+ }
+ let target = target.unwrap_or(match direction {
+ TerminalSearchDirection::Forward => 0,
+ TerminalSearchDirection::Backward => total - 1,
+ });
+ let retained = limit.min(total);
+ let start = target
+ .saturating_sub(retained / 2)
+ .min(total.saturating_sub(retained));
+ let end = start.saturating_add(retained);
+ let mut index = 0usize;
+ let mut matches = Vec::with_capacity(retained);
+ for line in &self.lines {
+ for found in regex.find_iter(&line.text) {
+ let Some(text_match) = to_match(line, found) else {
continue;
};
- let start_span = &line.spans[start_index];
- let end_span = &line.spans[end_index];
- matches.push(TerminalTextMatch {
- start: start_span.start,
- end: end_span.end,
- source_fingerprint: text_fingerprint(found.as_str()),
- scan_cols: self.cols,
- scan_screen: active_screen,
- });
+ if index >= start && index < end {
+ matches.push(text_match);
+ }
+ index = index.saturating_add(1);
+ if index >= end {
+ break;
+ }
+ }
+ if index >= end {
+ break;
}
}
- matches
- }
-
- fn contains_match(&self, text_match: TerminalTextMatch) -> bool {
- self.lines.iter().any(|line| {
- let Ok(start_index) = line
- .spans
- .binary_search_by_key(&text_match.start, |span| span.start)
- else {
- return false;
- };
- let Ok(end_index) = line
- .spans
- .binary_search_by_key(&text_match.end, |span| span.end)
- else {
- return false;
- };
- let start_span = &line.spans[start_index];
- let end_span = &line.spans[end_index];
- start_span.byte_start <= end_span.byte_end
- && text_fingerprint(&line.text[start_span.byte_start..end_span.byte_end])
- == text_match.source_fingerprint
- })
+ TerminalSearchWindow {
+ matches,
+ current: Some(target - start),
+ current_global: Some(target),
+ total,
+ }
}
fn word_motion(
@@ -1187,7 +1261,6 @@ impl GhosttyPaneTerminal {
/// Returns the latest OSC 0/2 title retained for agent detection, or `""`
/// if no title has been seen or the last update was an empty clear.
- #[allow(dead_code)] // exposed for Stage C (detection loop wiring)
pub fn agent_osc_title(&self) -> String {
self.core
.lock()
@@ -1197,7 +1270,6 @@ impl GhosttyPaneTerminal {
/// Returns the latest OSC 9 progress payload retained for agent detection,
/// or `""` if none has been seen.
- #[allow(dead_code)] // exposed for Stage C (detection loop wiring)
pub fn agent_osc_progress(&self) -> String {
self.core
.lock()
@@ -1449,7 +1521,6 @@ impl GhosttyPaneTerminal {
let Ok(mut core) = self.core.lock() else {
return;
};
- #[cfg(windows)]
core.kitty_keyboard.observe(ansi.as_bytes());
core.terminal.write(ansi.as_bytes());
#[cfg(windows)]
@@ -1514,6 +1585,7 @@ impl GhosttyPaneTerminal {
}
if input_state.modify_other_keys {
+ core.kitty_keyboard.observe(b"\x1b[>4;2m");
core.terminal.write(b"\x1b[>4;2m");
}
@@ -1675,17 +1747,6 @@ impl GhosttyPaneTerminal {
core.kitty_keyboard.replay_ansi()
}
- pub fn keyboard_report_all_requested(&self) -> bool {
- self.core.lock().is_ok_and(|core| {
- let protocol = crate::input::KeyboardProtocol::from_kitty_flags(
- core.terminal.kitty_keyboard_flags().unwrap_or(0) as u16,
- );
- protocol.reports_all_keys()
- || (protocol.reports_event_types()
- && core.terminal.modify_other_keys_enabled().unwrap_or(false))
- })
- }
-
pub fn bracketed_paste_enabled(&self) -> bool {
self.mode_enabled(crate::ghostty::MODE_BRACKETED_PASTE)
}
@@ -1700,6 +1761,12 @@ impl GhosttyPaneTerminal {
.is_ok_and(|core| core.terminal.mouse_tracking_enabled().unwrap_or(false))
}
+ pub fn modify_other_keys_level(&self) -> u8 {
+ self.core
+ .lock()
+ .map_or(0, |core| core.kitty_keyboard.modify_other_keys_level())
+ }
+
pub fn sgr_pixel_mouse_enabled(&self) -> bool {
self.mode_enabled(crate::ghostty::MODE_MOUSE_SGR_PIXELS)
}
@@ -2363,12 +2430,16 @@ fn ghostty_collect_dirty_patch(
if render_state.update(terminal).is_err() {
fallback!("render_state_update_error");
}
- match render_state.dirty() {
+ let collect_all_rows = match render_state.dirty() {
Ok(crate::ghostty::Dirty::Clean) => finish!(TerminalDirtyPatchOutcome::Clean),
- Ok(crate::ghostty::Dirty::Partial) => {}
- Ok(crate::ghostty::Dirty::Full) => fallback!("dirty_full"),
+ Ok(crate::ghostty::Dirty::Partial) => false,
+ // A full dirty state means that every visible row may have changed. It
+ // is still safe to send this as a bounded patch: the client replaces
+ // only this pane's viewport, rather than falling back to the whole
+ // shell surface.
+ Ok(crate::ghostty::Dirty::Full) => true,
Err(_) => fallback!("dirty_read_error"),
- }
+ };
let colors = render_state.colors().ok();
let default_bg = colors
@@ -2399,7 +2470,7 @@ fn ghostty_collect_dirty_patch(
let Ok(dirty) = rows.dirty() else {
fallback!("row_dirty_read_error");
};
- if dirty {
+ if collect_all_rows || dirty {
match rows.selection() {
Ok(None) => {}
Ok(Some(_)) => fallback!("row_selection_present"),
@@ -2448,6 +2519,9 @@ fn ghostty_collect_dirty_patch(
y += 1;
}
+ // Nothing above mutates dirty state. Only clear it after every row has
+ // been collected successfully, so a safety fallback leaves the next
+ // collection with the same information.
let dirty_ys: std::collections::HashSet<u16> = patch_rows.iter().map(|(row, _)| *row).collect();
if !dirty_ys.is_empty() {
let Ok(mut clear_row_iterator) = crate::ghostty::RowIterator::new() else {
@@ -3347,6 +3421,30 @@ mod tests {
}
#[test]
+ fn dirty_full_collects_bounded_viewport_patch() {
+ let (tx, _rx) = mpsc::channel(4);
+ let mut terminal = crate::ghostty::Terminal::new(4, 3, 200).unwrap();
+ terminal.write(b"one\r\ntwo\r\nthree");
+ let pane = PaneTerminal::new(GhosttyPaneTerminal::new(terminal, tx).unwrap());
+
+ let patch = match pane.collect_dirty_patch(4, 3) {
+ TerminalDirtyPatchOutcome::Patch(patch) => patch,
+ outcome => panic!("expected viewport patch, got {outcome:?}"),
+ };
+
+ assert_eq!(patch.rows.len(), 3);
+ assert_eq!(
+ patch.rows.iter().map(|(row, _)| *row).collect::<Vec<_>>(),
+ vec![0, 1, 2]
+ );
+ assert!(patch.rows.iter().all(|(_, cells)| cells.len() == 4));
+ assert!(matches!(
+ pane.collect_dirty_patch(4, 3),
+ TerminalDirtyPatchOutcome::Clean
+ ));
+ }
+
+ #[test]
fn palette_overrides_are_none_without_an_osc4_write() {
let default = [rgb(1, 2, 3); 256];
assert!(PaletteOverrides::new(&default, &default).is_none());
@@ -3419,7 +3517,17 @@ mod tests {
query: &str,
case_sensitive: bool,
) -> Vec<TerminalTextMatch> {
- buffer.search(query, case_sensitive, crate::ghostty::ActiveScreen::Primary)
+ buffer
+ .search_window(
+ query,
+ case_sensitive,
+ crate::ghostty::ActiveScreen::Primary,
+ TerminalSearchDirection::Forward,
+ TerminalTextPoint { row: 0, col: 0 },
+ None,
+ usize::MAX,
+ )
+ .matches
}
fn write_numbered_lines(terminal: &mut crate::ghostty::Terminal, count: usize) {
@@ -3652,56 +3760,6 @@ mod tests {
}
#[test]
- fn live_terminal_match_validation_rejects_overwritten_text() {
- let (tx, _rx) = mpsc::channel(4);
- let mut terminal = crate::ghostty::Terminal::new(20, 3, 100).unwrap();
- terminal.write(b"alpha needle");
- let pane = PaneTerminal::new(GhosttyPaneTerminal::new(terminal, tx).unwrap());
-
- let text_match = pane.search_text_matches("needle", true)[0];
- assert!(pane.text_match_is_current(text_match));
- pane.ghostty
- .core
- .lock()
- .unwrap()
- .terminal
- .write(b"\r\x1b[2Kalpha changed");
- assert!(!pane.text_match_is_current(text_match));
- }
-
- #[test]
- fn live_terminal_match_validation_handles_soft_wrapped_matches() {
- let (tx, _rx) = mpsc::channel(4);
- let mut terminal = crate::ghostty::Terminal::new(5, 3, 100).unwrap();
- terminal.write(b"abcdef");
- let pane = PaneTerminal::new(GhosttyPaneTerminal::new(terminal, tx).unwrap());
-
- let text_match = pane.search_text_matches("def", true)[0];
-
- assert_eq!(text_match.start, TerminalTextPoint { row: 0, col: 3 });
- assert_eq!(text_match.end, TerminalTextPoint { row: 1, col: 0 });
- assert!(pane.text_match_is_current(text_match));
- }
-
- #[test]
- fn live_terminal_match_validation_rejects_an_active_screen_change() {
- let (tx, _rx) = mpsc::channel(4);
- let mut terminal = crate::ghostty::Terminal::new(20, 3, 100).unwrap();
- terminal.write(b"alpha needle");
- let pane = PaneTerminal::new(GhosttyPaneTerminal::new(terminal, tx).unwrap());
-
- let text_match = pane.search_text_matches("needle", true)[0];
- pane.ghostty
- .core
- .lock()
- .unwrap()
- .terminal
- .write(b"\x1b[?1049hneedle");
-
- assert!(!pane.text_match_is_current(text_match));
- }
-
- #[test]
fn live_terminal_word_motion_expands_across_long_blank_history() {
let (tx, _rx) = mpsc::channel(4);
let mut terminal = crate::ghostty::Terminal::new(10, 3, 200).unwrap();
@@ -3725,7 +3783,16 @@ mod tests {
let word = "a".repeat(132);
terminal.write(word.as_bytes());
let pane = PaneTerminal::new(GhosttyPaneTerminal::new(terminal, tx).unwrap());
- let text_match = pane.search_text_matches(&word, true)[0];
+ let text_match = pane
+ .search_text_window(
+ &word,
+ true,
+ TerminalSearchDirection::Forward,
+ TerminalTextPoint { row: 0, col: 0 },
+ None,
+ 1,
+ )
+ .matches[0];
assert_eq!(
pane.word_motion_target(
@@ -3744,7 +3811,16 @@ mod tests {
let word = "界".repeat(66);
terminal.write(word.as_bytes());
let pane = PaneTerminal::new(GhosttyPaneTerminal::new(terminal, tx).unwrap());
- let text_match = pane.search_text_matches(&word, true)[0];
+ let text_match = pane
+ .search_text_window(
+ &word,
+ true,
+ TerminalSearchDirection::Forward,
+ TerminalTextPoint { row: 0, col: 0 },
+ None,
+ 1,
+ )
+ .matches[0];
// The word end sits on the head cell of the final wide glyph, past the
// initial read window, so the window has to expand to reach it.
@@ -4476,6 +4552,7 @@ mod tests {
color_scheme_reporting: true,
})
);
+ assert_eq!(pane.modify_other_keys_level(), 2);
let encoded = pane.encode_terminal_key(
crate::input::TerminalKey::new(
@@ -4693,6 +4770,7 @@ mod tests {
let key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap();
pane.seed_history_ansi("\x1b[>4;1m");
+ assert_eq!(pane.modify_other_keys_level(), 1);
let encoded = pane.encode_terminal_key(key.clone(), crate::input::KeyboardProtocol::Legacy);
assert_eq!(encoded, b"\x1b[27;2;13~");
@@ -5053,35 +5131,6 @@ mod tests {
}
#[test]
- fn dirty_patch_keeps_halfwidth_katakana_voiced_tail_empty() {
- let (tx, _rx) = mpsc::channel(4);
- let terminal = crate::ghostty::Terminal::new(20, 1, 0).unwrap();
- let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
- let backend = ratatui::backend::TestBackend::new(20, 1);
- let mut terminal = ratatui::Terminal::new(backend).unwrap();
- terminal
- .draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 1), false))
- .unwrap();
- {
- let mut core = pane.core.lock().unwrap();
- core.terminal.write("ガZ".as_bytes());
- }
-
- let patch = match pane.collect_dirty_patch(20, 1) {
- TerminalDirtyPatchOutcome::Patch(patch) => patch,
- other => panic!("expected dirty patch, got {other:?}"),
- };
- let row = &patch.rows[0].1;
-
- assert_eq!(row[0].symbol, "カ\u{ff9e}");
- assert_eq!(
- row[1].symbol, "",
- "wide spacer tail must stay empty in retained terminal patches"
- );
- assert_eq!(row[2].symbol, "Z");
- }
-
- #[test]
fn pane_scrollback_controls_round_trip_and_clamp_without_ui_interference() {
let (tx, _rx) = mpsc::channel(4);
let mut terminal = crate::ghostty::Terminal::new(80, 3, 100).unwrap();
@@ -5974,34 +6023,6 @@ mod tests {
}
#[test]
- fn dirty_patch_preserves_curly_underline_style() {
- let (tx, _rx) = mpsc::channel(4);
- let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();
- let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
- let backend = ratatui::backend::TestBackend::new(20, 5);
- let mut terminal = ratatui::Terminal::new(backend).unwrap();
- terminal
- .draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 5), false))
- .unwrap();
- {
- let mut core = pane.core.lock().unwrap();
- core.terminal.write(b"\x1b[4:3mU");
- }
-
- let patch = match pane.collect_dirty_patch(20, 5) {
- TerminalDirtyPatchOutcome::Patch(patch) => patch,
- other => panic!("expected dirty patch, got {other:?}"),
- };
-
- let cell = &patch.rows[0].1[0];
- assert_eq!(cell.symbol, "U");
- assert_eq!(
- crate::protocol::underline_style_from_modifier(cell.modifier),
- 3
- );
- }
-
- #[test]
fn full_frame_preserves_curly_underline_style() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();