From 41e009f9b7b46d335d41b365e41f9ad57d571ecb Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 31 Mar 2026 20:49:05 +0300 Subject: [PATCH] fix alt-screen scrollback handling --- Cargo.lock | 2 - Cargo.toml | 3 + src/app/input.rs | 66 +- src/app/state.rs | 1 + src/pane.rs | 56 +- src/ui.rs | 271 ++++-- vendor/vt100/.cargo-ok | 1 + vendor/vt100/.cargo_vcs_info.json | 6 + vendor/vt100/CHANGELOG.md | 396 +++++++++ vendor/vt100/Cargo.lock | 540 ++++++++++++ vendor/vt100/Cargo.toml | 75 ++ vendor/vt100/Cargo.toml.orig | 28 + vendor/vt100/LICENSE | 21 + vendor/vt100/README.md | 36 + vendor/vt100/src/attrs.rs | 144 +++ vendor/vt100/src/callbacks.rs | 69 ++ vendor/vt100/src/cell.rs | 179 ++++ vendor/vt100/src/grid.rs | 743 ++++++++++++++++ vendor/vt100/src/lib.rs | 64 ++ vendor/vt100/src/parser.rs | 96 ++ vendor/vt100/src/perform.rs | 277 ++++++ vendor/vt100/src/row.rs | 474 ++++++++++ vendor/vt100/src/screen.rs | 1356 +++++++++++++++++++++++++++++ vendor/vt100/src/term.rs | 551 ++++++++++++ 24 files changed, 5383 insertions(+), 72 deletions(-) create mode 100644 vendor/vt100/.cargo-ok create mode 100644 vendor/vt100/.cargo_vcs_info.json create mode 100644 vendor/vt100/CHANGELOG.md create mode 100644 vendor/vt100/Cargo.lock create mode 100644 vendor/vt100/Cargo.toml create mode 100644 vendor/vt100/Cargo.toml.orig create mode 100644 vendor/vt100/LICENSE create mode 100644 vendor/vt100/README.md create mode 100644 vendor/vt100/src/attrs.rs create mode 100644 vendor/vt100/src/callbacks.rs create mode 100644 vendor/vt100/src/cell.rs create mode 100644 vendor/vt100/src/grid.rs create mode 100644 vendor/vt100/src/lib.rs create mode 100644 vendor/vt100/src/parser.rs create mode 100644 vendor/vt100/src/perform.rs create mode 100644 vendor/vt100/src/row.rs create mode 100644 vendor/vt100/src/screen.rs create mode 100644 vendor/vt100/src/term.rs diff --git a/Cargo.lock b/Cargo.lock index ffdba8e4..6d9489d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1704,8 +1704,6 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vt100" version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9" dependencies = [ "itoa", "unicode-width", diff --git a/Cargo.toml b/Cargo.toml index 66db635c..e4519b18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,6 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tui-term = "0.3" vt100 = "0.16" + +[patch.crates-io] +vt100 = { path = "vendor/vt100" } diff --git a/src/app/input.rs b/src/app/input.rs index 9827d4d2..05667ab9 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -10,6 +10,12 @@ use tracing::warn; use crate::layout::{NavDirection, PaneInfo, SplitBorder}; use crate::selection::Selection; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ScrollbarClickTarget { + Thumb { grab_row_offset: u16 }, + Track { offset_from_bottom: usize }, +} + use super::state::{ key_matches, AppState, ContextMenuKind, ContextMenuState, DragState, DragTarget, Mode, }; @@ -926,14 +932,23 @@ impl AppState { return None; } - if let Some((pane_id, offset_from_bottom)) = + if let Some((pane_id, target)) = self.scrollbar_target_at(mouse.column, mouse.row) { self.focus_pane(pane_id); - self.set_pane_scroll_offset(pane_id, offset_from_bottom); - self.drag = Some(DragState { - target: DragTarget::PaneScrollbar { pane_id }, - }); + match target { + ScrollbarClickTarget::Thumb { grab_row_offset } => { + self.drag = Some(DragState { + target: DragTarget::PaneScrollbar { + pane_id, + grab_row_offset, + }, + }); + } + ScrollbarClickTarget::Track { offset_from_bottom } => { + self.set_pane_scroll_offset(pane_id, offset_from_bottom); + } + } if self.mode != Mode::Terminal { self.mode = Mode::Terminal; } @@ -1022,10 +1037,15 @@ impl AppState { ws.layout.set_ratio_at(&path, ratio); } } - DragTarget::PaneScrollbar { pane_id } => { - if let Some(offset_from_bottom) = - self.scrollbar_offset_for_pane_row(*pane_id, mouse.row) - { + DragTarget::PaneScrollbar { + pane_id, + grab_row_offset, + } => { + if let Some(offset_from_bottom) = self.scrollbar_offset_for_pane_row( + *pane_id, + mouse.row, + *grab_row_offset, + ) { self.set_pane_scroll_offset(*pane_id, offset_from_bottom); } } @@ -1331,7 +1351,11 @@ impl AppState { } } - fn scrollbar_target_at(&self, col: u16, row: u16) -> Option<(crate::layout::PaneId, usize)> { + fn scrollbar_target_at( + &self, + col: u16, + row: u16, + ) -> Option<(crate::layout::PaneId, ScrollbarClickTarget)> { let ws = self.active.and_then(|i| self.workspaces.get(i))?; let info = self.view.pane_infos.iter().find(|info| { crate::ui::pane_scrollbar_rect(info).is_some_and(|track| { @@ -1347,16 +1371,23 @@ impl AppState { return None; } let track = crate::ui::pane_scrollbar_rect(info)?; - Some(( - info.id, - crate::ui::scrollbar_offset_from_row(metrics, track, row), - )) + if let Some(grab_row_offset) = crate::ui::scrollbar_thumb_grab_offset(metrics, track, row) { + Some((info.id, ScrollbarClickTarget::Thumb { grab_row_offset })) + } else { + Some(( + info.id, + ScrollbarClickTarget::Track { + offset_from_bottom: crate::ui::scrollbar_offset_from_row(metrics, track, row), + }, + )) + } } fn scrollbar_offset_for_pane_row( &self, pane_id: crate::layout::PaneId, row: u16, + grab_row_offset: u16, ) -> Option { let ws = self.active.and_then(|i| self.workspaces.get(i))?; let info = self @@ -1370,7 +1401,12 @@ impl AppState { if metrics.max_offset_from_bottom == 0 { return None; } - Some(crate::ui::scrollbar_offset_from_row(metrics, track, row)) + Some(crate::ui::scrollbar_offset_from_drag_row( + metrics, + track, + row, + grab_row_offset, + )) } } diff --git a/src/app/state.rs b/src/app/state.rs index caedb419..cd45bbb6 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -397,6 +397,7 @@ pub(crate) enum DragTarget { }, PaneScrollbar { pane_id: crate::layout::PaneId, + grab_row_offset: u16, }, SidebarDivider, } diff --git a/src/pane.rs b/src/pane.rs index c6b8e175..f25c6a42 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -100,6 +100,12 @@ pub struct ScrollMetrics { pub viewport_rows: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScrollState { + pub metrics: ScrollMetrics, + pub alternate_screen: bool, +} + impl Drop for PaneRuntime { fn drop(&mut self) { // Abort detection task immediately. @@ -124,7 +130,7 @@ fn max_scrollback(parser: &mut vt100::Parser) -> usize { max_scrollback } -fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usize) -> String { +fn parser_rows(parser: &mut vt100::Parser, lines: usize) -> Vec { let max_scrollback = max_scrollback(parser); let screen = parser.screen_mut(); let original_scrollback = screen.scrollback(); @@ -150,7 +156,10 @@ fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usiz screen.set_scrollback(original_scrollback); rows.extend(visible_rows); trim_trailing_blank_rows(&mut rows); + rows +} +fn recent_text_from_rows(rows: &[String], lines: usize) -> String { let start = rows.len().saturating_sub(lines); let text = rows[start..].join("\n"); if text.is_empty() { @@ -160,6 +169,11 @@ fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usiz } } +fn recent_text_from_parser(parser: &mut vt100::Parser, lines: usize) -> String { + let rows = parser_rows(parser, lines); + recent_text_from_rows(&rows, lines) +} + fn wait_for_processes_to_exit(pids: &[u32], timeout: std::time::Duration) -> bool { let deadline = std::time::Instant::now() + timeout; loop { @@ -324,6 +338,7 @@ impl PaneRuntime { Ok(n) => { if let Ok(mut p) = parser.write() { p.process(&buf[..n]); + // Snapshot live screen content for detection. // Always reads at scrollback 0 (current view), // without touching the user's scroll position. @@ -557,20 +572,27 @@ impl PaneRuntime { } } - pub fn scroll_metrics(&self) -> Option { + pub fn scroll_state(&self) -> Option { let Ok(mut parser) = self.parser.write() else { return None; }; let max_offset_from_bottom = max_scrollback(&mut parser); let screen = parser.screen(); let (viewport_rows, _) = screen.size(); - Some(ScrollMetrics { - offset_from_bottom: screen.scrollback(), - max_offset_from_bottom, - viewport_rows: viewport_rows as usize, + Some(ScrollState { + metrics: ScrollMetrics { + offset_from_bottom: screen.scrollback(), + max_offset_from_bottom, + viewport_rows: viewport_rows as usize, + }, + alternate_screen: screen.alternate_screen(), }) } + pub fn scroll_metrics(&self) -> Option { + self.scroll_state().map(|state| state.metrics) + } + pub fn visible_text(&self) -> String { let Ok(content) = self.screen_content.read() else { return String::new(); @@ -636,6 +658,28 @@ mod tests { assert_eq!(rows, vec!["hello".to_string()]); } + #[test] + fn alternate_screen_accumulates_its_own_scrollback() { + let responses = PtyResponses::new(); + let mut parser = vt100::Parser::new_with_callbacks(2, 10, 100, responses); + parser.process(b"\x1b[?1049h1\r\n2\r\n3"); + + assert!(parser.screen().alternate_screen()); + assert_eq!(max_scrollback(&mut parser), 1); + assert_eq!(recent_text_from_parser(&mut parser, 3), "1\n2\n3\n"); + } + + #[test] + fn top_anchored_scroll_regions_feed_scrollback() { + let responses = PtyResponses::new(); + let mut parser = vt100::Parser::new_with_callbacks(5, 10, 100, responses); + parser.process(b"\x1b[?1049h1\r\n2\r\n3\r\n4\r\n5"); + parser.process(b"\x1b[1;3r\x1b[3;1H\r\nX"); + + assert!(parser.screen().alternate_screen()); + assert_eq!(max_scrollback(&mut parser), 1); + } + #[test] fn claude_working_is_sticky_for_short_gap() { let now = std::time::Instant::now(); diff --git a/src/ui.rs b/src/ui.rs index 64564487..df9216a1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,11 +1,8 @@ use ratatui::{ - layout::{Constraint, Layout, Margin, Rect}, + layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{ - Block, Borders, Clear, List, ListItem, ListState, Paragraph, Scrollbar, - ScrollbarOrientation, ScrollbarState, Tabs, - }, + widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs}, Frame, }; use tui_term::widget::PseudoTerminal; @@ -124,10 +121,14 @@ fn compute_pane_infos(app: &AppState, area: Rect) -> Vec { let mut inner_rect = area; let mut scrollbar_rect = None; if let Some(rt) = ws.runtimes.get(&focused_id) { - if rt - .scroll_metrics() - .is_some_and(|metrics| metrics.max_offset_from_bottom > 0 && area.width > 1) - { + let detected_agent = ws + .panes + .get(&focused_id) + .and_then(|pane| pane.detected_agent); + if rt.scroll_state().is_some_and(|state| { + should_show_scrollbar(state.metrics, state.alternate_screen, detected_agent) + && area.width > 1 + }) { inner_rect.width = inner_rect.width.saturating_sub(1); scrollbar_rect = Some(Rect::new( area.x + area.width.saturating_sub(1), @@ -167,10 +168,11 @@ fn compute_pane_infos(app: &AppState, area: Rect) -> Vec { let mut inner_rect = pane_inner; let mut scrollbar_rect = None; if let Some(rt) = ws.runtimes.get(&info.id) { - if rt - .scroll_metrics() - .is_some_and(|metrics| metrics.max_offset_from_bottom > 0 && pane_inner.width > 1) - { + let detected_agent = ws.panes.get(&info.id).and_then(|pane| pane.detected_agent); + if rt.scroll_state().is_some_and(|state| { + should_show_scrollbar(state.metrics, state.alternate_screen, detected_agent) + && pane_inner.width > 1 + }) { inner_rect.width = inner_rect.width.saturating_sub(1); scrollbar_rect = Some(Rect::new( pane_inner.x + pane_inner.width.saturating_sub(1), @@ -633,7 +635,9 @@ fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) { // Draw terminal content if let Ok(parser) = rt.parser.read() { - let pt = PseudoTerminal::new(parser.screen()); + let show_cursor = parser.screen().scrollback() == 0; + let pt = PseudoTerminal::new(parser.screen()) + .cursor(tui_term::widget::Cursor::default().visibility(show_cursor)); frame.render_widget(pt, info.inner_rect); } render_pane_scrollbar(app, frame, info, rt); @@ -695,25 +699,125 @@ pub(crate) fn pane_scrollbar_rect(info: &PaneInfo) -> Option { info.scrollbar_rect } +fn should_show_scrollbar( + metrics: crate::pane::ScrollMetrics, + alternate_screen: bool, + detected_agent: Option, +) -> bool { + if metrics.max_offset_from_bottom == 0 { + return false; + } + + if !alternate_screen { + return true; + } + + metrics.offset_from_bottom > 0 || detected_agent.is_some() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ScrollbarThumb { + pub top: u16, + pub len: u16, +} + +pub(crate) fn scrollbar_thumb( + metrics: crate::pane::ScrollMetrics, + track: Rect, +) -> Option { + if metrics.max_offset_from_bottom == 0 || track.height == 0 { + return None; + } + + let track_height = track.height as usize; + let total_rows = metrics.max_offset_from_bottom + metrics.viewport_rows; + if total_rows == 0 { + return None; + } + + let thumb_len = ((metrics.viewport_rows * track_height) as f32 / total_rows as f32) + .round() + .max(1.0) + .min(track_height as f32) as usize; + let max_thumb_top = track_height.saturating_sub(thumb_len); + let scrolled_from_top = metrics + .max_offset_from_bottom + .saturating_sub(metrics.offset_from_bottom); + let thumb_top = if max_thumb_top == 0 || metrics.max_offset_from_bottom == 0 { + 0 + } else { + ((scrolled_from_top * max_thumb_top) as f32 / metrics.max_offset_from_bottom as f32) + .round() + .clamp(0.0, max_thumb_top as f32) as usize + }; + + Some(ScrollbarThumb { + top: track.y + thumb_top as u16, + len: thumb_len as u16, + }) +} + +pub(crate) fn scrollbar_thumb_grab_offset( + metrics: crate::pane::ScrollMetrics, + track: Rect, + row: u16, +) -> Option { + let thumb = scrollbar_thumb(metrics, track)?; + (row >= thumb.top && row < thumb.top + thumb.len).then_some(row - thumb.top) +} + +fn scrollbar_offset_from_thumb_top( + metrics: crate::pane::ScrollMetrics, + track: Rect, + thumb_top: usize, +) -> usize { + if metrics.max_offset_from_bottom == 0 { + return 0; + } + + let thumb_len = scrollbar_thumb(metrics, track) + .map(|thumb| thumb.len as usize) + .unwrap_or(1); + let max_thumb_top = track.height as usize - thumb_len.min(track.height as usize); + if max_thumb_top == 0 { + return 0; + } + + let desired_top = thumb_top.min(max_thumb_top); + let scrolled_from_top = ((desired_top * metrics.max_offset_from_bottom) as f32 + / max_thumb_top as f32) + .round() as usize; + metrics + .max_offset_from_bottom + .saturating_sub(scrolled_from_top) +} + pub(crate) fn scrollbar_offset_from_row( metrics: crate::pane::ScrollMetrics, track: Rect, row: u16, ) -> usize { - if metrics.max_offset_from_bottom == 0 || track.height <= 1 { - return metrics.max_offset_from_bottom; - } - - let clamped_row = row.clamp(track.y, track.y + track.height.saturating_sub(1)); - let row_offset = clamped_row.saturating_sub(track.y) as f32; - let max_row = track.height.saturating_sub(1) as f32; - let ratio = if max_row == 0.0 { - 0.0 - } else { - row_offset / max_row + let thumb = match scrollbar_thumb(metrics, track) { + Some(thumb) => thumb, + None => return 0, }; - let top_position = (ratio * metrics.max_offset_from_bottom as f32).round() as usize; - metrics.max_offset_from_bottom.saturating_sub(top_position) + let clamped_row = row.clamp(track.y, track.y + track.height.saturating_sub(1)); + let row_offset = clamped_row.saturating_sub(track.y) as usize; + let thumb_center = (thumb.len as usize) / 2; + let desired_top = row_offset.saturating_sub(thumb_center); + scrollbar_offset_from_thumb_top(metrics, track, desired_top) +} + +pub(crate) fn scrollbar_offset_from_drag_row( + metrics: crate::pane::ScrollMetrics, + track: Rect, + row: u16, + grab_row_offset: u16, +) -> usize { + let clamped_row = row.clamp(track.y, track.y + track.height.saturating_sub(1)); + let row_offset = clamped_row.saturating_sub(track.y) as usize; + let desired_top = row_offset.saturating_sub(grab_row_offset as usize); + scrollbar_offset_from_thumb_top(metrics, track, desired_top) } fn render_pane_scrollbar( @@ -732,34 +836,27 @@ fn render_pane_scrollbar( return; } - let content_length = metrics.max_offset_from_bottom + metrics.viewport_rows; - let position = metrics - .max_offset_from_bottom - .saturating_sub(metrics.offset_from_bottom); - let mut scrollbar_state = ScrollbarState::new(content_length) - .position(position) - .viewport_content_length(metrics.viewport_rows); + let Some(thumb) = scrollbar_thumb(metrics, track) else { + return; + }; + let (track_color, thumb_color, thumb_symbol) = if info.is_focused { (app.palette.overlay0, app.palette.overlay1, "▐") } else { (app.palette.surface_dim, app.palette.overlay0, "▕") }; - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(None) - .end_symbol(None) - .track_symbol(Some("▕")) - .track_style(Style::default().fg(track_color)) - .thumb_symbol(thumb_symbol) - .thumb_style(Style::default().fg(thumb_color)); - frame.render_stateful_widget( - scrollbar, - track.inner(Margin { - vertical: 0, - horizontal: 0, - }), - &mut scrollbar_state, - ); + let buf = frame.buffer_mut(); + for y in track.y..track.y + track.height { + let cell = &mut buf[(track.x, y)]; + cell.set_symbol("▕"); + cell.set_style(Style::default().fg(track_color)); + } + for y in thumb.top..thumb.top + thumb.len { + let cell = &mut buf[(track.x, y)]; + cell.set_symbol(thumb_symbol); + cell.set_style(Style::default().fg(thumb_color)); + } } fn render_selection_highlight( @@ -1747,6 +1844,67 @@ mod tests { assert_eq!(pane_scrollbar_rect(&info), Some(Rect::new(10, 1, 1, 6))); } + #[test] + fn alternate_screen_scrollbar_stays_hidden_for_unidentified_live_bottom() { + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: 0, + max_offset_from_bottom: 20, + viewport_rows: 5, + }; + + assert!(!should_show_scrollbar(metrics, true, None)); + } + + #[test] + fn alternate_screen_scrollbar_shows_for_agents_at_live_bottom() { + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: 0, + max_offset_from_bottom: 20, + viewport_rows: 5, + }; + + assert!(should_show_scrollbar( + metrics, + true, + Some(crate::detect::Agent::Codex) + )); + } + + #[test] + fn alternate_screen_scrollbar_shows_when_user_scrolled_up() { + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: 3, + max_offset_from_bottom: 20, + viewport_rows: 5, + }; + + assert!(should_show_scrollbar(metrics, true, None)); + } + + #[test] + fn normal_screen_scrollbar_shows_with_scrollback() { + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: 0, + max_offset_from_bottom: 20, + viewport_rows: 5, + }; + + assert!(should_show_scrollbar(metrics, false, None)); + } + + #[test] + fn scrollbar_thumb_reaches_bottom_when_scrolled_to_bottom() { + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: 0, + max_offset_from_bottom: 20, + viewport_rows: 5, + }; + let track = Rect::new(9, 4, 1, 5); + + let thumb = scrollbar_thumb(metrics, track).expect("thumb"); + assert_eq!(thumb.top + thumb.len, track.y + track.height); + } + #[test] fn scrollbar_offset_mapping_hits_top_middle_and_bottom() { let metrics = crate::pane::ScrollMetrics { @@ -1760,4 +1918,19 @@ mod tests { assert_eq!(scrollbar_offset_from_row(metrics, track, 6), 10); assert_eq!(scrollbar_offset_from_row(metrics, track, 8), 0); } + + #[test] + fn dragging_from_current_thumb_row_preserves_offset() { + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: 7, + max_offset_from_bottom: 20, + viewport_rows: 5, + }; + let track = Rect::new(9, 4, 1, 8); + let thumb = scrollbar_thumb(metrics, track).expect("thumb"); + let row = thumb.top + thumb.len / 2; + let grab = scrollbar_thumb_grab_offset(metrics, track, row).expect("grab"); + + assert_eq!(scrollbar_offset_from_drag_row(metrics, track, row, grab), 7); + } } diff --git a/vendor/vt100/.cargo-ok b/vendor/vt100/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/vendor/vt100/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/vt100/.cargo_vcs_info.json b/vendor/vt100/.cargo_vcs_info.json new file mode 100644 index 00000000..99725106 --- /dev/null +++ b/vendor/vt100/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "eb66ffaf7d771c13303ef73b29f6f2a56fdacecf" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/vt100/CHANGELOG.md b/vendor/vt100/CHANGELOG.md new file mode 100644 index 00000000..d589ed7a --- /dev/null +++ b/vendor/vt100/CHANGELOG.md @@ -0,0 +1,396 @@ +# Changelog + +## [0.16.2] - 2025-07-11 + +### Fixed + +* Fixed potential cursor out of bounds when using decrc after resizing. (#13) + +## [0.16.1] - 2025-07-10 + +### Changed + +* Reverted to the 2021 edition for now. + +## [0.16.0] - 2025-07-08 + +### Added + +* `Parser::process_cb`, which works the same as `Parser::process` except that + it calls callbacks during parsing when it finds a terminal escape which is + potentially useful but not something that affects the screen itself. +* Support for xterm window resize request escape codes, via the new callback + mechanism. +* Support for dim formatting. (Daniel Faust, #9) +* Support for CNL/CPL escape codes. (Danny Weinberg, #10) +* Support for OSC 52 (clipboard manipulation). + +### Removed + +* These methods on `Screen` have been removed in favor of the new callback + API described above: + * `title_formatted` + * `title_diff` + * `title` + * `icon_name` + * `bells_diff` + * `audible_bell_count` + * `visual_bell_count` + * `errors` +* Additionally, unhandled escape sequences no longer log to STDERR; they + instead call various callback methods which can be defined to log if + desired. +* `Cell` no longer implements `Default`. +* `Screen` no longer implements `vte::Perform`. + +### Changed + +* `Parser::set_size` and `Parser::set_scrollback` have been moved to methods + on `Screen`, and `Parser::screen_mut` was added to get a mutable reference + to the screen. +* `Cell::contents` now returns `&str` instead of `String`, eliminating an + allocation in many cases. (Chris Olszewski, #14) + +### Fixed + +* Fixed some issues with calculating scrollback offsets correctly in + `Grid::visible_rows`. (rezigned, #11) + +## [0.15.2] - 2023-02-05 + +### Changed + +* Bumped dependencies + +## [0.15.1] - 2021-12-21 + +### Changed + +* Removed a lot of unnecessary test data from the packaged crate, making + downloads faster + +## [0.15.0] - 2021-12-15 + +### Added + +* `Screen::errors` to track the number of parsing errors seen so far + +### Fixed + +* No longer generate spurious diffs in some cases where the cursor is past the + end of a row +* Fix restoring the cursor position when scrolled back + +### Changed + +* Various internal refactorings + +## [0.14.0] - 2021-12-06 + +### Changed + +* Unknown UTF-8 characters default to a width of 1, rather than 0 (except for + control characters, as mentioned below) + +### Fixed + +* Ignore C1 control characters rather than adding them to the cell data, since + they are non-printable + +## [0.13.2] - 2021-12-05 + +### Changed + +* Delay allocation of the alternate screen until it is used (saves a bit of + memory in basic cases) + +## [0.13.1] - 2021-12-04 + +### Fixed + +* Fixed various line wrapping state issues +* Fixed cursor positioning after writing zero width characters at the end of + the line +* Fixed `Screen::cursor_state_formatted` to draw the last character in a line + with the appropriate drawing attributes if it needs to redraw it + +## [0.13.0] - 2021-11-17 + +### Added + +* `Screen::alternate_screen` to determine if the alternate screen is in use +* `Screen::row_wrapped` to determine whether the row at the given index should + wrap its text +* `Screen::cursor_state_formatted` to set the cursor position and hidden state + (including internal state like the one-past-the-end state which isn't visible + in the return value of `cursor_position`) + +### Fixed + +* `Screen::rows_formatted` now outputs correct escape codes in some edge cases + at the beginning of a row when the previous row was wrapped +* VPA escape sequence can no longer position the cursor off the screen + +## [0.12.0] - 2021-03-09 + +### Added + +* `Screen::state_formatted` and `Screen::state_diff` convenience wrappers + +### Fixed + +* `Screen::attributes_formatted` now correctly resets previously set attributes + where necessary + +### Removed + +* Removed `Screen::attributes_diff`, since I can't actually think of any + situation where it does a thing that makes sense. + +## [0.11.1] - 2021-03-07 + +### Changed + +* Drop dependency on `enumset` + +## [0.11.0] - 2021-03-07 + +### Added + +* `Screen::attributes_formatted` and `Screen::attributes_diff` to retrieve the + current state of the drawing attributes as escape sequences +* `Screen::fgcolor`, `Screen::bgcolor`, `Screen::bold`, `Screen::italic`, + `Screen::underline`, and `Screen::inverse` to retrieve the current state of + the drawing attributes directly + +## [0.10.0] - 2021-03-06 + +### Added + +* Implementation of `std::io::Write` for `Parser` + +## [0.9.0] - 2021-03-05 + +### Added + +* `Screen::contents_between`, for returning the contents logically between two + given cells (for things like clipboard selection) +* Support SGR subparameters (so `\e[38:2:255:0:0m` behaves the same way as + `\e[38;2;255;0;0m`) + +### Fixed + +* Bump `enumset` to fix a dependency which fails to build + +## [0.8.1] - 2020-02-09 + +### Changed + +* Bumped `vte` dep to 0.6. + +## [0.8.0] - 2019-12-07 + +### Removed + +* Removed the unicode-normalization feature altogether - it turns out that it + still has a couple edge cases where it causes incorrect behavior, and fixing + those would be a lot more effort. + +### Fixed + +* Fix a couple more end-of-line/wrapping bugs, especially around cursor + positioning. +* Fix applying combining characters to wide characters. +* Ensure cells can't have contents with width zero (to avoid ambiguity). If an + empty cell gets a combining character applied to it, default that cell to a + (normal-width) space first. + +## [0.7.0] - 2019-11-23 + +### Added + +* New (default-on) cargo feature `unicode-normalization` which can be disabled + to disable normalizing cell contents to NFC - it's a pretty small edge case, + and the data tables required to support it are quite large, which affects + size-sensitive targets like wasm + +## [0.6.3] - 2019-11-20 + +### Fixed + +* Fix output of `contents_formatted` and `contents_diff` when the cursor + position ends at one past the end of a row. +* If the cursor position is one past the end of a row, any char, even a + combining char, needs to cause the cursor position to wrap. + +## [0.6.2] - 2019-11-13 + +### Fixed + +* Fix zero-width characters when the cursor is at the end of a row. + +## [0.6.1] - 2019-11-13 + +### Added + +* Add more debug logging for unhandled escape sequences. + +### Changed + +* Unhandled escape sequence warnings are now at the `debug` log level. + +## [0.6.0] - 2019-11-13 + +### Added + +* `Screen::input_mode_formatted` and `Screen::input_mode_diff` give escape + codes to set the current terminal input modes. +* `Screen::title_formatted` and `Screen::title_diff` give escape codes to set + the terminal window title. +* `Screen::bells_diff` gives escape codes to trigger any audible or visual + bells which have been seen since the previous state. + +### Changed + +* `Screen::contents_diff` no longer includes audible or visual bells (see + `Screen::bells_diff` instead). + +## [0.5.1] - 2019-11-12 + +### Fixed + +* `Screen::set_size` now actually resizes when requested (previously the + underlying storage was not being resized, leading to panics when writing + outside of the original screen). + +## [0.5.0] - 2019-11-12 + +### Added + +* Scrollback support. +* `Default` impl for `Parser` which creates an 80x24 terminal with no + scrollback. + +### Removed + +* `Parser::screen_mut` (and the `pub` `&mut self` methods on `Screen`). The few + things you can do to change the screen state directly are now exposed as + methods on `Parser` itself. + +### Changed + +* `Cell::contents` now returns a `String` instead of a `&str`. +* `Screen::check_audible_bell` and `Screen::check_visual_bell` have been + replaced with `Screen::audible_bell_count` and `Screen::visual_bell_count`. + You should keep track of the "since the last method call" state yourself + instead of having the screen track it for you. + +### Fixed + +* Lots of performance and output optimizations. +* Clearing a cell now sets all of that cell's attributes to the current + attribute set, since different terminals render different things for an empty + cell based on the attributes. +* `Screen::contents_diff` now includes audible and visual bells when + appropriate. + +## [0.4.0] - 2019-11-08 + +### Removed + +* `Screen::fgcolor`, `Screen::bgcolor`, `Screen::bold`, `Screen::italic`, + `Screen::underline`, `Screen::inverse`, and `Screen::alternate_screen`: + these are just implementation details that people shouldn't need to care + about. + +### Fixed + +* Fixed cursor movement when the cursor position is already outside of an + active scroll region. + +## [0.3.2] - 2019-11-08 + +### Fixed + +* Clearing cells now correctly sets the cell background color. +* Fixed a couple bugs in wide character handling in `contents_formatted` and + `contents_diff`. +* Fixed RI when the cursor is at the top of the screen (fixes scrolling up in + `less`, for instance). +* Fixed VPA incorrectly being clamped to the scroll region. +* Stop treating soft hyphen specially (as far as i can tell, no other terminals + do this, and i'm not sure why i thought it was necessary to begin with). +* `contents_formatted` now also resets attributes at the start, like + `contents_diff` does. + +## [0.3.1] - 2019-11-06 + +### Fixed + +* Make `contents_formatted` explicitly show the cursor when necessary, in case + the cursor was previously hidden. + +## [0.3.0] - 2019-11-06 + +### Added + +* `Screen::rows` which is like `Screen::contents` except that it returns the + data by row instead of all at once, and also allows you to restrict the + region returned to a subset of columns. +* `Screen::rows_formatted` which is like `Screen::rows`, but returns escape + sequences sufficient to draw the requested subset of each row. +* `Screen::contents_diff` and `Screen::rows_diff` which return escape sequences + sufficient to turn the visible state of one screen (or a subset of the screen + in the case of `rows_diff`) into another. + +### Changed + +* The screen is now exposed separately from the parser, and is cloneable. +* `contents_formatted` now returns `Vec` instead of `String`. +* `contents` and `contents_formatted` now only allow getting the contents of + the entire screen rather than a subset (but see the entry for `rows` and + `rows_formatted` above). + +### Removed + +* `Cell::new`, since there's not really any reason that this is useful for + someone to do from outside of the crate. + +### Fixed + +* `contents_formatted` now preserves the state of empty cells instead of + filling them with spaces. +* We now clear the row wrapping state when the number of columns in the + terminal is changed. +* `contents_formatted` now ensures that the cursor has the correct hidden state + and location. +* `contents_formatted` now clears the screen before starting to draw. + +## [0.2.0] - 2019-11-04 + +### Changed + +* Reimplemented in pure safe rust, with a much more accurate parser +* A bunch of minor API tweaks, some backwards-incompatible + +## [0.1.2] - 2016-06-04 + +### Fixed + +* Fix returning uninit memory in get_string_formatted/get_string_plaintext +* Handle emoji and zero width unicode characters properly +* Fix cursor positioning with regards to scroll regions and wrapping +* Fix parsing of (ignored) character set escapes +* Explicitly suppress status report escapes + +## [0.1.1] - 2016-04-28 + +### Fixed + +* Fix builds + +## [0.1.0] - 2016-04-28 + +### Added + +* Initial release diff --git a/vendor/vt100/Cargo.lock b/vendor/vt100/Cargo.lock new file mode 100644 index 00000000..b984ee78 --- /dev/null +++ b/vendor/vt100/Cargo.lock @@ -0,0 +1,540 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand 0.8.5", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "vt100" +version = "0.16.2" +dependencies = [ + "itoa", + "nix", + "quickcheck", + "rand 0.9.1", + "serde", + "serde_json", + "terminal_size", + "unicode-width", + "vte", +] + +[[package]] +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "memchr", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/vendor/vt100/Cargo.toml b/vendor/vt100/Cargo.toml new file mode 100644 index 00000000..098eea05 --- /dev/null +++ b/vendor/vt100/Cargo.toml @@ -0,0 +1,75 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.70" +name = "vt100" +version = "0.16.2" +authors = ["Jesse Luehrs "] +build = false +include = [ + "src/**/*", + "LICENSE", + "README.md", + "CHANGELOG.md", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Library for parsing terminal data" +homepage = "https://github.com/doy/vt100-rust" +readme = "README.md" +keywords = [ + "terminal", + "vt100", +] +categories = [ + "command-line-interface", + "encoding", +] +license = "MIT" +repository = "https://github.com/doy/vt100-rust" + +[lib] +name = "vt100" +path = "src/lib.rs" + +[dependencies.itoa] +version = "1.0.15" + +[dependencies.unicode-width] +version = "0.2.1" + +[dependencies.vte] +version = "0.15.0" + +[dev-dependencies.nix] +version = "0.30.1" +features = ["term"] + +[dev-dependencies.quickcheck] +version = "1.0" + +[dev-dependencies.rand] +version = "0.9" + +[dev-dependencies.serde] +version = "1.0.219" +features = ["derive"] + +[dev-dependencies.serde_json] +version = "1.0.140" + +[dev-dependencies.terminal_size] +version = "0.4.2" diff --git a/vendor/vt100/Cargo.toml.orig b/vendor/vt100/Cargo.toml.orig new file mode 100644 index 00000000..db35e207 --- /dev/null +++ b/vendor/vt100/Cargo.toml.orig @@ -0,0 +1,28 @@ +[package] +name = "vt100" +version = "0.16.2" +authors = ["Jesse Luehrs "] +edition = "2021" +rust-version = "1.70" + +description = "Library for parsing terminal data" +homepage = "https://github.com/doy/vt100-rust" +repository = "https://github.com/doy/vt100-rust" +readme = "README.md" +keywords = ["terminal", "vt100"] +categories = ["command-line-interface", "encoding"] +license = "MIT" +include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] + +[dependencies] +itoa = "1.0.15" +unicode-width = "0.2.1" +vte = "0.15.0" + +[dev-dependencies] +nix = { version = "0.30.1", features = ["term"] } +quickcheck = "1.0" +rand = "0.9" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +terminal_size = "0.4.2" diff --git a/vendor/vt100/LICENSE b/vendor/vt100/LICENSE new file mode 100644 index 00000000..a1dd5b0e --- /dev/null +++ b/vendor/vt100/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Jesse Luehrs + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/vt100/README.md b/vendor/vt100/README.md new file mode 100644 index 00000000..a8e1b8f6 --- /dev/null +++ b/vendor/vt100/README.md @@ -0,0 +1,36 @@ +# vt100 + +This crate parses a terminal byte stream and provides an in-memory +representation of the rendered contents. + +## Overview + +This is essentially the terminal parser component of a graphical terminal +emulator pulled out into a separate crate. Although you can use this crate +to build a graphical terminal emulator, it also contains functionality +necessary for implementing terminal applications that want to run other +terminal applications - programs like `screen` or `tmux` for example. + +## Synopsis + +```rust +let mut parser = vt100::Parser::new(24, 80, 0); + +let screen = parser.screen().clone(); +parser.process(b"this text is \x1b[31mRED\x1b[m"); +assert_eq!( + parser.screen().cell(0, 13).unwrap().fgcolor(), + vt100::Color::Idx(1), +); + +let screen = parser.screen().clone(); +parser.process(b"\x1b[3D\x1b[32mGREEN"); +assert_eq!( + parser.screen().contents_formatted(), + &b"\x1b[?25h\x1b[m\x1b[H\x1b[Jthis text is \x1b[32mGREEN"[..], +); +assert_eq!( + parser.screen().contents_diff(&screen), + &b"\x1b[1;14H\x1b[32mGREEN"[..], +); +``` diff --git a/vendor/vt100/src/attrs.rs b/vendor/vt100/src/attrs.rs new file mode 100644 index 00000000..e5044f2e --- /dev/null +++ b/vendor/vt100/src/attrs.rs @@ -0,0 +1,144 @@ +use crate::term::BufWrite as _; + +/// Represents a foreground or background color for cells. +#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)] +pub enum Color { + /// The default terminal color. + #[default] + Default, + + /// An indexed terminal color. + Idx(u8), + + /// An RGB terminal color. The parameters are (red, green, blue). + Rgb(u8, u8, u8), +} + +const TEXT_MODE_INTENSITY: u8 = 0b0000_0011; +const TEXT_MODE_BOLD: u8 = 0b0000_0001; +const TEXT_MODE_DIM: u8 = 0b0000_0010; +const TEXT_MODE_ITALIC: u8 = 0b0000_0100; +const TEXT_MODE_UNDERLINE: u8 = 0b0000_1000; +const TEXT_MODE_INVERSE: u8 = 0b0001_0000; + +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +pub struct Attrs { + pub fgcolor: Color, + pub bgcolor: Color, + pub mode: u8, +} + +impl Attrs { + pub fn bold(&self) -> bool { + self.mode & TEXT_MODE_BOLD != 0 + } + + pub fn dim(&self) -> bool { + self.mode & TEXT_MODE_DIM != 0 + } + + fn intensity(&self) -> u8 { + self.mode & TEXT_MODE_INTENSITY + } + + pub fn set_bold(&mut self) { + self.mode &= !TEXT_MODE_INTENSITY; + self.mode |= TEXT_MODE_BOLD; + } + + pub fn set_dim(&mut self) { + self.mode &= !TEXT_MODE_INTENSITY; + self.mode |= TEXT_MODE_DIM; + } + + pub fn set_normal_intensity(&mut self) { + self.mode &= !TEXT_MODE_INTENSITY; + } + + pub fn italic(&self) -> bool { + self.mode & TEXT_MODE_ITALIC != 0 + } + + pub fn set_italic(&mut self, italic: bool) { + if italic { + self.mode |= TEXT_MODE_ITALIC; + } else { + self.mode &= !TEXT_MODE_ITALIC; + } + } + + pub fn underline(&self) -> bool { + self.mode & TEXT_MODE_UNDERLINE != 0 + } + + pub fn set_underline(&mut self, underline: bool) { + if underline { + self.mode |= TEXT_MODE_UNDERLINE; + } else { + self.mode &= !TEXT_MODE_UNDERLINE; + } + } + + pub fn inverse(&self) -> bool { + self.mode & TEXT_MODE_INVERSE != 0 + } + + pub fn set_inverse(&mut self, inverse: bool) { + if inverse { + self.mode |= TEXT_MODE_INVERSE; + } else { + self.mode &= !TEXT_MODE_INVERSE; + } + } + + pub fn write_escape_code_diff( + &self, + contents: &mut Vec, + other: &Self, + ) { + if self != other && self == &Self::default() { + crate::term::ClearAttrs.write_buf(contents); + return; + } + + let attrs = crate::term::Attrs::default(); + + let attrs = if self.fgcolor == other.fgcolor { + attrs + } else { + attrs.fgcolor(self.fgcolor) + }; + let attrs = if self.bgcolor == other.bgcolor { + attrs + } else { + attrs.bgcolor(self.bgcolor) + }; + let attrs = if self.intensity() == other.intensity() { + attrs + } else { + attrs.intensity(match self.intensity() { + 0 => crate::term::Intensity::Normal, + TEXT_MODE_BOLD => crate::term::Intensity::Bold, + TEXT_MODE_DIM => crate::term::Intensity::Dim, + _ => unreachable!(), + }) + }; + let attrs = if self.italic() == other.italic() { + attrs + } else { + attrs.italic(self.italic()) + }; + let attrs = if self.underline() == other.underline() { + attrs + } else { + attrs.underline(self.underline()) + }; + let attrs = if self.inverse() == other.inverse() { + attrs + } else { + attrs.inverse(self.inverse()) + }; + + attrs.write_buf(contents); + } +} diff --git a/vendor/vt100/src/callbacks.rs b/vendor/vt100/src/callbacks.rs new file mode 100644 index 00000000..cce6f6c0 --- /dev/null +++ b/vendor/vt100/src/callbacks.rs @@ -0,0 +1,69 @@ +/// This trait is used by the parser to handle extra escape sequences that +/// don't have an impact on the terminal screen directly. +pub trait Callbacks { + /// This callback is called when the terminal requests an audible bell + /// (typically with `^G`). + fn audible_bell(&mut self, _: &mut crate::Screen) {} + /// This callback is called when the terminal requests a visual bell + /// (typically with `\eg`). + fn visual_bell(&mut self, _: &mut crate::Screen) {} + /// This callback is called when the terminal requests a resize + /// (typically with `\e[8;;t`). + fn resize(&mut self, _: &mut crate::Screen, _request: (u16, u16)) {} + /// This callback is called when the terminal requests the window title + /// to be set (typically with `\e]1;\a`) + fn set_window_icon_name( + &mut self, + _: &mut crate::Screen, + _icon_name: &[u8], + ) { + } + /// This callback is called when the terminal requests the window title + /// to be set (typically with `\e]2;\a`) + fn set_window_title(&mut self, _: &mut crate::Screen, _title: &[u8]) {} + /// This callback is called when the terminal requests data to be copied + /// to the system clipboard (typically with `\e]52;<ty>;<data>\a`). Note + /// that `data` will be encoded as base64. + fn copy_to_clipboard( + &mut self, + _: &mut crate::Screen, + _ty: &[u8], + _data: &[u8], + ) { + } + /// This callback is called when the terminal requests data to be pasted + /// from the system clipboard (typically with `\e]52;<ty>;?\a`). + fn paste_from_clipboard(&mut self, _: &mut crate::Screen, _ty: &[u8]) {} + /// This callback is called when the terminal receives an escape sequence + /// which is otherwise not implemented. + fn unhandled_char(&mut self, _: &mut crate::Screen, _c: char) {} + /// This callback is called when the terminal receives a control + /// character which is otherwise not implemented. + fn unhandled_control(&mut self, _: &mut crate::Screen, _b: u8) {} + /// This callback is called when the terminal receives an escape sequence + /// which is otherwise not implemented. + fn unhandled_escape( + &mut self, + _: &mut crate::Screen, + _i1: Option<u8>, + _i2: Option<u8>, + _b: u8, + ) { + } + /// This callback is called when the terminal receives a CSI sequence + /// (`\e[`) which is otherwise not implemented. + fn unhandled_csi( + &mut self, + _: &mut crate::Screen, + _i1: Option<u8>, + _i2: Option<u8>, + _params: &[&[u16]], + _c: char, + ) { + } + /// This callback is called when the terminal receives a OSC sequence + /// (`\e]`) which is otherwise not implemented. + fn unhandled_osc(&mut self, _: &mut crate::Screen, _params: &[&[u8]]) {} +} + +impl Callbacks for () {} diff --git a/vendor/vt100/src/cell.rs b/vendor/vt100/src/cell.rs new file mode 100644 index 00000000..c000cf30 --- /dev/null +++ b/vendor/vt100/src/cell.rs @@ -0,0 +1,179 @@ +use unicode_width::UnicodeWidthChar as _; + +// chosen to make the size of the cell struct 32 bytes +const CONTENT_BYTES: usize = 22; + +const IS_WIDE: u8 = 0b1000_0000; +const IS_WIDE_CONTINUATION: u8 = 0b0100_0000; +const LEN_BITS: u8 = 0b0001_1111; + +/// Represents a single terminal cell. +#[derive(Clone, Debug, Eq)] +pub struct Cell { + contents: [u8; CONTENT_BYTES], + len: u8, + attrs: crate::attrs::Attrs, +} +const _: () = assert!(std::mem::size_of::<Cell>() == 32); + +impl PartialEq<Self> for Cell { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len { + return false; + } + if self.attrs != other.attrs { + return false; + } + let len = self.len(); + self.contents[..len] == other.contents[..len] + } +} + +impl Cell { + pub(crate) fn new() -> Self { + Self { + contents: Default::default(), + len: 0, + attrs: crate::attrs::Attrs::default(), + } + } + + fn len(&self) -> usize { + usize::from(self.len & LEN_BITS) + } + + pub(crate) fn set(&mut self, c: char, a: crate::attrs::Attrs) { + self.len = 0; + self.append_char(0, c); + // strings in this context should always be an arbitrary character + // followed by zero or more zero-width characters, so we should only + // have to look at the first character + self.set_wide(c.width().unwrap_or(1) > 1); + self.attrs = a; + } + + pub(crate) fn append(&mut self, c: char) { + let len = self.len(); + if len >= CONTENT_BYTES - 4 { + return; + } + if len == 0 { + self.contents[0] = b' '; + self.len += 1; + } + + // we already checked that we have space for another codepoint + self.append_char(self.len(), c); + } + + // Writes bytes representing c at start + // Requires caller to verify start <= CODEPOINTS_IN_CELL * 4 + fn append_char(&mut self, start: usize, c: char) { + c.encode_utf8(&mut self.contents[start..]); + self.len += u8::try_from(c.len_utf8()).unwrap(); + } + + pub(crate) fn clear(&mut self, attrs: crate::attrs::Attrs) { + self.len = 0; + self.attrs = attrs; + } + + /// Returns the text contents of the cell. + /// + /// Can include multiple unicode characters if combining characters are + /// used, but will contain at most one character with a non-zero character + /// width. + // Since contents has been constructed by appending chars encoded as UTF-8 it will be valid UTF-8 + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn contents(&self) -> &str { + std::str::from_utf8(&self.contents[..self.len()]).unwrap() + } + + /// Returns whether the cell contains any text data. + #[must_use] + pub fn has_contents(&self) -> bool { + self.len() > 0 + } + + /// Returns whether the text data in the cell represents a wide character. + #[must_use] + pub fn is_wide(&self) -> bool { + self.len & IS_WIDE != 0 + } + + /// Returns whether the cell contains the second half of a wide character + /// (in other words, whether the previous cell in the row contains a wide + /// character) + #[must_use] + pub fn is_wide_continuation(&self) -> bool { + self.len & IS_WIDE_CONTINUATION != 0 + } + + fn set_wide(&mut self, wide: bool) { + if wide { + self.len |= IS_WIDE; + } else { + self.len &= !IS_WIDE; + } + } + + pub(crate) fn set_wide_continuation(&mut self, wide: bool) { + if wide { + self.len |= IS_WIDE_CONTINUATION; + } else { + self.len &= !IS_WIDE_CONTINUATION; + } + } + + pub(crate) fn attrs(&self) -> &crate::attrs::Attrs { + &self.attrs + } + + /// Returns the foreground color of the cell. + #[must_use] + pub fn fgcolor(&self) -> crate::Color { + self.attrs.fgcolor + } + + /// Returns the background color of the cell. + #[must_use] + pub fn bgcolor(&self) -> crate::Color { + self.attrs.bgcolor + } + + /// Returns whether the cell should be rendered with the bold text + /// attribute. + #[must_use] + pub fn bold(&self) -> bool { + self.attrs.bold() + } + + /// Returns whether the cell should be rendered with the dim text + /// attribute. + #[must_use] + pub fn dim(&self) -> bool { + self.attrs.dim() + } + + /// Returns whether the cell should be rendered with the italic text + /// attribute. + #[must_use] + pub fn italic(&self) -> bool { + self.attrs.italic() + } + + /// Returns whether the cell should be rendered with the underlined text + /// attribute. + #[must_use] + pub fn underline(&self) -> bool { + self.attrs.underline() + } + + /// Returns whether the cell should be rendered with the inverse text + /// attribute. + #[must_use] + pub fn inverse(&self) -> bool { + self.attrs.inverse() + } +} diff --git a/vendor/vt100/src/grid.rs b/vendor/vt100/src/grid.rs new file mode 100644 index 00000000..b1c5d341 --- /dev/null +++ b/vendor/vt100/src/grid.rs @@ -0,0 +1,743 @@ +use crate::term::BufWrite as _; + +#[derive(Clone, Debug)] +pub struct Grid { + size: Size, + pos: Pos, + saved_pos: Pos, + rows: Vec<crate::row::Row>, + scroll_top: u16, + scroll_bottom: u16, + origin_mode: bool, + saved_origin_mode: bool, + scrollback: std::collections::VecDeque<crate::row::Row>, + scrollback_len: usize, + scrollback_offset: usize, +} + +impl Grid { + pub fn new(size: Size, scrollback_len: usize) -> Self { + Self { + size, + pos: Pos::default(), + saved_pos: Pos::default(), + rows: vec![], + scroll_top: 0, + scroll_bottom: size.rows - 1, + origin_mode: false, + saved_origin_mode: false, + scrollback: std::collections::VecDeque::new(), + scrollback_len, + scrollback_offset: 0, + } + } + + pub fn allocate_rows(&mut self) { + if self.rows.is_empty() { + self.rows.extend( + std::iter::repeat_with(|| { + crate::row::Row::new(self.size.cols) + }) + .take(usize::from(self.size.rows)), + ); + } + } + + fn new_row(&self) -> crate::row::Row { + crate::row::Row::new(self.size.cols) + } + + pub fn clear(&mut self) { + self.pos = Pos::default(); + self.saved_pos = Pos::default(); + for row in self.drawing_rows_mut() { + row.clear(crate::attrs::Attrs::default()); + } + self.scroll_top = 0; + self.scroll_bottom = self.size.rows - 1; + self.origin_mode = false; + self.saved_origin_mode = false; + } + + pub fn clear_scrollback(&mut self) { + self.scrollback.clear(); + self.scrollback_offset = 0; + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn set_size(&mut self, size: Size) { + if size.cols != self.size.cols { + for row in &mut self.rows { + row.wrap(false); + } + } + + if self.scroll_bottom == self.size.rows - 1 { + self.scroll_bottom = size.rows - 1; + } + + self.size = size; + for row in &mut self.rows { + row.resize(size.cols, crate::Cell::new()); + } + self.rows.resize(usize::from(size.rows), self.new_row()); + + if self.scroll_bottom >= size.rows { + self.scroll_bottom = size.rows - 1; + } + if self.scroll_bottom < self.scroll_top { + self.scroll_top = 0; + } + + self.row_clamp_top(false); + self.row_clamp_bottom(false); + self.col_clamp(); + + if self.saved_pos.row > self.size.rows - 1 { + self.saved_pos.row = self.size.rows - 1; + } + if self.saved_pos.col > self.size.cols - 1 { + self.saved_pos.col = self.size.cols - 1; + } + } + + pub fn pos(&self) -> Pos { + self.pos + } + + pub fn set_pos(&mut self, mut pos: Pos) { + if self.origin_mode { + pos.row = pos.row.saturating_add(self.scroll_top); + } + self.pos = pos; + self.row_clamp_top(self.origin_mode); + self.row_clamp_bottom(self.origin_mode); + self.col_clamp(); + } + + pub fn save_cursor(&mut self) { + self.saved_pos = self.pos; + self.saved_origin_mode = self.origin_mode; + } + + pub fn restore_cursor(&mut self) { + self.pos = self.saved_pos; + self.origin_mode = self.saved_origin_mode; + } + + pub fn visible_rows(&self) -> impl Iterator<Item = &crate::row::Row> { + let scrollback_len = self.scrollback.len(); + let rows_len = self.rows.len(); + self.scrollback + .iter() + .skip(scrollback_len - self.scrollback_offset) + // when scrollback_offset > rows_len (e.g. rows = 3, + // scrollback_len = 10, offset = 9) the skip(10 - 9) + // will take 9 rows instead of 3. we need to set + // the upper bound to rows_len (e.g. 3) + .take(rows_len) + // same for rows_len - scrollback_offset (e.g. 3 - 9). + // it'll panic with overflow. we have to saturate the subtraction. + .chain( + self.rows + .iter() + .take(rows_len.saturating_sub(self.scrollback_offset)), + ) + } + + pub fn drawing_rows(&self) -> impl Iterator<Item = &crate::row::Row> { + self.rows.iter() + } + + pub fn drawing_rows_mut( + &mut self, + ) -> impl Iterator<Item = &mut crate::row::Row> { + self.rows.iter_mut() + } + + pub fn visible_row(&self, row: u16) -> Option<&crate::row::Row> { + self.visible_rows().nth(usize::from(row)) + } + + pub fn drawing_row(&self, row: u16) -> Option<&crate::row::Row> { + self.drawing_rows().nth(usize::from(row)) + } + + pub fn drawing_row_mut( + &mut self, + row: u16, + ) -> Option<&mut crate::row::Row> { + self.drawing_rows_mut().nth(usize::from(row)) + } + + pub fn current_row_mut(&mut self) -> &mut crate::row::Row { + self.drawing_row_mut(self.pos.row) + // we assume self.pos.row is always valid + .unwrap() + } + + pub fn visible_cell(&self, pos: Pos) -> Option<&crate::Cell> { + self.visible_row(pos.row).and_then(|r| r.get(pos.col)) + } + + pub fn drawing_cell(&self, pos: Pos) -> Option<&crate::Cell> { + self.drawing_row(pos.row).and_then(|r| r.get(pos.col)) + } + + pub fn drawing_cell_mut(&mut self, pos: Pos) -> Option<&mut crate::Cell> { + self.drawing_row_mut(pos.row) + .and_then(|r| r.get_mut(pos.col)) + } + + pub fn scrollback_len(&self) -> usize { + self.scrollback_len + } + + pub fn scrollback(&self) -> usize { + self.scrollback_offset + } + + pub fn set_scrollback(&mut self, rows: usize) { + self.scrollback_offset = rows.min(self.scrollback.len()); + } + + pub fn write_contents(&self, contents: &mut String) { + let mut wrapping = false; + for row in self.visible_rows() { + row.write_contents(contents, 0, self.size.cols, wrapping); + if !row.wrapped() { + contents.push('\n'); + } + wrapping = row.wrapped(); + } + + while contents.ends_with('\n') { + contents.truncate(contents.len() - 1); + } + } + + pub fn write_contents_formatted( + &self, + contents: &mut Vec<u8>, + ) -> crate::attrs::Attrs { + crate::term::ClearAttrs.write_buf(contents); + crate::term::ClearScreen.write_buf(contents); + + let mut prev_attrs = crate::attrs::Attrs::default(); + let mut prev_pos = Pos::default(); + let mut wrapping = false; + for (i, row) in self.visible_rows().enumerate() { + // we limit the number of cols to a u16 (see Size), so + // visible_rows() can never return more rows than will fit + let i = i.try_into().unwrap(); + let (new_pos, new_attrs) = row.write_contents_formatted( + contents, + 0, + self.size.cols, + i, + wrapping, + Some(prev_pos), + Some(prev_attrs), + ); + prev_pos = new_pos; + prev_attrs = new_attrs; + wrapping = row.wrapped(); + } + + self.write_cursor_position_formatted( + contents, + Some(prev_pos), + Some(prev_attrs), + ); + + prev_attrs + } + + pub fn write_contents_diff( + &self, + contents: &mut Vec<u8>, + prev: &Self, + mut prev_attrs: crate::attrs::Attrs, + ) -> crate::attrs::Attrs { + let mut prev_pos = prev.pos; + let mut wrapping = false; + let mut prev_wrapping = false; + for (i, (row, prev_row)) in + self.visible_rows().zip(prev.visible_rows()).enumerate() + { + // we limit the number of cols to a u16 (see Size), so + // visible_rows() can never return more rows than will fit + let i = i.try_into().unwrap(); + let (new_pos, new_attrs) = row.write_contents_diff( + contents, + prev_row, + 0, + self.size.cols, + i, + wrapping, + prev_wrapping, + prev_pos, + prev_attrs, + ); + prev_pos = new_pos; + prev_attrs = new_attrs; + wrapping = row.wrapped(); + prev_wrapping = prev_row.wrapped(); + } + + self.write_cursor_position_formatted( + contents, + Some(prev_pos), + Some(prev_attrs), + ); + + prev_attrs + } + + pub fn write_cursor_position_formatted( + &self, + contents: &mut Vec<u8>, + prev_pos: Option<Pos>, + prev_attrs: Option<crate::attrs::Attrs>, + ) { + let prev_attrs = prev_attrs.unwrap_or_default(); + // writing a character to the last column of a row doesn't wrap the + // cursor immediately - it waits until the next character is actually + // drawn. it is only possible for the cursor to have this kind of + // position after drawing a character though, so if we end in this + // position, we need to redraw the character at the end of the row. + if prev_pos != Some(self.pos) && self.pos.col >= self.size.cols { + let mut pos = Pos { + row: self.pos.row, + col: self.size.cols - 1, + }; + if self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and self.size.cols + // - 1 is always a valid column + .unwrap() + .is_wide_continuation() + { + pos.col = self.size.cols - 2; + } + let cell = + // we assume self.pos.row is always valid, and self.size.cols + // - 2 must be a valid column because self.size.cols - 1 is + // always valid and we just checked that the cell at + // self.size.cols - 1 is a wide continuation character, which + // means that the first half of the wide character must be + // before it + self.drawing_cell(pos).unwrap(); + if cell.has_contents() { + if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + } + cell.attrs().write_escape_code_diff(contents, &prev_attrs); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff(contents, cell.attrs()); + } else { + // if the cell doesn't have contents, we can't have gotten + // here by drawing a character in the last column. this means + // that as far as i'm aware, we have to have reached here from + // a newline when we were already after the end of an earlier + // row. in the case where we are already after the end of an + // earlier row, we can just write a few newlines, otherwise we + // also need to do the same as above to get ourselves to after + // the end of a row. + let mut found = false; + for i in (0..self.pos.row).rev() { + pos.row = i; + pos.col = self.size.cols - 1; + if self + .drawing_cell(pos) + // i is always less than self.pos.row, which we assume + // to be always valid, so it must also be valid. + // self.size.cols - 1 is always a valid col. + .unwrap() + .is_wide_continuation() + { + pos.col = self.size.cols - 2; + } + let cell = self + .drawing_cell(pos) + // i is always less than self.pos.row, which we assume + // to be always valid, so it must also be valid. + // self.size.cols - 2 is valid because self.size.cols + // - 1 is always valid, and col gets set to + // self.size.cols - 2 when the cell at self.size.cols + // - 1 is a wide continuation character, meaning that + // the first half of the wide character must be before + // it + .unwrap(); + if cell.has_contents() { + if let Some(prev_pos) = prev_pos { + if prev_pos.row != i + || prev_pos.col < self.size.cols + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + cell.attrs().write_escape_code_diff( + contents, + &prev_attrs, + ); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff( + contents, + cell.attrs(), + ); + } + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + cell.attrs().write_escape_code_diff( + contents, + &prev_attrs, + ); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff( + contents, + cell.attrs(), + ); + } + contents.extend( + "\n".repeat(usize::from(self.pos.row - i)) + .as_bytes(), + ); + found = true; + break; + } + } + + // this can happen if you get the cursor off the end of a row, + // and then do something to clear the end of the current row + // without moving the cursor (IL, DL, ED, EL, etc). we know + // there can't be something in the last column because we + // would have caught that above, so it should be safe to + // overwrite it. + if !found { + pos = Pos { + row: self.pos.row, + col: self.size.cols - 1, + }; + if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + } + contents.push(b' '); + // we know that the cell has no contents, but it still may + // have drawing attributes (background color, etc) + let end_cell = self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and + // self.size.cols - 1 is always a valid column + .unwrap(); + end_cell + .attrs() + .write_escape_code_diff(contents, &prev_attrs); + crate::term::SaveCursor.write_buf(contents); + crate::term::Backspace.write_buf(contents); + crate::term::EraseChar::new(1).write_buf(contents); + crate::term::RestoreCursor.write_buf(contents); + prev_attrs + .write_escape_code_diff(contents, end_cell.attrs()); + } + } + } else if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, self.pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(self.pos).write_buf(contents); + } + } + + pub fn erase_all(&mut self, attrs: crate::attrs::Attrs) { + for row in self.drawing_rows_mut() { + row.clear(attrs); + } + } + + pub fn erase_all_forward(&mut self, attrs: crate::attrs::Attrs) { + let pos = self.pos; + for row in self.drawing_rows_mut().skip(usize::from(pos.row) + 1) { + row.clear(attrs); + } + + self.erase_row_forward(attrs); + } + + pub fn erase_all_backward(&mut self, attrs: crate::attrs::Attrs) { + let pos = self.pos; + for row in self.drawing_rows_mut().take(usize::from(pos.row)) { + row.clear(attrs); + } + + self.erase_row_backward(attrs); + } + + pub fn erase_row(&mut self, attrs: crate::attrs::Attrs) { + self.current_row_mut().clear(attrs); + } + + pub fn erase_row_forward(&mut self, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in pos.col..size.cols { + row.erase(col, attrs); + } + } + + pub fn erase_row_backward(&mut self, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in 0..=pos.col.min(size.cols - 1) { + row.erase(col, attrs); + } + } + + pub fn insert_cells(&mut self, count: u16) { + let size = self.size; + let pos = self.pos; + let wide = pos.col < size.cols + && self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and we know we are + // not off the end of a row because we just checked pos.col < + // size.cols + .unwrap() + .is_wide_continuation(); + let row = self.current_row_mut(); + for _ in 0..count { + if wide { + row.get_mut(pos.col).unwrap().set_wide_continuation(false); + } + row.insert(pos.col, crate::Cell::new()); + if wide { + row.get_mut(pos.col).unwrap().set_wide_continuation(true); + } + } + row.truncate(size.cols); + } + + pub fn delete_cells(&mut self, count: u16) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for _ in 0..(count.min(size.cols - pos.col)) { + row.remove(pos.col); + } + row.resize(size.cols, crate::Cell::new()); + } + + pub fn erase_cells(&mut self, count: u16, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in pos.col..((pos.col.saturating_add(count)).min(size.cols)) { + row.erase(col, attrs); + } + } + + pub fn insert_lines(&mut self, count: u16) { + for _ in 0..count { + self.rows.remove(usize::from(self.scroll_bottom)); + self.rows.insert(usize::from(self.pos.row), self.new_row()); + // self.scroll_bottom is maintained to always be a valid row + self.rows[usize::from(self.scroll_bottom)].wrap(false); + } + } + + pub fn delete_lines(&mut self, count: u16) { + for _ in 0..(count.min(self.size.rows - self.pos.row)) { + self.rows + .insert(usize::from(self.scroll_bottom) + 1, self.new_row()); + self.rows.remove(usize::from(self.pos.row)); + } + } + + pub fn scroll_up(&mut self, count: u16) { + for _ in 0..(count.min(self.size.rows - self.scroll_top)) { + self.rows + .insert(usize::from(self.scroll_bottom) + 1, self.new_row()); + let removed = self.rows.remove(usize::from(self.scroll_top)); + if self.scrollback_len > 0 && self.scroll_top == 0 { + self.scrollback.push_back(removed); + while self.scrollback.len() > self.scrollback_len { + self.scrollback.pop_front(); + } + if self.scrollback_offset > 0 { + self.scrollback_offset = + self.scrollback.len().min(self.scrollback_offset + 1); + } + } + } + } + + pub fn scroll_down(&mut self, count: u16) { + for _ in 0..count { + self.rows.remove(usize::from(self.scroll_bottom)); + self.rows + .insert(usize::from(self.scroll_top), self.new_row()); + // self.scroll_bottom is maintained to always be a valid row + self.rows[usize::from(self.scroll_bottom)].wrap(false); + } + } + + pub fn set_scroll_region(&mut self, top: u16, bottom: u16) { + let bottom = bottom.min(self.size().rows - 1); + if top < bottom { + self.scroll_top = top; + self.scroll_bottom = bottom; + } else { + self.scroll_top = 0; + self.scroll_bottom = self.size().rows - 1; + } + self.pos.row = self.scroll_top; + self.pos.col = 0; + } + + fn in_scroll_region(&self) -> bool { + self.pos.row >= self.scroll_top && self.pos.row <= self.scroll_bottom + } + + pub fn set_origin_mode(&mut self, mode: bool) { + self.origin_mode = mode; + self.set_pos(Pos { row: 0, col: 0 }); + } + + pub fn row_inc_clamp(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_add(count); + self.row_clamp_bottom(in_scroll_region); + } + + pub fn row_inc_scroll(&mut self, count: u16) -> u16 { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_add(count); + let lines = self.row_clamp_bottom(in_scroll_region); + if in_scroll_region { + self.scroll_up(lines); + lines + } else { + 0 + } + } + + pub fn row_dec_clamp(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_sub(count); + self.row_clamp_top(in_scroll_region); + } + + pub fn row_dec_scroll(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + // need to account for clamping by both row_clamp_top and by + // saturating_sub + let extra_lines = count.saturating_sub(self.pos.row); + self.pos.row = self.pos.row.saturating_sub(count); + let lines = self.row_clamp_top(in_scroll_region); + self.scroll_down(lines + extra_lines); + } + + pub fn row_set(&mut self, i: u16) { + self.pos.row = i; + self.row_clamp(); + } + + pub fn col_inc(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_add(count); + } + + pub fn col_inc_clamp(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_add(count); + self.col_clamp(); + } + + pub fn col_dec(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_sub(count); + } + + pub fn col_tab(&mut self) { + self.pos.col -= self.pos.col % 8; + self.pos.col += 8; + self.col_clamp(); + } + + pub fn col_set(&mut self, i: u16) { + self.pos.col = i; + self.col_clamp(); + } + + pub fn col_wrap(&mut self, width: u16, wrap: bool) { + if self.pos.col > self.size.cols - width { + let mut prev_pos = self.pos; + self.pos.col = 0; + let scrolled = self.row_inc_scroll(1); + prev_pos.row -= scrolled; + let new_pos = self.pos; + self.drawing_row_mut(prev_pos.row) + // we assume self.pos.row is always valid, and so prev_pos.row + // must be valid because it is always less than or equal to + // self.pos.row + .unwrap() + .wrap(wrap && prev_pos.row + 1 == new_pos.row); + } + } + + fn row_clamp_top(&mut self, limit_to_scroll_region: bool) -> u16 { + if limit_to_scroll_region && self.pos.row < self.scroll_top { + let rows = self.scroll_top - self.pos.row; + self.pos.row = self.scroll_top; + rows + } else { + 0 + } + } + + fn row_clamp_bottom(&mut self, limit_to_scroll_region: bool) -> u16 { + let bottom = if limit_to_scroll_region { + self.scroll_bottom + } else { + self.size.rows - 1 + }; + if self.pos.row > bottom { + let rows = self.pos.row - bottom; + self.pos.row = bottom; + rows + } else { + 0 + } + } + + fn row_clamp(&mut self) { + if self.pos.row > self.size.rows - 1 { + self.pos.row = self.size.rows - 1; + } + } + + fn col_clamp(&mut self) { + if self.pos.col > self.size.cols - 1 { + self.pos.col = self.size.cols - 1; + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Size { + pub rows: u16, + pub cols: u16, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Pos { + pub row: u16, + pub col: u16, +} diff --git a/vendor/vt100/src/lib.rs b/vendor/vt100/src/lib.rs new file mode 100644 index 00000000..19746a94 --- /dev/null +++ b/vendor/vt100/src/lib.rs @@ -0,0 +1,64 @@ +//! This crate parses a terminal byte stream and provides an in-memory +//! representation of the rendered contents. +//! +//! # Overview +//! +//! This is essentially the terminal parser component of a graphical terminal +//! emulator pulled out into a separate crate. Although you can use this crate +//! to build a graphical terminal emulator, it also contains functionality +//! necessary for implementing terminal applications that want to run other +//! terminal applications - programs like `screen` or `tmux` for example. +//! +//! # Synopsis +//! +//! ``` +//! let mut parser = vt100::Parser::new(24, 80, 0); +//! +//! let screen = parser.screen().clone(); +//! parser.process(b"this text is \x1b[31mRED\x1b[m"); +//! assert_eq!( +//! parser.screen().cell(0, 13).unwrap().fgcolor(), +//! vt100::Color::Idx(1), +//! ); +//! +//! let screen = parser.screen().clone(); +//! parser.process(b"\x1b[3D\x1b[32mGREEN"); +//! assert_eq!( +//! parser.screen().contents_formatted(), +//! &b"\x1b[?25h\x1b[m\x1b[H\x1b[Jthis text is \x1b[32mGREEN"[..], +//! ); +//! assert_eq!( +//! parser.screen().contents_diff(&screen), +//! &b"\x1b[1;14H\x1b[32mGREEN"[..], +//! ); +//! ``` + +#![warn(missing_docs)] +#![warn(clippy::cargo)] +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![warn(clippy::as_conversions)] +#![warn(clippy::get_unwrap)] +#![allow(clippy::cognitive_complexity)] +#![allow(clippy::missing_const_for_fn)] +#![allow(clippy::similar_names)] +#![allow(clippy::struct_excessive_bools)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::type_complexity)] + +mod attrs; +mod callbacks; +mod cell; +mod grid; +mod parser; +mod perform; +mod row; +mod screen; +mod term; + +pub use attrs::Color; +pub use callbacks::Callbacks; +pub use cell::Cell; +pub use parser::Parser; +pub use screen::{MouseProtocolEncoding, MouseProtocolMode, Screen}; diff --git a/vendor/vt100/src/parser.rs b/vendor/vt100/src/parser.rs new file mode 100644 index 00000000..c81495b8 --- /dev/null +++ b/vendor/vt100/src/parser.rs @@ -0,0 +1,96 @@ +/// A parser for terminal output which produces an in-memory representation of +/// the terminal contents. +pub struct Parser<CB: crate::callbacks::Callbacks = ()> { + parser: vte::Parser, + screen: crate::perform::WrappedScreen<CB>, +} + +impl Parser { + /// Creates a new terminal parser of the given size and with the given + /// amount of scrollback. + #[must_use] + pub fn new(rows: u16, cols: u16, scrollback_len: usize) -> Self { + Self { + parser: vte::Parser::new(), + screen: crate::perform::WrappedScreen::new( + rows, + cols, + scrollback_len, + ), + } + } +} + +impl<CB: crate::callbacks::Callbacks> Parser<CB> { + /// Creates a new terminal parser of the given size and with the given + /// amount of scrollback. Terminal events will be reported via method + /// calls on the provided [`Callbacks`](crate::callbacks::Callbacks) + /// implementation. + pub fn new_with_callbacks( + rows: u16, + cols: u16, + scrollback_len: usize, + callbacks: CB, + ) -> Self { + Self { + parser: vte::Parser::new(), + screen: crate::perform::WrappedScreen::new_with_callbacks( + rows, + cols, + scrollback_len, + callbacks, + ), + } + } + + /// Processes the contents of the given byte string, and updates the + /// in-memory terminal state. + pub fn process(&mut self, bytes: &[u8]) { + self.parser.advance(&mut self.screen, bytes); + } + + /// Returns a reference to a [`Screen`](crate::Screen) object containing + /// the terminal state. + #[must_use] + pub fn screen(&self) -> &crate::Screen { + &self.screen.screen + } + + /// Returns a mutable reference to a [`Screen`](crate::Screen) object + /// containing the terminal state. + #[must_use] + pub fn screen_mut(&mut self) -> &mut crate::Screen { + &mut self.screen.screen + } + + /// Returns a reference to the [`Callbacks`](crate::callbacks::Callbacks) + /// state object passed into the constructor. + pub fn callbacks(&self) -> &CB { + &self.screen.callbacks + } + + /// Returns a mutable reference to the + /// [`Callbacks`](crate::callbacks::Callbacks) state object passed into + /// the constructor. + pub fn callbacks_mut(&mut self) -> &mut CB { + &mut self.screen.callbacks + } +} + +impl Default for Parser { + /// Returns a parser with dimensions 80x24 and no scrollback. + fn default() -> Self { + Self::new(24, 80, 0) + } +} + +impl std::io::Write for Parser { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + self.process(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/vendor/vt100/src/perform.rs b/vendor/vt100/src/perform.rs new file mode 100644 index 00000000..e36cfd83 --- /dev/null +++ b/vendor/vt100/src/perform.rs @@ -0,0 +1,277 @@ +const BASE64: &[u8] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +const CLIPBOARD_SELECTOR: &[u8] = b"cpqs01234567"; + +pub struct WrappedScreen<CB: crate::callbacks::Callbacks = ()> { + pub screen: crate::screen::Screen, + pub callbacks: CB, +} + +impl WrappedScreen<()> { + pub fn new(rows: u16, cols: u16, scrollback_len: usize) -> Self { + Self::new_with_callbacks(rows, cols, scrollback_len, ()) + } +} + +impl<CB: crate::callbacks::Callbacks> WrappedScreen<CB> { + pub fn new_with_callbacks( + rows: u16, + cols: u16, + scrollback_len: usize, + callbacks: CB, + ) -> Self { + Self { + screen: crate::screen::Screen::new( + crate::grid::Size { rows, cols }, + scrollback_len, + ), + callbacks, + } + } +} + +impl<CB: crate::callbacks::Callbacks> vte::Perform for WrappedScreen<CB> { + fn print(&mut self, c: char) { + if c == '\u{fffd}' || ('\u{80}'..'\u{a0}').contains(&c) { + self.callbacks.unhandled_char(&mut self.screen, c); + } else { + self.screen.text(c); + } + } + + fn execute(&mut self, b: u8) { + match b { + 7 => self.callbacks.audible_bell(&mut self.screen), + 8 => self.screen.bs(), + 9 => self.screen.tab(), + 10 => self.screen.lf(), + 11 => self.screen.vt(), + 12 => self.screen.ff(), + 13 => self.screen.cr(), + // we don't implement shift in/out alternate character sets, but + // it shouldn't count as an "error" + 14 | 15 => {} + _ => self.callbacks.unhandled_control(&mut self.screen, b), + } + } + + fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, b: u8) { + if let Some(i) = intermediates.first() { + self.callbacks.unhandled_escape( + &mut self.screen, + Some(*i), + intermediates.get(1).copied(), + b, + ); + } else { + match b { + b'7' => self.screen.decsc(), + b'8' => self.screen.decrc(), + b'=' => self.screen.deckpam(), + b'>' => self.screen.deckpnm(), + b'M' => self.screen.ri(), + b'c' => self.screen.ris(), + b'g' => self.callbacks.visual_bell(&mut self.screen), + _ => { + self.callbacks.unhandled_escape( + &mut self.screen, + None, + None, + b, + ); + } + } + } + } + + fn csi_dispatch( + &mut self, + params: &vte::Params, + intermediates: &[u8], + _ignore: bool, + c: char, + ) { + let unhandled = |screen: &mut crate::screen::Screen| { + self.callbacks.unhandled_csi( + screen, + intermediates.first().copied(), + intermediates.get(1).copied(), + ¶ms.iter().collect::<Vec<_>>(), + c, + ); + }; + match intermediates.first() { + None => match c { + '@' => self.screen.ich(canonicalize_params_1(params, 1)), + 'A' => self.screen.cuu(canonicalize_params_1(params, 1)), + 'B' => self.screen.cud(canonicalize_params_1(params, 1)), + 'C' => self.screen.cuf(canonicalize_params_1(params, 1)), + 'D' => self.screen.cub(canonicalize_params_1(params, 1)), + 'E' => self.screen.cnl(canonicalize_params_1(params, 1)), + 'F' => self.screen.cpl(canonicalize_params_1(params, 1)), + 'G' => self.screen.cha(canonicalize_params_1(params, 1)), + 'H' => self.screen.cup(canonicalize_params_2(params, 1, 1)), + 'J' => self + .screen + .ed(canonicalize_params_1(params, 0), unhandled), + 'K' => self + .screen + .el(canonicalize_params_1(params, 0), unhandled), + 'L' => self.screen.il(canonicalize_params_1(params, 1)), + 'M' => self.screen.dl(canonicalize_params_1(params, 1)), + 'P' => self.screen.dch(canonicalize_params_1(params, 1)), + 'S' => self.screen.su(canonicalize_params_1(params, 1)), + 'T' => self.screen.sd(canonicalize_params_1(params, 1)), + 'X' => self.screen.ech(canonicalize_params_1(params, 1)), + 'd' => self.screen.vpa(canonicalize_params_1(params, 1)), + 'm' => self.screen.sgr(params, unhandled), + 'r' => self.screen.decstbm(canonicalize_params_decstbm( + params, + self.screen.grid().size(), + )), + 't' => { + let mut params_iter = params.iter(); + let op = + params_iter.next().and_then(|x| x.first().copied()); + if op == Some(8) { + let (screen_rows, screen_cols) = self.screen.size(); + let rows = + params_iter.next().map_or(screen_rows, |x| { + *x.first().unwrap_or(&screen_rows) + }); + let cols = + params_iter.next().map_or(screen_cols, |x| { + *x.first().unwrap_or(&screen_cols) + }); + self.callbacks.resize(&mut self.screen, (rows, cols)); + } else { + self.callbacks.unhandled_csi( + &mut self.screen, + None, + None, + ¶ms.iter().collect::<Vec<_>>(), + c, + ); + } + } + _ => { + self.callbacks.unhandled_csi( + &mut self.screen, + None, + None, + ¶ms.iter().collect::<Vec<_>>(), + c, + ); + } + }, + Some(b'?') => match c { + 'J' => self + .screen + .decsed(canonicalize_params_1(params, 0), unhandled), + 'K' => self + .screen + .decsel(canonicalize_params_1(params, 0), unhandled), + 'h' => self.screen.decset(params, unhandled), + 'l' => self.screen.decrst(params, unhandled), + _ => { + self.callbacks.unhandled_csi( + &mut self.screen, + Some(b'?'), + intermediates.get(1).copied(), + ¶ms.iter().collect::<Vec<_>>(), + c, + ); + } + }, + Some(i) => { + self.callbacks.unhandled_csi( + &mut self.screen, + Some(*i), + intermediates.get(1).copied(), + ¶ms.iter().collect::<Vec<_>>(), + c, + ); + } + } + } + + fn osc_dispatch(&mut self, params: &[&[u8]], _bel_terminated: bool) { + match params { + [b"0", s] => { + self.callbacks.set_window_icon_name(&mut self.screen, s); + self.callbacks.set_window_title(&mut self.screen, s); + } + [b"1", s] => { + self.callbacks.set_window_icon_name(&mut self.screen, s); + } + [b"2", s] => { + self.callbacks.set_window_title(&mut self.screen, s); + } + [b"52", ty, data] => { + match ( + ty.iter().all(|c| CLIPBOARD_SELECTOR.contains(c)), + *data, + ) { + (true, b"?") => { + self.callbacks + .paste_from_clipboard(&mut self.screen, ty); + } + (true, data) + if data.iter().all(|c| BASE64.contains(c)) => + { + self.callbacks.copy_to_clipboard( + &mut self.screen, + ty, + data, + ); + } + _ => { + self.callbacks + .unhandled_osc(&mut self.screen, params); + } + } + } + _ => { + self.callbacks.unhandled_osc(&mut self.screen, params); + } + } + } +} + +fn canonicalize_params_1(params: &vte::Params, default: u16) -> u16 { + let first = params.iter().next().map_or(0, |x| *x.first().unwrap_or(&0)); + if first == 0 { + default + } else { + first + } +} + +fn canonicalize_params_2( + params: &vte::Params, + default1: u16, + default2: u16, +) -> (u16, u16) { + let mut iter = params.iter(); + let first = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let first = if first == 0 { default1 } else { first }; + + let second = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let second = if second == 0 { default2 } else { second }; + + (first, second) +} + +fn canonicalize_params_decstbm( + params: &vte::Params, + size: crate::grid::Size, +) -> (u16, u16) { + let mut iter = params.iter(); + let top = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let top = if top == 0 { 1 } else { top }; + + let bottom = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let bottom = if bottom == 0 { size.rows } else { bottom }; + + (top, bottom) +} diff --git a/vendor/vt100/src/row.rs b/vendor/vt100/src/row.rs new file mode 100644 index 00000000..4ae8d868 --- /dev/null +++ b/vendor/vt100/src/row.rs @@ -0,0 +1,474 @@ +use crate::term::BufWrite as _; + +#[derive(Clone, Debug)] +pub struct Row { + cells: Vec<crate::Cell>, + wrapped: bool, +} + +impl Row { + pub fn new(cols: u16) -> Self { + Self { + cells: vec![crate::Cell::new(); usize::from(cols)], + wrapped: false, + } + } + + fn cols(&self) -> u16 { + self.cells + .len() + .try_into() + // we limit the number of cols to a u16 (see Size) + .unwrap() + } + + pub fn clear(&mut self, attrs: crate::attrs::Attrs) { + for cell in &mut self.cells { + cell.clear(attrs); + } + self.wrapped = false; + } + + fn cells(&self) -> impl Iterator<Item = &crate::Cell> { + self.cells.iter() + } + + pub fn get(&self, col: u16) -> Option<&crate::Cell> { + self.cells.get(usize::from(col)) + } + + pub fn get_mut(&mut self, col: u16) -> Option<&mut crate::Cell> { + self.cells.get_mut(usize::from(col)) + } + + pub fn insert(&mut self, i: u16, cell: crate::Cell) { + self.cells.insert(usize::from(i), cell); + self.wrapped = false; + } + + pub fn remove(&mut self, i: u16) { + self.clear_wide(i); + self.cells.remove(usize::from(i)); + self.wrapped = false; + } + + pub fn erase(&mut self, i: u16, attrs: crate::attrs::Attrs) { + let wide = self.cells[usize::from(i)].is_wide(); + self.clear_wide(i); + self.cells[usize::from(i)].clear(attrs); + if i == self.cols() - if wide { 2 } else { 1 } { + self.wrapped = false; + } + } + + pub fn truncate(&mut self, len: u16) { + self.cells.truncate(usize::from(len)); + self.wrapped = false; + let last_cell = &mut self.cells[usize::from(len) - 1]; + if last_cell.is_wide() { + last_cell.clear(*last_cell.attrs()); + } + } + + pub fn resize(&mut self, len: u16, cell: crate::Cell) { + self.cells.resize(usize::from(len), cell); + self.wrapped = false; + } + + pub fn wrap(&mut self, wrap: bool) { + self.wrapped = wrap; + } + + pub fn wrapped(&self) -> bool { + self.wrapped + } + + pub fn clear_wide(&mut self, col: u16) { + let cell = &self.cells[usize::from(col)]; + let other = if cell.is_wide() { + &mut self.cells[usize::from(col + 1)] + } else if cell.is_wide_continuation() { + &mut self.cells[usize::from(col - 1)] + } else { + return; + }; + other.clear(*other.attrs()); + } + + pub fn write_contents( + &self, + contents: &mut String, + start: u16, + width: u16, + wrapping: bool, + ) { + let mut prev_was_wide = false; + + let mut prev_col = start; + for (col, cell) in self + .cells() + .enumerate() + .skip(usize::from(start)) + .take(usize::from(width)) + { + if prev_was_wide { + prev_was_wide = false; + continue; + } + prev_was_wide = cell.is_wide(); + + // we limit the number of cols to a u16 (see Size) + let col: u16 = col.try_into().unwrap(); + if cell.has_contents() { + for _ in 0..(col - prev_col) { + contents.push(' '); + } + prev_col += col - prev_col; + + contents.push_str(cell.contents()); + prev_col += if cell.is_wide() { 2 } else { 1 }; + } + } + if prev_col == start && wrapping { + contents.push('\n'); + } + } + + pub fn write_contents_formatted( + &self, + contents: &mut Vec<u8>, + start: u16, + width: u16, + row: u16, + wrapping: bool, + prev_pos: Option<crate::grid::Pos>, + prev_attrs: Option<crate::attrs::Attrs>, + ) -> (crate::grid::Pos, crate::attrs::Attrs) { + let mut prev_was_wide = false; + let default_cell = crate::Cell::new(); + + let mut prev_pos = prev_pos.unwrap_or_else(|| { + if wrapping { + crate::grid::Pos { + row: row - 1, + col: self.cols(), + } + } else { + crate::grid::Pos { row, col: start } + } + }); + let mut prev_attrs = prev_attrs.unwrap_or_default(); + + let first_cell = &self.cells[usize::from(start)]; + if wrapping && first_cell == &default_cell { + let default_attrs = default_cell.attrs(); + if &prev_attrs != default_attrs { + default_attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *default_attrs; + } + contents.push(b' '); + crate::term::Backspace.write_buf(contents); + crate::term::EraseChar::new(1).write_buf(contents); + prev_pos = crate::grid::Pos { row, col: 0 }; + } + + let mut erase: Option<(u16, &crate::attrs::Attrs)> = None; + for (col, cell) in self + .cells() + .enumerate() + .skip(usize::from(start)) + .take(usize::from(width)) + { + if prev_was_wide { + prev_was_wide = false; + continue; + } + prev_was_wide = cell.is_wide(); + + // we limit the number of cols to a u16 (see Size) + let col: u16 = col.try_into().unwrap(); + let pos = crate::grid::Pos { row, col }; + + if let Some((prev_col, attrs)) = erase { + if cell.has_contents() || cell.attrs() != attrs { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)) + .as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::EraseChar::new(pos.col - prev_col) + .write_buf(contents); + erase = None; + } + } + + if cell != &default_cell { + let attrs = cell.attrs(); + if cell.has_contents() { + if pos != prev_pos { + if !wrapping + || prev_pos.row + 1 != pos.row + || prev_pos.col + < self.cols() - u16::from(cell.is_wide()) + || pos.col != 0 + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } + prev_pos = pos; + } + + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + + prev_pos.col += if cell.is_wide() { 2 } else { 1 }; + let cell_contents = cell.contents(); + contents.extend(cell_contents.as_bytes()); + } else if erase.is_none() { + erase = Some((pos.col, attrs)); + } + } + } + if let Some((prev_col, attrs)) = erase { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)).as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::ClearRowForward.write_buf(contents); + } + + (prev_pos, prev_attrs) + } + + // while it's true that most of the logic in this is identical to + // write_contents_formatted, i can't figure out how to break out the + // common parts without making things noticeably slower. + pub fn write_contents_diff( + &self, + contents: &mut Vec<u8>, + prev: &Self, + start: u16, + width: u16, + row: u16, + wrapping: bool, + prev_wrapping: bool, + mut prev_pos: crate::grid::Pos, + mut prev_attrs: crate::attrs::Attrs, + ) -> (crate::grid::Pos, crate::attrs::Attrs) { + let mut prev_was_wide = false; + + let first_cell = &self.cells[usize::from(start)]; + let prev_first_cell = &prev.cells[usize::from(start)]; + if wrapping + && !prev_wrapping + && first_cell == prev_first_cell + && prev_pos.row + 1 == row + && prev_pos.col + >= self.cols() - u16::from(prev_first_cell.is_wide()) + { + let first_cell_attrs = first_cell.attrs(); + if &prev_attrs != first_cell_attrs { + first_cell_attrs + .write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *first_cell_attrs; + } + let mut cell_contents = prev_first_cell.contents(); + let need_erase = if cell_contents.is_empty() { + cell_contents = " "; + true + } else { + false + }; + contents.extend(cell_contents.as_bytes()); + crate::term::Backspace.write_buf(contents); + if prev_first_cell.is_wide() { + crate::term::Backspace.write_buf(contents); + } + if need_erase { + crate::term::EraseChar::new(1).write_buf(contents); + } + prev_pos = crate::grid::Pos { row, col: 0 }; + } + + let mut erase: Option<(u16, &crate::attrs::Attrs)> = None; + for (col, (cell, prev_cell)) in self + .cells() + .zip(prev.cells()) + .enumerate() + .skip(usize::from(start)) + .take(usize::from(width)) + { + if prev_was_wide { + prev_was_wide = false; + continue; + } + prev_was_wide = cell.is_wide(); + + // we limit the number of cols to a u16 (see Size) + let col: u16 = col.try_into().unwrap(); + let pos = crate::grid::Pos { row, col }; + + if let Some((prev_col, attrs)) = erase { + if cell.has_contents() || cell.attrs() != attrs { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)) + .as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::EraseChar::new(pos.col - prev_col) + .write_buf(contents); + erase = None; + } + } + + if cell != prev_cell { + let attrs = cell.attrs(); + if cell.has_contents() { + if pos != prev_pos { + if !wrapping + || prev_pos.row + 1 != pos.row + || prev_pos.col + < self.cols() - u16::from(cell.is_wide()) + || pos.col != 0 + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } + prev_pos = pos; + } + + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + + prev_pos.col += if cell.is_wide() { 2 } else { 1 }; + contents.extend(cell.contents().as_bytes()); + } else if erase.is_none() { + erase = Some((pos.col, attrs)); + } + } + } + if let Some((prev_col, attrs)) = erase { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)).as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::ClearRowForward.write_buf(contents); + } + + // if this row is going from wrapped to not wrapped, we need to erase + // and redraw the last character to break wrapping. if this row is + // wrapped, we need to redraw the last character without erasing it to + // position the cursor after the end of the line correctly so that + // drawing the next line can just start writing and be wrapped. + if (!self.wrapped && prev.wrapped) || (!prev.wrapped && self.wrapped) + { + let end_pos = if self.cells[usize::from(self.cols() - 1)] + .is_wide_continuation() + { + crate::grid::Pos { + row, + col: self.cols() - 2, + } + } else { + crate::grid::Pos { + row, + col: self.cols() - 1, + } + }; + crate::term::MoveFromTo::new(prev_pos, end_pos) + .write_buf(contents); + prev_pos = end_pos; + if !self.wrapped { + crate::term::EraseChar::new(1).write_buf(contents); + } + let end_cell = &self.cells[usize::from(end_pos.col)]; + if end_cell.has_contents() { + let attrs = end_cell.attrs(); + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + contents.extend(end_cell.contents().as_bytes()); + prev_pos.col += if end_cell.is_wide() { 2 } else { 1 }; + } + } + + (prev_pos, prev_attrs) + } +} diff --git a/vendor/vt100/src/screen.rs b/vendor/vt100/src/screen.rs new file mode 100644 index 00000000..a11ed776 --- /dev/null +++ b/vendor/vt100/src/screen.rs @@ -0,0 +1,1356 @@ +use crate::term::BufWrite as _; +use unicode_width::UnicodeWidthChar as _; + +const MODE_APPLICATION_KEYPAD: u8 = 0b0000_0001; +const MODE_APPLICATION_CURSOR: u8 = 0b0000_0010; +const MODE_HIDE_CURSOR: u8 = 0b0000_0100; +const MODE_ALTERNATE_SCREEN: u8 = 0b0000_1000; +const MODE_BRACKETED_PASTE: u8 = 0b0001_0000; + +/// The xterm mouse handling mode currently in use. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub enum MouseProtocolMode { + /// Mouse handling is disabled. + #[default] + None, + + /// Mouse button events should be reported on button press. Also known as + /// X10 mouse mode. + Press, + + /// Mouse button events should be reported on button press and release. + /// Also known as VT200 mouse mode. + PressRelease, + + // Highlight, + /// Mouse button events should be reported on button press and release, as + /// well as when the mouse moves between cells while a button is held + /// down. + ButtonMotion, + + /// Mouse button events should be reported on button press and release, + /// and mouse motion events should be reported when the mouse moves + /// between cells regardless of whether a button is held down or not. + AnyMotion, + // DecLocator, +} + +/// The encoding to use for the enabled [`MouseProtocolMode`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub enum MouseProtocolEncoding { + /// Default single-printable-byte encoding. + #[default] + Default, + + /// UTF-8-based encoding. + Utf8, + + /// SGR-like encoding. + Sgr, + // Urxvt, +} + +/// Represents the overall terminal state. +#[derive(Clone, Debug)] +pub struct Screen { + grid: crate::grid::Grid, + alternate_grid: crate::grid::Grid, + + attrs: crate::attrs::Attrs, + saved_attrs: crate::attrs::Attrs, + + modes: u8, + mouse_protocol_mode: MouseProtocolMode, + mouse_protocol_encoding: MouseProtocolEncoding, +} + +impl Screen { + pub(crate) fn new( + size: crate::grid::Size, + scrollback_len: usize, + ) -> Self { + let mut grid = crate::grid::Grid::new(size, scrollback_len); + grid.allocate_rows(); + Self { + grid, + alternate_grid: crate::grid::Grid::new(size, scrollback_len), + + attrs: crate::attrs::Attrs::default(), + saved_attrs: crate::attrs::Attrs::default(), + + modes: 0, + mouse_protocol_mode: MouseProtocolMode::default(), + mouse_protocol_encoding: MouseProtocolEncoding::default(), + } + } + + /// Resizes the terminal. + pub fn set_size(&mut self, rows: u16, cols: u16) { + self.grid.set_size(crate::grid::Size { rows, cols }); + self.alternate_grid + .set_size(crate::grid::Size { rows, cols }); + } + + /// Returns the current size of the terminal. + /// + /// The return value will be (rows, cols). + #[must_use] + pub fn size(&self) -> (u16, u16) { + let size = self.grid().size(); + (size.rows, size.cols) + } + + /// Scrolls to the given position in the scrollback. + /// + /// This position indicates the offset from the top of the screen, and + /// should be `0` to put the normal screen in view. + /// + /// This affects the return values of methods called on the screen: for + /// instance, `screen.cell(0, 0)` will return the top left corner of the + /// screen after taking the scrollback offset into account. + /// + /// The value given will be clamped to the actual size of the scrollback. + pub fn set_scrollback(&mut self, rows: usize) { + self.grid_mut().set_scrollback(rows); + } + + /// Returns the current position in the scrollback. + /// + /// This position indicates the offset from the top of the screen, and is + /// `0` when the normal screen is in view. + #[must_use] + pub fn scrollback(&self) -> usize { + self.grid().scrollback() + } + + /// Returns the text contents of the terminal. + /// + /// This will not include any formatting information, and will be in plain + /// text format. + #[must_use] + pub fn contents(&self) -> String { + let mut contents = String::new(); + self.write_contents(&mut contents); + contents + } + + fn write_contents(&self, contents: &mut String) { + self.grid().write_contents(contents); + } + + /// Returns the text contents of the terminal by row, restricted to the + /// given subset of columns. + /// + /// This will not include any formatting information, and will be in plain + /// text format. + /// + /// Newlines will not be included. + pub fn rows( + &self, + start: u16, + width: u16, + ) -> impl Iterator<Item = String> + '_ { + self.grid().visible_rows().map(move |row| { + let mut contents = String::new(); + row.write_contents(&mut contents, start, width, false); + contents + }) + } + + /// Returns the text contents of the terminal logically between two cells. + /// This will include the remainder of the starting row after `start_col`, + /// followed by the entire contents of the rows between `start_row` and + /// `end_row`, followed by the beginning of the `end_row` up until + /// `end_col`. This is useful for things like determining the contents of + /// a clipboard selection. + #[must_use] + pub fn contents_between( + &self, + start_row: u16, + start_col: u16, + end_row: u16, + end_col: u16, + ) -> String { + match start_row.cmp(&end_row) { + std::cmp::Ordering::Less => { + let (_, cols) = self.size(); + let mut contents = String::new(); + for (i, row) in self + .grid() + .visible_rows() + .enumerate() + .skip(usize::from(start_row)) + .take(usize::from(end_row) - usize::from(start_row) + 1) + { + if i == usize::from(start_row) { + row.write_contents( + &mut contents, + start_col, + cols - start_col, + false, + ); + if !row.wrapped() { + contents.push('\n'); + } + } else if i == usize::from(end_row) { + row.write_contents(&mut contents, 0, end_col, false); + } else { + row.write_contents(&mut contents, 0, cols, false); + if !row.wrapped() { + contents.push('\n'); + } + } + } + contents + } + std::cmp::Ordering::Equal => { + if start_col < end_col { + self.rows(start_col, end_col - start_col) + .nth(usize::from(start_row)) + .unwrap_or_default() + } else { + String::new() + } + } + std::cmp::Ordering::Greater => String::new(), + } + } + + /// Return escape codes sufficient to reproduce the entire contents of the + /// current terminal state. This is a convenience wrapper around + /// [`contents_formatted`](Self::contents_formatted) and + /// [`input_mode_formatted`](Self::input_mode_formatted). + #[must_use] + pub fn state_formatted(&self) -> Vec<u8> { + let mut contents = vec![]; + self.write_contents_formatted(&mut contents); + self.write_input_mode_formatted(&mut contents); + contents + } + + /// Return escape codes sufficient to turn the terminal state of the + /// screen `prev` into the current terminal state. This is a convenience + /// wrapper around [`contents_diff`](Self::contents_diff) and + /// [`input_mode_diff`](Self::input_mode_diff). + #[must_use] + pub fn state_diff(&self, prev: &Self) -> Vec<u8> { + let mut contents = vec![]; + self.write_contents_diff(&mut contents, prev); + self.write_input_mode_diff(&mut contents, prev); + contents + } + + /// Returns the formatted visible contents of the terminal. + /// + /// Formatting information will be included inline as terminal escape + /// codes. The result will be suitable for feeding directly to a raw + /// terminal parser, and will result in the same visual output. + #[must_use] + pub fn contents_formatted(&self) -> Vec<u8> { + let mut contents = vec![]; + self.write_contents_formatted(&mut contents); + contents + } + + fn write_contents_formatted(&self, contents: &mut Vec<u8>) { + crate::term::HideCursor::new(self.hide_cursor()).write_buf(contents); + let prev_attrs = self.grid().write_contents_formatted(contents); + self.attrs.write_escape_code_diff(contents, &prev_attrs); + } + + /// Returns the formatted visible contents of the terminal by row, + /// restricted to the given subset of columns. + /// + /// Formatting information will be included inline as terminal escape + /// codes. The result will be suitable for feeding directly to a raw + /// terminal parser, and will result in the same visual output. + /// + /// You are responsible for positioning the cursor before printing each + /// row, and the final cursor position after displaying each row is + /// unspecified. + // the unwraps in this method shouldn't be reachable + #[allow(clippy::missing_panics_doc)] + pub fn rows_formatted( + &self, + start: u16, + width: u16, + ) -> impl Iterator<Item = Vec<u8>> + '_ { + let mut wrapping = false; + self.grid().visible_rows().enumerate().map(move |(i, row)| { + // number of rows in a grid is stored in a u16 (see Size), so + // visible_rows can never return enough rows to overflow here + let i = i.try_into().unwrap(); + let mut contents = vec![]; + row.write_contents_formatted( + &mut contents, + start, + width, + i, + wrapping, + None, + None, + ); + if start == 0 && width == self.grid.size().cols { + wrapping = row.wrapped(); + } + contents + }) + } + + /// Returns a terminal byte stream sufficient to turn the visible contents + /// of the screen described by `prev` into the visible contents of the + /// screen described by `self`. + /// + /// The result of rendering `prev.contents_formatted()` followed by + /// `self.contents_diff(prev)` should be equivalent to the result of + /// rendering `self.contents_formatted()`. This is primarily useful when + /// you already have a terminal parser whose state is described by `prev`, + /// since the diff will likely require less memory and cause less + /// flickering than redrawing the entire screen contents. + #[must_use] + pub fn contents_diff(&self, prev: &Self) -> Vec<u8> { + let mut contents = vec![]; + self.write_contents_diff(&mut contents, prev); + contents + } + + fn write_contents_diff(&self, contents: &mut Vec<u8>, prev: &Self) { + if self.hide_cursor() != prev.hide_cursor() { + crate::term::HideCursor::new(self.hide_cursor()) + .write_buf(contents); + } + let prev_attrs = self.grid().write_contents_diff( + contents, + prev.grid(), + prev.attrs, + ); + self.attrs.write_escape_code_diff(contents, &prev_attrs); + } + + /// Returns a sequence of terminal byte streams sufficient to turn the + /// visible contents of the subset of each row from `prev` (as described + /// by `start` and `width`) into the visible contents of the corresponding + /// row subset in `self`. + /// + /// You are responsible for positioning the cursor before printing each + /// row, and the final cursor position after displaying each row is + /// unspecified. + // the unwraps in this method shouldn't be reachable + #[allow(clippy::missing_panics_doc)] + pub fn rows_diff<'a>( + &'a self, + prev: &'a Self, + start: u16, + width: u16, + ) -> impl Iterator<Item = Vec<u8>> + 'a { + self.grid() + .visible_rows() + .zip(prev.grid().visible_rows()) + .enumerate() + .map(move |(i, (row, prev_row))| { + // number of rows in a grid is stored in a u16 (see Size), so + // visible_rows can never return enough rows to overflow here + let i = i.try_into().unwrap(); + let mut contents = vec![]; + row.write_contents_diff( + &mut contents, + prev_row, + start, + width, + i, + false, + false, + crate::grid::Pos { row: i, col: start }, + crate::attrs::Attrs::default(), + ); + contents + }) + } + + /// Returns terminal escape sequences sufficient to set the current + /// terminal's input modes. + /// + /// Supported modes are: + /// * application keypad + /// * application cursor + /// * bracketed paste + /// * xterm mouse support + #[must_use] + pub fn input_mode_formatted(&self) -> Vec<u8> { + let mut contents = vec![]; + self.write_input_mode_formatted(&mut contents); + contents + } + + fn write_input_mode_formatted(&self, contents: &mut Vec<u8>) { + crate::term::ApplicationKeypad::new( + self.mode(MODE_APPLICATION_KEYPAD), + ) + .write_buf(contents); + crate::term::ApplicationCursor::new( + self.mode(MODE_APPLICATION_CURSOR), + ) + .write_buf(contents); + crate::term::BracketedPaste::new(self.mode(MODE_BRACKETED_PASTE)) + .write_buf(contents); + crate::term::MouseProtocolMode::new( + self.mouse_protocol_mode, + MouseProtocolMode::None, + ) + .write_buf(contents); + crate::term::MouseProtocolEncoding::new( + self.mouse_protocol_encoding, + MouseProtocolEncoding::Default, + ) + .write_buf(contents); + } + + /// Returns terminal escape sequences sufficient to change the previous + /// terminal's input modes to the input modes enabled in the current + /// terminal. + #[must_use] + pub fn input_mode_diff(&self, prev: &Self) -> Vec<u8> { + let mut contents = vec![]; + self.write_input_mode_diff(&mut contents, prev); + contents + } + + fn write_input_mode_diff(&self, contents: &mut Vec<u8>, prev: &Self) { + if self.mode(MODE_APPLICATION_KEYPAD) + != prev.mode(MODE_APPLICATION_KEYPAD) + { + crate::term::ApplicationKeypad::new( + self.mode(MODE_APPLICATION_KEYPAD), + ) + .write_buf(contents); + } + if self.mode(MODE_APPLICATION_CURSOR) + != prev.mode(MODE_APPLICATION_CURSOR) + { + crate::term::ApplicationCursor::new( + self.mode(MODE_APPLICATION_CURSOR), + ) + .write_buf(contents); + } + if self.mode(MODE_BRACKETED_PASTE) != prev.mode(MODE_BRACKETED_PASTE) + { + crate::term::BracketedPaste::new(self.mode(MODE_BRACKETED_PASTE)) + .write_buf(contents); + } + crate::term::MouseProtocolMode::new( + self.mouse_protocol_mode, + prev.mouse_protocol_mode, + ) + .write_buf(contents); + crate::term::MouseProtocolEncoding::new( + self.mouse_protocol_encoding, + prev.mouse_protocol_encoding, + ) + .write_buf(contents); + } + + /// Returns terminal escape sequences sufficient to set the current + /// terminal's drawing attributes. + /// + /// Supported drawing attributes are: + /// * fgcolor + /// * bgcolor + /// * bold + /// * dim + /// * italic + /// * underline + /// * inverse + /// + /// This is not typically necessary, since + /// [`contents_formatted`](Self::contents_formatted) will leave + /// the current active drawing attributes in the correct state, but this + /// can be useful in the case of drawing additional things on top of a + /// terminal output, since you will need to restore the terminal state + /// without the terminal contents necessarily being the same. + #[must_use] + pub fn attributes_formatted(&self) -> Vec<u8> { + let mut contents = vec![]; + self.write_attributes_formatted(&mut contents); + contents + } + + fn write_attributes_formatted(&self, contents: &mut Vec<u8>) { + crate::term::ClearAttrs.write_buf(contents); + self.attrs.write_escape_code_diff( + contents, + &crate::attrs::Attrs::default(), + ); + } + + /// Returns the current cursor position of the terminal. + /// + /// The return value will be (row, col). + #[must_use] + pub fn cursor_position(&self) -> (u16, u16) { + let pos = self.grid().pos(); + (pos.row, pos.col) + } + + /// Returns terminal escape sequences sufficient to set the current + /// cursor state of the terminal. + /// + /// This is not typically necessary, since + /// [`contents_formatted`](Self::contents_formatted) will leave + /// the cursor in the correct state, but this can be useful in the case of + /// drawing additional things on top of a terminal output, since you will + /// need to restore the terminal state without the terminal contents + /// necessarily being the same. + /// + /// Note that the bytes returned by this function may alter the active + /// drawing attributes, because it may require redrawing existing cells in + /// order to position the cursor correctly (for instance, in the case + /// where the cursor is past the end of a row). Therefore, you should + /// ensure to reset the active drawing attributes if necessary after + /// processing this data, for instance by using + /// [`attributes_formatted`](Self::attributes_formatted). + #[must_use] + pub fn cursor_state_formatted(&self) -> Vec<u8> { + let mut contents = vec![]; + self.write_cursor_state_formatted(&mut contents); + contents + } + + fn write_cursor_state_formatted(&self, contents: &mut Vec<u8>) { + crate::term::HideCursor::new(self.hide_cursor()).write_buf(contents); + self.grid() + .write_cursor_position_formatted(contents, None, None); + + // we don't just call write_attributes_formatted here, because that + // would still be confusing - consider the case where the user sets + // their own unrelated drawing attributes (on a different parser + // instance) and then calls cursor_state_formatted. just documenting + // it and letting the user handle it on their own is more + // straightforward. + } + + /// Returns the [`Cell`](crate::Cell) object at the given location in the + /// terminal, if it exists. + #[must_use] + pub fn cell(&self, row: u16, col: u16) -> Option<&crate::Cell> { + self.grid().visible_cell(crate::grid::Pos { row, col }) + } + + /// Returns whether the text in row `row` should wrap to the next line. + #[must_use] + pub fn row_wrapped(&self, row: u16) -> bool { + self.grid() + .visible_row(row) + .is_some_and(crate::row::Row::wrapped) + } + + /// Returns whether the alternate screen is currently in use. + #[must_use] + pub fn alternate_screen(&self) -> bool { + self.mode(MODE_ALTERNATE_SCREEN) + } + + /// Returns whether the terminal should be in application keypad mode. + #[must_use] + pub fn application_keypad(&self) -> bool { + self.mode(MODE_APPLICATION_KEYPAD) + } + + /// Returns whether the terminal should be in application cursor mode. + #[must_use] + pub fn application_cursor(&self) -> bool { + self.mode(MODE_APPLICATION_CURSOR) + } + + /// Returns whether the terminal should be in hide cursor mode. + #[must_use] + pub fn hide_cursor(&self) -> bool { + self.mode(MODE_HIDE_CURSOR) + } + + /// Returns whether the terminal should be in bracketed paste mode. + #[must_use] + pub fn bracketed_paste(&self) -> bool { + self.mode(MODE_BRACKETED_PASTE) + } + + /// Returns the currently active [`MouseProtocolMode`]. + #[must_use] + pub fn mouse_protocol_mode(&self) -> MouseProtocolMode { + self.mouse_protocol_mode + } + + /// Returns the currently active [`MouseProtocolEncoding`]. + #[must_use] + pub fn mouse_protocol_encoding(&self) -> MouseProtocolEncoding { + self.mouse_protocol_encoding + } + + /// Returns the currently active foreground color. + #[must_use] + pub fn fgcolor(&self) -> crate::Color { + self.attrs.fgcolor + } + + /// Returns the currently active background color. + #[must_use] + pub fn bgcolor(&self) -> crate::Color { + self.attrs.bgcolor + } + + /// Returns whether newly drawn text should be rendered with the bold text + /// attribute. + #[must_use] + pub fn bold(&self) -> bool { + self.attrs.bold() + } + + /// Returns whether newly drawn text should be rendered with the dim text + /// attribute. + #[must_use] + pub fn dim(&self) -> bool { + self.attrs.dim() + } + + /// Returns whether newly drawn text should be rendered with the italic + /// text attribute. + #[must_use] + pub fn italic(&self) -> bool { + self.attrs.italic() + } + + /// Returns whether newly drawn text should be rendered with the + /// underlined text attribute. + #[must_use] + pub fn underline(&self) -> bool { + self.attrs.underline() + } + + /// Returns whether newly drawn text should be rendered with the inverse + /// text attribute. + #[must_use] + pub fn inverse(&self) -> bool { + self.attrs.inverse() + } + + pub(crate) fn grid(&self) -> &crate::grid::Grid { + if self.mode(MODE_ALTERNATE_SCREEN) { + &self.alternate_grid + } else { + &self.grid + } + } + + fn grid_mut(&mut self) -> &mut crate::grid::Grid { + if self.mode(MODE_ALTERNATE_SCREEN) { + &mut self.alternate_grid + } else { + &mut self.grid + } + } + + fn enter_alternate_grid(&mut self) { + self.grid_mut().set_scrollback(0); + self.set_mode(MODE_ALTERNATE_SCREEN); + self.alternate_grid.allocate_rows(); + self.grid_mut().set_scrollback(0); + } + + fn exit_alternate_grid(&mut self) { + self.clear_mode(MODE_ALTERNATE_SCREEN); + } + + fn save_cursor(&mut self) { + self.grid_mut().save_cursor(); + self.saved_attrs = self.attrs; + } + + fn restore_cursor(&mut self) { + self.grid_mut().restore_cursor(); + self.attrs = self.saved_attrs; + } + + fn set_mode(&mut self, mode: u8) { + self.modes |= mode; + } + + fn clear_mode(&mut self, mode: u8) { + self.modes &= !mode; + } + + fn mode(&self, mode: u8) -> bool { + self.modes & mode != 0 + } + + fn set_mouse_mode(&mut self, mode: MouseProtocolMode) { + self.mouse_protocol_mode = mode; + } + + fn clear_mouse_mode(&mut self, mode: MouseProtocolMode) { + if self.mouse_protocol_mode == mode { + self.mouse_protocol_mode = MouseProtocolMode::default(); + } + } + + fn set_mouse_encoding(&mut self, encoding: MouseProtocolEncoding) { + self.mouse_protocol_encoding = encoding; + } + + fn clear_mouse_encoding(&mut self, encoding: MouseProtocolEncoding) { + if self.mouse_protocol_encoding == encoding { + self.mouse_protocol_encoding = MouseProtocolEncoding::default(); + } + } +} + +impl Screen { + pub(crate) fn text(&mut self, c: char) { + let pos = self.grid().pos(); + let size = self.grid().size(); + let attrs = self.attrs; + + let width = c.width(); + if width.is_none() && (u32::from(c)) < 256 { + // don't even try to draw control characters + return; + } + let width = width + .unwrap_or(1) + .try_into() + // width() can only return 0, 1, or 2 + .unwrap(); + + // it doesn't make any sense to wrap if the last column in a row + // didn't already have contents. don't try to handle the case where a + // character wraps because there was only one column left in the + // previous row - literally everything handles this case differently, + // and this is tmux behavior (and also the simplest). i'm open to + // reconsidering this behavior, but only with a really good reason + // (xterm handles this by introducing the concept of triple width + // cells, which i really don't want to do). + let mut wrap = false; + if pos.col > size.cols - width { + let last_cell = self + .grid() + .drawing_cell(crate::grid::Pos { + row: pos.row, + col: size.cols - 1, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a valid + // row value. size.cols - 1 is also always a valid column. + .unwrap(); + if last_cell.has_contents() || last_cell.is_wide_continuation() { + wrap = true; + } + } + self.grid_mut().col_wrap(width, wrap); + let pos = self.grid().pos(); + + if width == 0 { + if pos.col > 0 { + let mut prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col - 1, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. pos.col - 1 is valid because we just + // checked for pos.col > 0. + .unwrap(); + if prev_cell.is_wide_continuation() { + prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col - 2, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. we know pos.col - 2 is valid + // because the cell at pos.col - 1 is a wide + // continuation character, which means there must be + // the first half of the wide character before it. + .unwrap(); + } + prev_cell.append(c); + } else if pos.row > 0 { + let prev_row = self + .grid() + .drawing_row(pos.row - 1) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. pos.row - 1 is valid because we just + // checked for pos.row > 0. + .unwrap(); + if prev_row.wrapped() { + let mut prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row - 1, + col: size.cols - 1, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. pos.row - 1 is valid because we + // just checked for pos.row > 0. col of size.cols - 1 + // is always valid. + .unwrap(); + if prev_cell.is_wide_continuation() { + prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row - 1, + col: size.cols - 2, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always + // have a valid row value. pos.row - 1 is valid + // because we just checked for pos.row > 0. col of + // size.cols - 2 is valid because the cell at + // size.cols - 1 is a wide continuation character, + // so it must have the first half of the wide + // character before it. + .unwrap(); + } + prev_cell.append(c); + } + } + } else { + if self + .grid() + .drawing_cell(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because we + // called col_wrap() immediately before this, which ensures + // that self.grid().pos().col has a valid value. + .unwrap() + .is_wide_continuation() + { + let prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col - 1, + }) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() immediately before this, which + // ensures that self.grid().pos().col has a valid value. + // pos.col - 1 is valid because the cell at pos.col is a + // wide continuation character, so it must have the first + // half of the wide character before it. + .unwrap(); + prev_cell.clear(attrs); + } + + if self + .grid() + .drawing_cell(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because we + // called col_wrap() immediately before this, which ensures + // that self.grid().pos().col has a valid value. + .unwrap() + .is_wide() + { + let next_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col + 1, + }) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() immediately before this, which + // ensures that self.grid().pos().col has a valid value. + // pos.col + 1 is valid because the cell at pos.col is a + // wide character, so it must have the second half of the + // wide character after it. + .unwrap(); + next_cell.set(' ', attrs); + } + + let cell = self + .grid_mut() + .drawing_cell_mut(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because we + // called col_wrap() immediately before this, which ensures + // that self.grid().pos().col has a valid value. + .unwrap(); + cell.set(c, attrs); + self.grid_mut().col_inc(1); + if width > 1 { + let pos = self.grid().pos(); + if self + .grid() + .drawing_cell(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() earlier, which ensures that + // self.grid().pos().col has a valid value. this is true + // even though we just called col_inc, because this branch + // only happens if width > 1, and col_wrap takes width + // into account. + .unwrap() + .is_wide() + { + let next_next_pos = crate::grid::Pos { + row: pos.row, + col: pos.col + 1, + }; + let next_next_cell = self + .grid_mut() + .drawing_cell_mut(next_next_pos) + // pos.row is valid because we assume + // self.grid().pos() to always have a valid row value. + // pos.col is valid because we called col_wrap() + // earlier, which ensures that self.grid().pos().col + // has a valid value. this is true even though we just + // called col_inc, because this branch only happens if + // width > 1, and col_wrap takes width into account. + // pos.col + 1 is valid because the cell at pos.col is + // wide, and so it must have the second half of the + // wide character after it. + .unwrap(); + next_next_cell.clear(attrs); + if next_next_pos.col == size.cols - 1 { + self.grid_mut() + .drawing_row_mut(pos.row) + // we assume self.grid().pos().row is always valid + .unwrap() + .wrap(false); + } + } + let next_cell = self + .grid_mut() + .drawing_cell_mut(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() earlier, which ensures that + // self.grid().pos().col has a valid value. this is true + // even though we just called col_inc, because this branch + // only happens if width > 1, and col_wrap takes width + // into account. + .unwrap(); + next_cell.clear(crate::attrs::Attrs::default()); + next_cell.set_wide_continuation(true); + self.grid_mut().col_inc(1); + } + } + } + + // control codes + + pub(crate) fn bs(&mut self) { + self.grid_mut().col_dec(1); + } + + pub(crate) fn tab(&mut self) { + self.grid_mut().col_tab(); + } + + pub(crate) fn lf(&mut self) { + self.grid_mut().row_inc_scroll(1); + } + + pub(crate) fn vt(&mut self) { + self.lf(); + } + + pub(crate) fn ff(&mut self) { + self.lf(); + } + + pub(crate) fn cr(&mut self) { + self.grid_mut().col_set(0); + } + + // escape codes + + // ESC 7 + pub(crate) fn decsc(&mut self) { + self.save_cursor(); + } + + // ESC 8 + pub(crate) fn decrc(&mut self) { + self.restore_cursor(); + } + + // ESC = + pub(crate) fn deckpam(&mut self) { + self.set_mode(MODE_APPLICATION_KEYPAD); + } + + // ESC > + pub(crate) fn deckpnm(&mut self) { + self.clear_mode(MODE_APPLICATION_KEYPAD); + } + + // ESC M + pub(crate) fn ri(&mut self) { + self.grid_mut().row_dec_scroll(1); + } + + // ESC c + pub(crate) fn ris(&mut self) { + *self = Self::new(self.grid.size(), self.grid.scrollback_len()); + } + + // csi codes + + // CSI @ + pub(crate) fn ich(&mut self, count: u16) { + self.grid_mut().insert_cells(count); + } + + // CSI A + pub(crate) fn cuu(&mut self, offset: u16) { + self.grid_mut().row_dec_clamp(offset); + } + + // CSI B + pub(crate) fn cud(&mut self, offset: u16) { + self.grid_mut().row_inc_clamp(offset); + } + + // CSI C + pub(crate) fn cuf(&mut self, offset: u16) { + self.grid_mut().col_inc_clamp(offset); + } + + // CSI D + pub(crate) fn cub(&mut self, offset: u16) { + self.grid_mut().col_dec(offset); + } + + // CSI E + pub(crate) fn cnl(&mut self, offset: u16) { + self.grid_mut().col_set(0); + self.grid_mut().row_inc_clamp(offset); + } + + // CSI F + pub(crate) fn cpl(&mut self, offset: u16) { + self.grid_mut().col_set(0); + self.grid_mut().row_dec_clamp(offset); + } + + // CSI G + pub(crate) fn cha(&mut self, col: u16) { + self.grid_mut().col_set(col - 1); + } + + // CSI H + pub(crate) fn cup(&mut self, (row, col): (u16, u16)) { + self.grid_mut().set_pos(crate::grid::Pos { + row: row - 1, + col: col - 1, + }); + } + + // CSI J + pub(crate) fn ed( + &mut self, + mode: u16, + mut unhandled: impl FnMut(&mut Self), + ) { + let attrs = self.attrs; + match mode { + 0 => self.grid_mut().erase_all_forward(attrs), + 1 => self.grid_mut().erase_all_backward(attrs), + 2 => self.grid_mut().erase_all(attrs), + _ => unhandled(self), + } + } + + // CSI ? J + pub(crate) fn decsed( + &mut self, + mode: u16, + unhandled: impl FnMut(&mut Self), + ) { + self.ed(mode, unhandled); + } + + // CSI K + pub(crate) fn el( + &mut self, + mode: u16, + mut unhandled: impl FnMut(&mut Self), + ) { + let attrs = self.attrs; + match mode { + 0 => self.grid_mut().erase_row_forward(attrs), + 1 => self.grid_mut().erase_row_backward(attrs), + 2 => self.grid_mut().erase_row(attrs), + _ => unhandled(self), + } + } + + // CSI ? K + pub(crate) fn decsel( + &mut self, + mode: u16, + unhandled: impl FnMut(&mut Self), + ) { + self.el(mode, unhandled); + } + + // CSI L + pub(crate) fn il(&mut self, count: u16) { + self.grid_mut().insert_lines(count); + } + + // CSI M + pub(crate) fn dl(&mut self, count: u16) { + self.grid_mut().delete_lines(count); + } + + // CSI P + pub(crate) fn dch(&mut self, count: u16) { + self.grid_mut().delete_cells(count); + } + + // CSI S + pub(crate) fn su(&mut self, count: u16) { + self.grid_mut().scroll_up(count); + } + + // CSI T + pub(crate) fn sd(&mut self, count: u16) { + self.grid_mut().scroll_down(count); + } + + // CSI X + pub(crate) fn ech(&mut self, count: u16) { + let attrs = self.attrs; + self.grid_mut().erase_cells(count, attrs); + } + + // CSI d + pub(crate) fn vpa(&mut self, row: u16) { + self.grid_mut().row_set(row - 1); + } + + // CSI ? h + pub(crate) fn decset( + &mut self, + params: &vte::Params, + mut unhandled: impl FnMut(&mut Self), + ) { + for param in params { + match param { + [1] => self.set_mode(MODE_APPLICATION_CURSOR), + [6] => self.grid_mut().set_origin_mode(true), + [9] => self.set_mouse_mode(MouseProtocolMode::Press), + [25] => self.clear_mode(MODE_HIDE_CURSOR), + [47] => self.enter_alternate_grid(), + [1000] => { + self.set_mouse_mode(MouseProtocolMode::PressRelease); + } + [1002] => { + self.set_mouse_mode(MouseProtocolMode::ButtonMotion); + } + [1003] => self.set_mouse_mode(MouseProtocolMode::AnyMotion), + [1005] => { + self.set_mouse_encoding(MouseProtocolEncoding::Utf8); + } + [1006] => { + self.set_mouse_encoding(MouseProtocolEncoding::Sgr); + } + [1049] => { + self.decsc(); + self.alternate_grid.clear(); + self.alternate_grid.clear_scrollback(); + self.enter_alternate_grid(); + } + [2004] => self.set_mode(MODE_BRACKETED_PASTE), + _ => unhandled(self), + } + } + } + + // CSI ? l + pub(crate) fn decrst( + &mut self, + params: &vte::Params, + mut unhandled: impl FnMut(&mut Self), + ) { + for param in params { + match param { + [1] => self.clear_mode(MODE_APPLICATION_CURSOR), + [6] => self.grid_mut().set_origin_mode(false), + [9] => self.clear_mouse_mode(MouseProtocolMode::Press), + [25] => self.set_mode(MODE_HIDE_CURSOR), + [47] => { + self.exit_alternate_grid(); + } + [1000] => { + self.clear_mouse_mode(MouseProtocolMode::PressRelease); + } + [1002] => { + self.clear_mouse_mode(MouseProtocolMode::ButtonMotion); + } + [1003] => { + self.clear_mouse_mode(MouseProtocolMode::AnyMotion); + } + [1005] => { + self.clear_mouse_encoding(MouseProtocolEncoding::Utf8); + } + [1006] => { + self.clear_mouse_encoding(MouseProtocolEncoding::Sgr); + } + [1049] => { + self.exit_alternate_grid(); + self.decrc(); + } + [2004] => self.clear_mode(MODE_BRACKETED_PASTE), + _ => unhandled(self), + } + } + } + + // CSI m + pub(crate) fn sgr( + &mut self, + params: &vte::Params, + mut unhandled: impl FnMut(&mut Self), + ) { + // XXX really i want to just be able to pass in a default Params + // instance with a 0 in it, but vte doesn't allow creating new Params + // instances + if params.is_empty() { + self.attrs = crate::attrs::Attrs::default(); + return; + } + + let mut iter = params.iter(); + + macro_rules! next_param { + () => { + match iter.next() { + Some(n) => n, + _ => return, + } + }; + } + + macro_rules! to_u8 { + ($n:expr) => { + if let Some(n) = u16_to_u8($n) { + n + } else { + return; + } + }; + } + + macro_rules! next_param_u8 { + () => { + if let &[n] = next_param!() { + to_u8!(n) + } else { + return; + } + }; + } + + loop { + match next_param!() { + [0] => self.attrs = crate::attrs::Attrs::default(), + [1] => self.attrs.set_bold(), + [2] => self.attrs.set_dim(), + [3] => self.attrs.set_italic(true), + [4] => self.attrs.set_underline(true), + [7] => self.attrs.set_inverse(true), + [22] => self.attrs.set_normal_intensity(), + [23] => self.attrs.set_italic(false), + [24] => self.attrs.set_underline(false), + [27] => self.attrs.set_inverse(false), + [n] if (30..=37).contains(n) => { + self.attrs.fgcolor = crate::Color::Idx(to_u8!(*n) - 30); + } + [38, 2, r, g, b] => { + self.attrs.fgcolor = + crate::Color::Rgb(to_u8!(*r), to_u8!(*g), to_u8!(*b)); + } + [38, 5, i] => { + self.attrs.fgcolor = crate::Color::Idx(to_u8!(*i)); + } + [38] => match next_param!() { + [2] => { + let r = next_param_u8!(); + let g = next_param_u8!(); + let b = next_param_u8!(); + self.attrs.fgcolor = crate::Color::Rgb(r, g, b); + } + [5] => { + self.attrs.fgcolor = + crate::Color::Idx(next_param_u8!()); + } + _ => { + unhandled(self); + return; + } + }, + [39] => { + self.attrs.fgcolor = crate::Color::Default; + } + [n] if (40..=47).contains(n) => { + self.attrs.bgcolor = crate::Color::Idx(to_u8!(*n) - 40); + } + [48, 2, r, g, b] => { + self.attrs.bgcolor = + crate::Color::Rgb(to_u8!(*r), to_u8!(*g), to_u8!(*b)); + } + [48, 5, i] => { + self.attrs.bgcolor = crate::Color::Idx(to_u8!(*i)); + } + [48] => match next_param!() { + [2] => { + let r = next_param_u8!(); + let g = next_param_u8!(); + let b = next_param_u8!(); + self.attrs.bgcolor = crate::Color::Rgb(r, g, b); + } + [5] => { + self.attrs.bgcolor = + crate::Color::Idx(next_param_u8!()); + } + _ => { + unhandled(self); + return; + } + }, + [49] => { + self.attrs.bgcolor = crate::Color::Default; + } + [n] if (90..=97).contains(n) => { + self.attrs.fgcolor = crate::Color::Idx(to_u8!(*n) - 82); + } + [n] if (100..=107).contains(n) => { + self.attrs.bgcolor = crate::Color::Idx(to_u8!(*n) - 92); + } + _ => unhandled(self), + } + } + } + + // CSI r + pub(crate) fn decstbm(&mut self, (top, bottom): (u16, u16)) { + self.grid_mut().set_scroll_region(top - 1, bottom - 1); + } +} + +fn u16_to_u8(i: u16) -> Option<u8> { + if i > u16::from(u8::MAX) { + None + } else { + // safe because we just ensured that the value fits in a u8 + Some(i.try_into().unwrap()) + } +} diff --git a/vendor/vt100/src/term.rs b/vendor/vt100/src/term.rs new file mode 100644 index 00000000..b6b898c6 --- /dev/null +++ b/vendor/vt100/src/term.rs @@ -0,0 +1,551 @@ +// TODO: read all of this from terminfo + +pub trait BufWrite { + fn write_buf(&self, buf: &mut Vec<u8>); +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ClearScreen; + +impl BufWrite for ClearScreen { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\x1b[H\x1b[J"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ClearRowForward; + +impl BufWrite for ClearRowForward { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\x1b[K"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct Crlf; + +impl BufWrite for Crlf { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\r\n"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct Backspace; + +impl BufWrite for Backspace { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\x08"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct SaveCursor; + +impl BufWrite for SaveCursor { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\x1b7"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct RestoreCursor; + +impl BufWrite for RestoreCursor { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\x1b8"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MoveTo { + row: u16, + col: u16, +} + +impl MoveTo { + pub fn new(pos: crate::grid::Pos) -> Self { + Self { + row: pos.row, + col: pos.col, + } + } +} + +impl BufWrite for MoveTo { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.row == 0 && self.col == 0 { + buf.extend_from_slice(b"\x1b[H"); + } else { + buf.extend_from_slice(b"\x1b["); + extend_itoa(buf, self.row + 1); + buf.push(b';'); + extend_itoa(buf, self.col + 1); + buf.push(b'H'); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ClearAttrs; + +impl BufWrite for ClearAttrs { + fn write_buf(&self, buf: &mut Vec<u8>) { + buf.extend_from_slice(b"\x1b[m"); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Intensity { + Normal, + Bold, + Dim, +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct Attrs { + fgcolor: Option<crate::Color>, + bgcolor: Option<crate::Color>, + intensity: Option<Intensity>, + italic: Option<bool>, + underline: Option<bool>, + inverse: Option<bool>, +} + +impl Attrs { + pub fn fgcolor(mut self, fgcolor: crate::Color) -> Self { + self.fgcolor = Some(fgcolor); + self + } + + pub fn bgcolor(mut self, bgcolor: crate::Color) -> Self { + self.bgcolor = Some(bgcolor); + self + } + + pub fn intensity(mut self, intensity: Intensity) -> Self { + self.intensity = Some(intensity); + self + } + + pub fn italic(mut self, italic: bool) -> Self { + self.italic = Some(italic); + self + } + + pub fn underline(mut self, underline: bool) -> Self { + self.underline = Some(underline); + self + } + + pub fn inverse(mut self, inverse: bool) -> Self { + self.inverse = Some(inverse); + self + } +} + +impl BufWrite for Attrs { + #[allow(unused_assignments)] + #[allow(clippy::branches_sharing_code)] + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.fgcolor.is_none() + && self.bgcolor.is_none() + && self.intensity.is_none() + && self.italic.is_none() + && self.underline.is_none() + && self.inverse.is_none() + { + return; + } + + buf.extend_from_slice(b"\x1b["); + let mut first = true; + + macro_rules! write_param { + ($i:expr) => {{ + if first { + first = false; + } else { + buf.push(b';'); + } + extend_itoa(buf, $i); + }}; + } + + if let Some(fgcolor) = self.fgcolor { + match fgcolor { + crate::Color::Default => { + write_param!(39); + } + crate::Color::Idx(i) => { + if i < 8 { + write_param!(i + 30); + } else if i < 16 { + write_param!(i + 82); + } else { + write_param!(38); + write_param!(5); + write_param!(i); + } + } + crate::Color::Rgb(r, g, b) => { + write_param!(38); + write_param!(2); + write_param!(r); + write_param!(g); + write_param!(b); + } + } + } + + if let Some(bgcolor) = self.bgcolor { + match bgcolor { + crate::Color::Default => { + write_param!(49); + } + crate::Color::Idx(i) => { + if i < 8 { + write_param!(i + 40); + } else if i < 16 { + write_param!(i + 92); + } else { + write_param!(48); + write_param!(5); + write_param!(i); + } + } + crate::Color::Rgb(r, g, b) => { + write_param!(48); + write_param!(2); + write_param!(r); + write_param!(g); + write_param!(b); + } + } + } + + if let Some(intensity) = self.intensity { + match intensity { + Intensity::Normal => write_param!(22), + Intensity::Bold => write_param!(1), + Intensity::Dim => write_param!(2), + } + } + + if let Some(italic) = self.italic { + if italic { + write_param!(3); + } else { + write_param!(23); + } + } + + if let Some(underline) = self.underline { + if underline { + write_param!(4); + } else { + write_param!(24); + } + } + + if let Some(inverse) = self.inverse { + if inverse { + write_param!(7); + } else { + write_param!(27); + } + } + + buf.push(b'm'); + } +} + +#[derive(Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MoveRight { + count: u16, +} + +impl MoveRight { + pub fn new(count: u16) -> Self { + Self { count } + } +} + +impl Default for MoveRight { + fn default() -> Self { + Self { count: 1 } + } +} + +impl BufWrite for MoveRight { + fn write_buf(&self, buf: &mut Vec<u8>) { + match self.count { + 0 => {} + 1 => buf.extend_from_slice(b"\x1b[C"), + n => { + buf.extend_from_slice(b"\x1b["); + extend_itoa(buf, n); + buf.push(b'C'); + } + } + } +} + +#[derive(Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct EraseChar { + count: u16, +} + +impl EraseChar { + pub fn new(count: u16) -> Self { + Self { count } + } +} + +impl Default for EraseChar { + fn default() -> Self { + Self { count: 1 } + } +} + +impl BufWrite for EraseChar { + fn write_buf(&self, buf: &mut Vec<u8>) { + match self.count { + 0 => {} + 1 => buf.extend_from_slice(b"\x1b[X"), + n => { + buf.extend_from_slice(b"\x1b["); + extend_itoa(buf, n); + buf.push(b'X'); + } + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct HideCursor { + state: bool, +} + +impl HideCursor { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for HideCursor { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.state { + buf.extend_from_slice(b"\x1b[?25l"); + } else { + buf.extend_from_slice(b"\x1b[?25h"); + } + } +} + +#[derive(Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MoveFromTo { + from: crate::grid::Pos, + to: crate::grid::Pos, +} + +impl MoveFromTo { + pub fn new(from: crate::grid::Pos, to: crate::grid::Pos) -> Self { + Self { from, to } + } +} + +impl BufWrite for MoveFromTo { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.to.row == self.from.row + 1 && self.to.col == 0 { + crate::term::Crlf.write_buf(buf); + } else if self.from.row == self.to.row && self.from.col < self.to.col + { + crate::term::MoveRight::new(self.to.col - self.from.col) + .write_buf(buf); + } else if self.to != self.from { + crate::term::MoveTo::new(self.to).write_buf(buf); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ApplicationKeypad { + state: bool, +} + +impl ApplicationKeypad { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for ApplicationKeypad { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.state { + buf.extend_from_slice(b"\x1b="); + } else { + buf.extend_from_slice(b"\x1b>"); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ApplicationCursor { + state: bool, +} + +impl ApplicationCursor { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for ApplicationCursor { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.state { + buf.extend_from_slice(b"\x1b[?1h"); + } else { + buf.extend_from_slice(b"\x1b[?1l"); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct BracketedPaste { + state: bool, +} + +impl BracketedPaste { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for BracketedPaste { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.state { + buf.extend_from_slice(b"\x1b[?2004h"); + } else { + buf.extend_from_slice(b"\x1b[?2004l"); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MouseProtocolMode { + mode: crate::MouseProtocolMode, + prev: crate::MouseProtocolMode, +} + +impl MouseProtocolMode { + pub fn new( + mode: crate::MouseProtocolMode, + prev: crate::MouseProtocolMode, + ) -> Self { + Self { mode, prev } + } +} + +impl BufWrite for MouseProtocolMode { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.mode == self.prev { + return; + } + + match self.mode { + crate::MouseProtocolMode::None => match self.prev { + crate::MouseProtocolMode::None => {} + crate::MouseProtocolMode::Press => { + buf.extend_from_slice(b"\x1b[?9l"); + } + crate::MouseProtocolMode::PressRelease => { + buf.extend_from_slice(b"\x1b[?1000l"); + } + crate::MouseProtocolMode::ButtonMotion => { + buf.extend_from_slice(b"\x1b[?1002l"); + } + crate::MouseProtocolMode::AnyMotion => { + buf.extend_from_slice(b"\x1b[?1003l"); + } + }, + crate::MouseProtocolMode::Press => { + buf.extend_from_slice(b"\x1b[?9h"); + } + crate::MouseProtocolMode::PressRelease => { + buf.extend_from_slice(b"\x1b[?1000h"); + } + crate::MouseProtocolMode::ButtonMotion => { + buf.extend_from_slice(b"\x1b[?1002h"); + } + crate::MouseProtocolMode::AnyMotion => { + buf.extend_from_slice(b"\x1b[?1003h"); + } + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MouseProtocolEncoding { + encoding: crate::MouseProtocolEncoding, + prev: crate::MouseProtocolEncoding, +} + +impl MouseProtocolEncoding { + pub fn new( + encoding: crate::MouseProtocolEncoding, + prev: crate::MouseProtocolEncoding, + ) -> Self { + Self { encoding, prev } + } +} + +impl BufWrite for MouseProtocolEncoding { + fn write_buf(&self, buf: &mut Vec<u8>) { + if self.encoding == self.prev { + return; + } + + match self.encoding { + crate::MouseProtocolEncoding::Default => match self.prev { + crate::MouseProtocolEncoding::Default => {} + crate::MouseProtocolEncoding::Utf8 => { + buf.extend_from_slice(b"\x1b[?1005l"); + } + crate::MouseProtocolEncoding::Sgr => { + buf.extend_from_slice(b"\x1b[?1006l"); + } + }, + crate::MouseProtocolEncoding::Utf8 => { + buf.extend_from_slice(b"\x1b[?1005h"); + } + crate::MouseProtocolEncoding::Sgr => { + buf.extend_from_slice(b"\x1b[?1006h"); + } + } + } +} + +fn extend_itoa<I: itoa::Integer>(buf: &mut Vec<u8>, i: I) { + let mut itoa_buf = itoa::Buffer::new(); + buf.extend_from_slice(itoa_buf.format(i).as_bytes()); +}