fix alt-screen scrollback handling
This commit is contained in:
parent
f826fb78dc
commit
41e009f9b7
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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<usize> {
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -397,6 +397,7 @@ pub(crate) enum DragTarget {
|
|||
},
|
||||
PaneScrollbar {
|
||||
pane_id: crate::layout::PaneId,
|
||||
grab_row_offset: u16,
|
||||
},
|
||||
SidebarDivider,
|
||||
}
|
||||
|
|
|
|||
56
src/pane.rs
56
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<PtyResponses>) -> usize {
|
|||
max_scrollback
|
||||
}
|
||||
|
||||
fn recent_text_from_parser(parser: &mut vt100::Parser<PtyResponses>, lines: usize) -> String {
|
||||
fn parser_rows(parser: &mut vt100::Parser<PtyResponses>, lines: usize) -> Vec<String> {
|
||||
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<PtyResponses>, 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<PtyResponses>, lines: usiz
|
|||
}
|
||||
}
|
||||
|
||||
fn recent_text_from_parser(parser: &mut vt100::Parser<PtyResponses>, 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<ScrollMetrics> {
|
||||
pub fn scroll_state(&self) -> Option<ScrollState> {
|
||||
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<ScrollMetrics> {
|
||||
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();
|
||||
|
|
|
|||
271
src/ui.rs
271
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<PaneInfo> {
|
|||
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<PaneInfo> {
|
|||
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<Rect> {
|
|||
info.scrollbar_rect
|
||||
}
|
||||
|
||||
fn should_show_scrollbar(
|
||||
metrics: crate::pane::ScrollMetrics,
|
||||
alternate_screen: bool,
|
||||
detected_agent: Option<crate::detect::Agent>,
|
||||
) -> 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<ScrollbarThumb> {
|
||||
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<u16> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"v":1}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"git": {
|
||||
"sha1": "eb66ffaf7d771c13303ef73b29f6f2a56fdacecf"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
|
|
@ -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<u8>` 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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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 <doy@tozt.net>"]
|
||||
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"
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "vt100"
|
||||
version = "0.16.2"
|
||||
authors = ["Jesse Luehrs <doy@tozt.net>"]
|
||||
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"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"[..],
|
||||
);
|
||||
```
|
||||
|
|
@ -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<u8>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;<rows>;<cols>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;<icon_name>\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;<title>\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 () {}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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());
|
||||
}
|
||||
Loading…
Reference in New Issue