1254 lines
48 KiB
Rust
1254 lines
48 KiB
Rust
use std::{
|
||
cmp::Ordering,
|
||
collections::VecDeque,
|
||
fmt::{self, Debug, Formatter},
|
||
};
|
||
|
||
const TABSTOP_WIDTH: usize = 8; // TODO: is this always right?
|
||
const SCROLL_BACK: usize = 10_000;
|
||
|
||
use crate::utils::logging::debug_log_to_file;
|
||
|
||
use crate::panes::terminal_character::{
|
||
CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
|
||
};
|
||
|
||
fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> {
|
||
let mut index_of_last_non_canonical_row = None;
|
||
for (i, row) in rows.iter().enumerate() {
|
||
if row.is_canonical {
|
||
break;
|
||
} else {
|
||
index_of_last_non_canonical_row = Some(i);
|
||
}
|
||
}
|
||
match index_of_last_non_canonical_row {
|
||
Some(index_of_last_non_canonical_row) => {
|
||
rows.drain(..=index_of_last_non_canonical_row).collect()
|
||
}
|
||
None => vec![],
|
||
}
|
||
}
|
||
|
||
fn get_bottom_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> Vec<Row> {
|
||
let mut index_of_last_non_canonical_row = None;
|
||
for (i, row) in rows.iter().enumerate().rev() {
|
||
index_of_last_non_canonical_row = Some(i);
|
||
if row.is_canonical {
|
||
break;
|
||
}
|
||
}
|
||
match index_of_last_non_canonical_row {
|
||
Some(index_of_last_non_canonical_row) => {
|
||
rows.drain(index_of_last_non_canonical_row..).collect()
|
||
}
|
||
None => vec![],
|
||
}
|
||
}
|
||
|
||
fn transfer_rows_down(
|
||
source: &mut VecDeque<Row>,
|
||
destination: &mut Vec<Row>,
|
||
count: usize,
|
||
max_src_width: Option<usize>,
|
||
max_dst_width: Option<usize>,
|
||
) {
|
||
let mut next_lines: Vec<Row> = vec![];
|
||
let mut lines_added_to_destination: isize = 0;
|
||
loop {
|
||
if lines_added_to_destination as usize == count {
|
||
break;
|
||
}
|
||
if next_lines.is_empty() {
|
||
match source.pop_back() {
|
||
Some(next_line) => {
|
||
let mut top_non_canonical_rows_in_dst = get_top_non_canonical_rows(destination);
|
||
lines_added_to_destination -= top_non_canonical_rows_in_dst.len() as isize;
|
||
next_lines.push(next_line);
|
||
next_lines.append(&mut top_non_canonical_rows_in_dst);
|
||
next_lines = match max_dst_width {
|
||
Some(max_row_width) => {
|
||
Row::from_rows(next_lines).split_to_rows_of_length(max_row_width)
|
||
}
|
||
None => vec![Row::from_rows(next_lines)],
|
||
};
|
||
if next_lines.is_empty() {
|
||
// no more lines at source, the line we popped was probably empty
|
||
break;
|
||
}
|
||
}
|
||
None => break, // no more rows
|
||
}
|
||
}
|
||
destination.insert(0, next_lines.pop().unwrap());
|
||
lines_added_to_destination += 1;
|
||
}
|
||
if !next_lines.is_empty() {
|
||
match max_src_width {
|
||
Some(max_row_width) => {
|
||
let excess_rows = Row::from_rows(next_lines).split_to_rows_of_length(max_row_width);
|
||
source.extend(excess_rows);
|
||
}
|
||
None => {
|
||
let excess_row = Row::from_rows(next_lines);
|
||
bounded_push(source, excess_row);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn transfer_rows_up(
|
||
source: &mut Vec<Row>,
|
||
destination: &mut VecDeque<Row>,
|
||
count: usize,
|
||
max_src_width: Option<usize>,
|
||
max_dst_width: Option<usize>,
|
||
) {
|
||
let mut next_lines: Vec<Row> = vec![];
|
||
for _ in 0..count {
|
||
if next_lines.is_empty() {
|
||
if !source.is_empty() {
|
||
let next_line = source.remove(0);
|
||
if !next_line.is_canonical {
|
||
let mut bottom_canonical_row_and_wraps_in_dst =
|
||
get_bottom_canonical_row_and_wraps(destination);
|
||
next_lines.append(&mut bottom_canonical_row_and_wraps_in_dst);
|
||
}
|
||
next_lines.push(next_line);
|
||
next_lines = match max_dst_width {
|
||
Some(max_row_width) => {
|
||
Row::from_rows(next_lines).split_to_rows_of_length(max_row_width)
|
||
}
|
||
None => vec![Row::from_rows(next_lines)],
|
||
};
|
||
} else {
|
||
break; // no more rows
|
||
}
|
||
}
|
||
bounded_push(destination, next_lines.remove(0));
|
||
}
|
||
if !next_lines.is_empty() {
|
||
match max_src_width {
|
||
Some(max_row_width) => {
|
||
let excess_rows = Row::from_rows(next_lines).split_to_rows_of_length(max_row_width);
|
||
for row in excess_rows {
|
||
source.insert(0, row);
|
||
}
|
||
}
|
||
None => {
|
||
let excess_row = Row::from_rows(next_lines);
|
||
source.insert(0, excess_row);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn bounded_push(vec: &mut VecDeque<Row>, value: Row) {
|
||
if vec.len() >= SCROLL_BACK {
|
||
vec.pop_front();
|
||
}
|
||
vec.push_back(value)
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct Grid {
|
||
lines_above: VecDeque<Row>,
|
||
viewport: Vec<Row>,
|
||
lines_below: Vec<Row>,
|
||
alternative_lines_above_viewport_and_cursor: Option<(VecDeque<Row>, Vec<Row>, Cursor)>,
|
||
cursor: Cursor,
|
||
scroll_region: Option<(usize, usize)>,
|
||
pending_styles: CharacterStyles,
|
||
pub should_render: bool,
|
||
pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "[D")
|
||
pub clear_viewport_before_rendering: bool,
|
||
pub width: usize,
|
||
pub height: usize,
|
||
}
|
||
|
||
impl Debug for Grid {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||
for (i, row) in self.viewport.iter().enumerate() {
|
||
if row.is_canonical {
|
||
writeln!(f, "{:?} (C): {:?}", i, row)?;
|
||
} else {
|
||
writeln!(f, "{:?} (W): {:?}", i, row)?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Grid {
|
||
pub fn new(rows: usize, columns: usize) -> Self {
|
||
Grid {
|
||
lines_above: VecDeque::with_capacity(SCROLL_BACK),
|
||
viewport: vec![Row::new().canonical()],
|
||
lines_below: vec![],
|
||
cursor: Cursor::new(0, 0),
|
||
scroll_region: None,
|
||
width: columns,
|
||
height: rows,
|
||
pending_styles: CharacterStyles::new(),
|
||
should_render: true,
|
||
cursor_key_mode: false,
|
||
alternative_lines_above_viewport_and_cursor: None,
|
||
clear_viewport_before_rendering: false,
|
||
}
|
||
}
|
||
pub fn advance_to_next_tabstop(&mut self, styles: CharacterStyles) {
|
||
let columns_until_next_tabstop = TABSTOP_WIDTH - (self.cursor.x % TABSTOP_WIDTH);
|
||
let columns_until_screen_end = self.width - self.cursor.x;
|
||
let columns_to_advance =
|
||
std::cmp::min(columns_until_next_tabstop, columns_until_screen_end);
|
||
let mut empty_character = EMPTY_TERMINAL_CHARACTER;
|
||
empty_character.styles = styles;
|
||
for _ in 0..columns_to_advance {
|
||
self.add_character(empty_character)
|
||
}
|
||
}
|
||
fn cursor_canonical_line_index(&self) -> usize {
|
||
let mut cursor_canonical_line_index = 0;
|
||
let mut canonical_lines_traversed = 0;
|
||
for (i, line) in self.viewport.iter().enumerate() {
|
||
if line.is_canonical {
|
||
cursor_canonical_line_index = canonical_lines_traversed;
|
||
canonical_lines_traversed += 1;
|
||
}
|
||
if i == self.cursor.y {
|
||
break;
|
||
}
|
||
}
|
||
cursor_canonical_line_index
|
||
}
|
||
// TODO: merge these two funtions
|
||
fn cursor_index_in_canonical_line(&self) -> usize {
|
||
let mut cursor_canonical_line_index = 0;
|
||
let mut cursor_index_in_canonical_line = 0;
|
||
for (i, line) in self.viewport.iter().enumerate() {
|
||
if line.is_canonical {
|
||
cursor_canonical_line_index = i;
|
||
}
|
||
if i == self.cursor.y {
|
||
let line_wrap_position_in_line = self.cursor.y - cursor_canonical_line_index;
|
||
cursor_index_in_canonical_line = line_wrap_position_in_line + self.cursor.x;
|
||
break;
|
||
}
|
||
}
|
||
cursor_index_in_canonical_line
|
||
}
|
||
fn canonical_line_y_coordinates(&self, canonical_line_index: usize) -> usize {
|
||
let mut canonical_lines_traversed = 0;
|
||
let mut y_coordinates = 0;
|
||
for (i, line) in self.viewport.iter().enumerate() {
|
||
if line.is_canonical {
|
||
canonical_lines_traversed += 1;
|
||
if canonical_lines_traversed == canonical_line_index + 1 {
|
||
y_coordinates = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
y_coordinates
|
||
}
|
||
pub fn scroll_up_one_line(&mut self) {
|
||
if !self.lines_above.is_empty() && self.viewport.len() == self.height {
|
||
let line_to_push_down = self.viewport.pop().unwrap();
|
||
self.lines_below.insert(0, line_to_push_down);
|
||
let line_to_insert_at_viewport_top = self.lines_above.pop_back().unwrap();
|
||
self.viewport.insert(0, line_to_insert_at_viewport_top);
|
||
}
|
||
}
|
||
pub fn scroll_down_one_line(&mut self) {
|
||
if !self.lines_below.is_empty() && self.viewport.len() == self.height {
|
||
let mut line_to_push_up = self.viewport.remove(0);
|
||
if line_to_push_up.is_canonical {
|
||
bounded_push(&mut self.lines_above, line_to_push_up);
|
||
} else {
|
||
let mut last_line_above = self.lines_above.pop_back().unwrap();
|
||
last_line_above.append(&mut line_to_push_up.columns);
|
||
bounded_push(&mut self.lines_above, last_line_above);
|
||
}
|
||
let line_to_insert_at_viewport_bottom = self.lines_below.remove(0);
|
||
self.viewport.push(line_to_insert_at_viewport_bottom);
|
||
}
|
||
}
|
||
pub fn change_size(&mut self, new_rows: usize, new_columns: usize) {
|
||
if new_columns != self.width {
|
||
let mut cursor_canonical_line_index = self.cursor_canonical_line_index();
|
||
let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line();
|
||
let mut viewport_canonical_lines = vec![];
|
||
for mut row in self.viewport.drain(..) {
|
||
if !row.is_canonical
|
||
&& viewport_canonical_lines.is_empty()
|
||
&& !self.lines_above.is_empty()
|
||
{
|
||
let mut first_line_above = self.lines_above.pop_back().unwrap();
|
||
first_line_above.append(&mut row.columns);
|
||
viewport_canonical_lines.push(first_line_above);
|
||
cursor_canonical_line_index += 1;
|
||
} else if row.is_canonical {
|
||
viewport_canonical_lines.push(row);
|
||
} else {
|
||
match viewport_canonical_lines.last_mut() {
|
||
Some(last_line) => {
|
||
last_line.append(&mut row.columns);
|
||
}
|
||
None => {
|
||
// the state is corrupted somehow
|
||
// this is a bug and I'm not yet sure why it happens
|
||
// usually it fixes itself and is a result of some race
|
||
// TODO: investigate why this happens and solve it
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let mut new_viewport_rows = vec![];
|
||
for mut canonical_line in viewport_canonical_lines {
|
||
let mut canonical_line_parts: Vec<Row> = vec![];
|
||
if canonical_line.columns.is_empty() {
|
||
canonical_line_parts.push(Row::new().canonical());
|
||
}
|
||
while !canonical_line.columns.is_empty() {
|
||
let next_wrap = if canonical_line.len() > new_columns {
|
||
canonical_line.columns.drain(..new_columns)
|
||
} else {
|
||
canonical_line.columns.drain(..)
|
||
};
|
||
let row = Row::from_columns(next_wrap.collect());
|
||
// if there are no more parts, this row is canonical as long as it originally
|
||
// was canonical (it might not have been for example if it's the first row in
|
||
// the viewport, and the actual canonical row is above it in the scrollback)
|
||
let row = if canonical_line_parts.is_empty() && canonical_line.is_canonical {
|
||
row.canonical()
|
||
} else {
|
||
row
|
||
};
|
||
canonical_line_parts.push(row);
|
||
}
|
||
new_viewport_rows.append(&mut canonical_line_parts);
|
||
}
|
||
self.viewport = new_viewport_rows;
|
||
|
||
let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index);
|
||
let new_cursor_x = (cursor_index_in_canonical_line / new_columns)
|
||
+ (cursor_index_in_canonical_line % new_columns);
|
||
let current_viewport_row_count = self.viewport.len();
|
||
match current_viewport_row_count.cmp(&self.height) {
|
||
Ordering::Less => {
|
||
let row_count_to_transfer = self.height - current_viewport_row_count;
|
||
transfer_rows_down(
|
||
&mut self.lines_above,
|
||
&mut self.viewport,
|
||
row_count_to_transfer,
|
||
None,
|
||
Some(new_columns),
|
||
);
|
||
let rows_pulled = self.viewport.len() - current_viewport_row_count;
|
||
new_cursor_y += rows_pulled;
|
||
}
|
||
Ordering::Greater => {
|
||
let row_count_to_transfer = current_viewport_row_count - self.height;
|
||
if row_count_to_transfer > new_cursor_y {
|
||
new_cursor_y = 0;
|
||
} else {
|
||
new_cursor_y -= row_count_to_transfer;
|
||
}
|
||
transfer_rows_up(
|
||
&mut self.viewport,
|
||
&mut self.lines_above,
|
||
row_count_to_transfer,
|
||
Some(new_columns),
|
||
None,
|
||
);
|
||
}
|
||
Ordering::Equal => {}
|
||
}
|
||
self.cursor.y = new_cursor_y;
|
||
self.cursor.x = new_cursor_x;
|
||
}
|
||
if new_rows != self.height {
|
||
let current_viewport_row_count = self.viewport.len();
|
||
match current_viewport_row_count.cmp(&new_rows) {
|
||
Ordering::Less => {
|
||
let row_count_to_transfer = new_rows - current_viewport_row_count;
|
||
transfer_rows_down(
|
||
&mut self.lines_above,
|
||
&mut self.viewport,
|
||
row_count_to_transfer,
|
||
None,
|
||
Some(new_columns),
|
||
);
|
||
let rows_pulled = self.viewport.len() - current_viewport_row_count;
|
||
self.cursor.y += rows_pulled;
|
||
}
|
||
Ordering::Greater => {
|
||
let row_count_to_transfer = current_viewport_row_count - new_rows;
|
||
if row_count_to_transfer > self.cursor.y {
|
||
self.cursor.y = 0;
|
||
} else {
|
||
self.cursor.y -= row_count_to_transfer;
|
||
}
|
||
transfer_rows_up(
|
||
&mut self.viewport,
|
||
&mut self.lines_above,
|
||
row_count_to_transfer,
|
||
Some(new_columns),
|
||
None,
|
||
);
|
||
}
|
||
Ordering::Equal => {}
|
||
}
|
||
}
|
||
self.height = new_rows;
|
||
self.width = new_columns;
|
||
if self.scroll_region.is_some() {
|
||
self.set_scroll_region_to_viewport_size();
|
||
}
|
||
}
|
||
pub fn as_character_lines(&self) -> Vec<Vec<TerminalCharacter>> {
|
||
let mut lines: Vec<Vec<TerminalCharacter>> = self
|
||
.viewport
|
||
.iter()
|
||
.map(|r| {
|
||
let mut line: Vec<TerminalCharacter> = r.columns.iter().copied().collect();
|
||
// pad line
|
||
line.resize(self.width, EMPTY_TERMINAL_CHARACTER);
|
||
line
|
||
})
|
||
.collect();
|
||
let empty_row = vec![EMPTY_TERMINAL_CHARACTER; self.width];
|
||
for _ in lines.len()..self.height {
|
||
lines.push(empty_row.clone());
|
||
}
|
||
lines
|
||
}
|
||
pub fn cursor_coordinates(&self) -> Option<(usize, usize)> {
|
||
if self.cursor.is_hidden {
|
||
None
|
||
} else {
|
||
Some((self.cursor.x, self.cursor.y))
|
||
}
|
||
}
|
||
pub fn move_viewport_up(&mut self, count: usize) {
|
||
for _ in 0..count {
|
||
self.scroll_up_one_line();
|
||
}
|
||
}
|
||
pub fn move_viewport_down(&mut self, count: usize) {
|
||
for _ in 0..count {
|
||
self.scroll_down_one_line();
|
||
}
|
||
}
|
||
pub fn reset_viewport(&mut self) {
|
||
let row_count_below = self.lines_below.len();
|
||
for _ in 0..row_count_below {
|
||
self.scroll_down_one_line();
|
||
}
|
||
}
|
||
pub fn rotate_scroll_region_up(&mut self, count: usize) {
|
||
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
|
||
for _ in 0..count {
|
||
let columns = vec![EMPTY_TERMINAL_CHARACTER; self.width];
|
||
if scroll_region_bottom < self.viewport.len() {
|
||
self.viewport.remove(scroll_region_bottom);
|
||
}
|
||
if scroll_region_top < self.viewport.len() {
|
||
self.viewport
|
||
.insert(scroll_region_top, Row::from_columns(columns).canonical());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pub fn rotate_scroll_region_down(&mut self, count: usize) {
|
||
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
|
||
for _ in 0..count {
|
||
let columns = vec![EMPTY_TERMINAL_CHARACTER; self.width];
|
||
self.viewport.remove(scroll_region_top);
|
||
if self.viewport.len() > scroll_region_top {
|
||
self.viewport
|
||
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
|
||
} else {
|
||
self.viewport.push(Row::from_columns(columns).canonical());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pub fn add_canonical_line(&mut self) {
|
||
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
|
||
if self.cursor.y == scroll_region_bottom {
|
||
// end of scroll region
|
||
// when we have a scroll region set and we're at its bottom
|
||
// we need to delete its first line, thus shifting all lines in it upwards
|
||
// then we add an empty line at its end which will be filled by the application
|
||
// controlling the scroll region (presumably filled by whatever comes next in the
|
||
// scroll buffer, but that's not something we control)
|
||
if scroll_region_top >= self.viewport.len() {
|
||
// the state is corrupted
|
||
return;
|
||
}
|
||
self.viewport.remove(scroll_region_top);
|
||
let columns = vec![EMPTY_TERMINAL_CHARACTER; self.width];
|
||
self.viewport
|
||
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
|
||
return;
|
||
}
|
||
}
|
||
if self.viewport.len() <= self.cursor.y + 1 {
|
||
// FIXME: this should add an empty line with the pad_character
|
||
// but for some reason this breaks rendering in various situations
|
||
// it needs to be investigated and fixed
|
||
let new_row = Row::new().canonical();
|
||
self.viewport.push(new_row);
|
||
}
|
||
if self.cursor.y == self.height - 1 {
|
||
let row_count_to_transfer = 1;
|
||
transfer_rows_up(
|
||
&mut self.viewport,
|
||
&mut self.lines_above,
|
||
row_count_to_transfer,
|
||
Some(self.width),
|
||
None,
|
||
);
|
||
} else {
|
||
self.cursor.y += 1;
|
||
}
|
||
}
|
||
pub fn move_cursor_to_beginning_of_line(&mut self) {
|
||
self.cursor.x = 0;
|
||
}
|
||
pub fn move_cursor_backwards(&mut self, count: usize) {
|
||
if self.cursor.x > count {
|
||
self.cursor.x -= count;
|
||
} else {
|
||
self.cursor.x = 0;
|
||
}
|
||
}
|
||
pub fn insert_character_at_cursor_position(&mut self, terminal_character: TerminalCharacter) {
|
||
match self.viewport.get_mut(self.cursor.y) {
|
||
Some(row) => {
|
||
row.insert_character_at(terminal_character, self.cursor.x);
|
||
if row.len() > self.width {
|
||
row.truncate(self.width);
|
||
}
|
||
}
|
||
None => {
|
||
// pad lines until cursor if they do not exist
|
||
for _ in self.viewport.len()..self.cursor.y {
|
||
self.viewport.push(Row::new().canonical());
|
||
}
|
||
self.viewport
|
||
.push(Row::new().with_character(terminal_character).canonical());
|
||
}
|
||
}
|
||
}
|
||
pub fn add_character_at_cursor_position(&mut self, terminal_character: TerminalCharacter) {
|
||
match self.viewport.get_mut(self.cursor.y) {
|
||
Some(row) => row.add_character_at(terminal_character, self.cursor.x),
|
||
None => {
|
||
// pad lines until cursor if they do not exist
|
||
for _ in self.viewport.len()..self.cursor.y {
|
||
self.viewport.push(Row::new().canonical());
|
||
}
|
||
self.viewport
|
||
.push(Row::new().with_character(terminal_character).canonical());
|
||
}
|
||
}
|
||
}
|
||
pub fn add_character(&mut self, terminal_character: TerminalCharacter) {
|
||
// TODO: try to separate adding characters from moving the cursors in this function
|
||
if self.cursor.x >= self.width {
|
||
// line wrap
|
||
self.cursor.x = 0;
|
||
if self.cursor.y == self.height - 1 {
|
||
let row_count_to_transfer = 1;
|
||
transfer_rows_up(
|
||
&mut self.viewport,
|
||
&mut self.lines_above,
|
||
row_count_to_transfer,
|
||
Some(self.width),
|
||
None,
|
||
);
|
||
let wrapped_row = Row::new();
|
||
self.viewport.push(wrapped_row);
|
||
} else {
|
||
self.cursor.y += 1;
|
||
if self.viewport.len() <= self.cursor.y {
|
||
let line_wrapped_row = Row::new();
|
||
self.viewport.push(line_wrapped_row);
|
||
}
|
||
}
|
||
}
|
||
self.add_character_at_cursor_position(terminal_character);
|
||
self.move_cursor_forward_until_edge(1);
|
||
}
|
||
pub fn move_cursor_forward_until_edge(&mut self, count: usize) {
|
||
let count_to_move = std::cmp::min(count, self.width - (self.cursor.x));
|
||
self.cursor.x += count_to_move;
|
||
}
|
||
pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) {
|
||
self.viewport
|
||
.get_mut(self.cursor.y)
|
||
.unwrap()
|
||
.truncate(self.cursor.x);
|
||
if self.cursor.x < self.width - 1 {
|
||
let mut characters_to_append = vec![replace_with; self.width - self.cursor.x];
|
||
self.viewport
|
||
.get_mut(self.cursor.y)
|
||
.unwrap()
|
||
.append(&mut characters_to_append);
|
||
}
|
||
}
|
||
pub fn replace_characters_in_line_before_cursor(&mut self, replace_with: TerminalCharacter) {
|
||
let line_part = vec![replace_with; self.cursor.x];
|
||
let row = self.viewport.get_mut(self.cursor.y).unwrap();
|
||
row.replace_beginning_with(line_part);
|
||
}
|
||
pub fn clear_all_after_cursor(&mut self, replace_with: TerminalCharacter) {
|
||
let cursor_row = self.viewport.get_mut(self.cursor.y).unwrap();
|
||
cursor_row.truncate(self.cursor.x);
|
||
let mut replace_with_columns_in_cursor_row = vec![replace_with; self.width - self.cursor.x];
|
||
cursor_row.append(&mut replace_with_columns_in_cursor_row);
|
||
|
||
let replace_with_columns = vec![replace_with; self.width];
|
||
self.replace_characters_in_line_after_cursor(replace_with);
|
||
for row in self.viewport.iter_mut().skip(self.cursor.y + 1) {
|
||
row.replace_columns(replace_with_columns.clone());
|
||
}
|
||
}
|
||
pub fn clear_cursor_line(&mut self) {
|
||
self.viewport.get_mut(self.cursor.y).unwrap().truncate(0);
|
||
}
|
||
pub fn clear_all(&mut self, replace_with: TerminalCharacter) {
|
||
let replace_with_columns = vec![replace_with; self.width];
|
||
self.replace_characters_in_line_after_cursor(replace_with);
|
||
for row in self.viewport.iter_mut() {
|
||
row.replace_columns(replace_with_columns.clone());
|
||
}
|
||
}
|
||
fn pad_current_line_until(&mut self, position: usize) {
|
||
let current_row = self.viewport.get_mut(self.cursor.y).unwrap();
|
||
for _ in current_row.len()..position {
|
||
current_row.push(EMPTY_TERMINAL_CHARACTER);
|
||
}
|
||
}
|
||
fn pad_lines_until(&mut self, position: usize, pad_character: TerminalCharacter) {
|
||
for _ in self.viewport.len()..=position {
|
||
let columns = vec![pad_character; self.width];
|
||
self.viewport.push(Row::from_columns(columns).canonical());
|
||
}
|
||
}
|
||
pub fn move_cursor_to(&mut self, x: usize, y: usize, pad_character: TerminalCharacter) {
|
||
self.cursor.x = std::cmp::min(self.width - 1, x);
|
||
self.cursor.y = std::cmp::min(self.height - 1, y);
|
||
self.pad_lines_until(self.cursor.y, pad_character);
|
||
self.pad_current_line_until(self.cursor.x);
|
||
}
|
||
pub fn move_cursor_up(&mut self, count: usize) {
|
||
self.cursor.y = if self.cursor.y < count {
|
||
0
|
||
} else {
|
||
self.cursor.y - count
|
||
};
|
||
}
|
||
pub fn move_cursor_up_with_scrolling(&mut self, count: usize) {
|
||
let (scroll_region_top, scroll_region_bottom) =
|
||
self.scroll_region.unwrap_or((0, self.height - 1));
|
||
for _ in 0..count {
|
||
let current_line_index = self.cursor.y;
|
||
if current_line_index == scroll_region_top {
|
||
// if we're at the top line, we create a new line and remove the last line that
|
||
// would otherwise overflow
|
||
if scroll_region_bottom < self.viewport.len() {
|
||
self.viewport.remove(scroll_region_bottom);
|
||
}
|
||
self.viewport.insert(current_line_index, Row::new()); // TODO: .canonical() ?
|
||
} else if current_line_index > scroll_region_top
|
||
&& current_line_index <= scroll_region_bottom
|
||
{
|
||
self.move_cursor_up(count);
|
||
}
|
||
}
|
||
}
|
||
pub fn move_cursor_down(&mut self, count: usize, pad_character: TerminalCharacter) {
|
||
let lines_to_add = if self.cursor.y + count > self.height - 1 {
|
||
(self.cursor.y + count) - (self.height - 1)
|
||
} else {
|
||
0
|
||
};
|
||
self.cursor.y = if self.cursor.y + count > self.height - 1 {
|
||
self.height - 1
|
||
} else {
|
||
self.cursor.y + count
|
||
};
|
||
for _ in 0..lines_to_add {
|
||
self.add_canonical_line();
|
||
}
|
||
self.pad_lines_until(self.cursor.y, pad_character);
|
||
}
|
||
pub fn move_cursor_back(&mut self, count: usize) {
|
||
if self.cursor.x < count {
|
||
self.cursor.x = 0;
|
||
} else {
|
||
self.cursor.x -= count;
|
||
}
|
||
}
|
||
pub fn hide_cursor(&mut self) {
|
||
self.cursor.is_hidden = true;
|
||
}
|
||
pub fn show_cursor(&mut self) {
|
||
self.cursor.is_hidden = false;
|
||
}
|
||
pub fn set_scroll_region(&mut self, top_line_index: usize, bottom_line_index: usize) {
|
||
self.scroll_region = Some((top_line_index, bottom_line_index));
|
||
}
|
||
pub fn clear_scroll_region(&mut self) {
|
||
self.scroll_region = None;
|
||
}
|
||
pub fn set_scroll_region_to_viewport_size(&mut self) {
|
||
self.scroll_region = Some((0, self.height - 1));
|
||
}
|
||
pub fn delete_lines_in_scroll_region(
|
||
&mut self,
|
||
count: usize,
|
||
pad_character: TerminalCharacter,
|
||
) {
|
||
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
|
||
let current_line_index = self.cursor.y;
|
||
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom
|
||
{
|
||
// when deleting lines inside the scroll region, we must make sure it stays the
|
||
// same size (and that other lines below it aren't shifted inside it)
|
||
// so we delete the current line(s) and add an empty line at the end of the scroll
|
||
// region
|
||
for _ in 0..count {
|
||
self.viewport.remove(current_line_index);
|
||
let columns = vec![pad_character; self.width];
|
||
if self.viewport.len() > scroll_region_bottom {
|
||
self.viewport
|
||
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
|
||
} else {
|
||
self.viewport.push(Row::from_columns(columns).canonical());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pub fn add_empty_lines_in_scroll_region(
|
||
&mut self,
|
||
count: usize,
|
||
pad_character: TerminalCharacter,
|
||
) {
|
||
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
|
||
let current_line_index = self.cursor.y;
|
||
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom
|
||
{
|
||
// when adding empty lines inside the scroll region, we must make sure it stays the
|
||
// same size and that lines don't "leak" outside of it
|
||
// so we add an empty line where the cursor currently is, and delete the last line
|
||
// of the scroll region
|
||
for _ in 0..count {
|
||
if scroll_region_bottom < self.viewport.len() {
|
||
self.viewport.remove(scroll_region_bottom);
|
||
}
|
||
let columns = vec![pad_character; self.width];
|
||
self.viewport
|
||
.insert(current_line_index, Row::from_columns(columns).canonical());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pub fn move_cursor_to_column(&mut self, column: usize) {
|
||
self.cursor.x = column;
|
||
self.pad_current_line_until(self.cursor.x);
|
||
}
|
||
pub fn move_cursor_to_line(&mut self, line: usize, pad_character: TerminalCharacter) {
|
||
self.cursor.y = std::cmp::min(self.height - 1, line);
|
||
self.pad_lines_until(self.cursor.y, pad_character);
|
||
self.pad_current_line_until(self.cursor.x);
|
||
}
|
||
pub fn replace_with_empty_chars(&mut self, count: usize, empty_char_style: CharacterStyles) {
|
||
let mut empty_character = EMPTY_TERMINAL_CHARACTER;
|
||
empty_character.styles = empty_char_style;
|
||
let pad_until = std::cmp::min(self.width, self.cursor.x + count);
|
||
self.pad_current_line_until(pad_until);
|
||
let current_row = self.viewport.get_mut(self.cursor.y).unwrap();
|
||
for i in 0..count {
|
||
current_row.replace_character_at(empty_character, self.cursor.x + i);
|
||
}
|
||
}
|
||
pub fn erase_characters(&mut self, count: usize, empty_char_style: CharacterStyles) {
|
||
let mut empty_character = EMPTY_TERMINAL_CHARACTER;
|
||
empty_character.styles = empty_char_style;
|
||
let current_row = self.viewport.get_mut(self.cursor.y).unwrap();
|
||
for _ in 0..count {
|
||
current_row.delete_character(self.cursor.x);
|
||
}
|
||
let mut empty_space_to_append = vec![empty_character; count];
|
||
self.viewport
|
||
.get_mut(self.cursor.y)
|
||
.unwrap()
|
||
.append(&mut empty_space_to_append);
|
||
}
|
||
fn add_newline(&mut self) {
|
||
self.add_canonical_line();
|
||
self.mark_for_rerender();
|
||
}
|
||
pub fn mark_for_rerender(&mut self) {
|
||
self.should_render = true;
|
||
}
|
||
fn reset_terminal_state(&mut self) {
|
||
self.lines_above = VecDeque::with_capacity(SCROLL_BACK);
|
||
self.lines_below = vec![];
|
||
self.viewport = vec![Row::new().canonical()];
|
||
self.alternative_lines_above_viewport_and_cursor = None;
|
||
self.cursor_key_mode = false;
|
||
self.scroll_region = None;
|
||
self.clear_viewport_before_rendering = true;
|
||
self.cursor = Cursor::new(0, 0);
|
||
}
|
||
}
|
||
|
||
impl vte::Perform for Grid {
|
||
fn print(&mut self, c: char) {
|
||
// apparently, building TerminalCharacter like this without a "new" method
|
||
// is a little faster
|
||
let terminal_character = TerminalCharacter {
|
||
character: c,
|
||
styles: self.pending_styles,
|
||
};
|
||
self.add_character(terminal_character);
|
||
}
|
||
|
||
fn execute(&mut self, byte: u8) {
|
||
match byte {
|
||
8 => {
|
||
// backspace
|
||
self.move_cursor_backwards(1);
|
||
}
|
||
9 => {
|
||
// tab
|
||
self.advance_to_next_tabstop(self.pending_styles);
|
||
}
|
||
10 => {
|
||
// 0a, newline
|
||
self.add_newline();
|
||
}
|
||
13 => {
|
||
// 0d, carriage return
|
||
self.move_cursor_to_beginning_of_line();
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn hook(&mut self, _params: &[i64], _intermediates: &[u8], _ignore: bool, _c: char) {
|
||
// TBD
|
||
}
|
||
|
||
fn put(&mut self, _byte: u8) {
|
||
// TBD
|
||
}
|
||
|
||
fn unhook(&mut self) {
|
||
// TBD
|
||
}
|
||
|
||
fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {
|
||
// TBD
|
||
}
|
||
|
||
fn csi_dispatch(&mut self, params: &[i64], _intermediates: &[u8], _ignore: bool, c: char) {
|
||
if c == 'm' {
|
||
self.pending_styles.add_style_from_ansi_params(params);
|
||
} else if c == 'C' {
|
||
// move cursor forward
|
||
let move_by = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
self.move_cursor_forward_until_edge(move_by);
|
||
} else if c == 'K' {
|
||
// clear line (0 => right, 1 => left, 2 => all)
|
||
if params[0] == 0 {
|
||
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
|
||
char_to_replace.styles = self.pending_styles;
|
||
self.replace_characters_in_line_after_cursor(char_to_replace);
|
||
} else if params[0] == 1 {
|
||
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
|
||
char_to_replace.styles = self.pending_styles;
|
||
self.replace_characters_in_line_before_cursor(char_to_replace);
|
||
} else if params[0] == 2 {
|
||
self.clear_cursor_line();
|
||
}
|
||
} else if c == 'J' {
|
||
// clear all (0 => below, 1 => above, 2 => all, 3 => saved)
|
||
let mut char_to_replace = EMPTY_TERMINAL_CHARACTER;
|
||
char_to_replace.styles = self.pending_styles;
|
||
if params[0] == 0 {
|
||
self.clear_all_after_cursor(char_to_replace);
|
||
} else if params[0] == 2 {
|
||
self.clear_all(char_to_replace);
|
||
}
|
||
// TODO: implement 1
|
||
} else if c == 'H' {
|
||
// goto row/col
|
||
// we subtract 1 from the row/column because these are 1 indexed
|
||
// (except when they are 0, in which case they should be 1
|
||
// don't look at me, I don't make the rules)
|
||
let (row, col) = if params.len() == 1 {
|
||
if params[0] == 0 {
|
||
(0, params[0] as usize)
|
||
} else {
|
||
((params[0] as usize).saturating_sub(1), params[0] as usize)
|
||
}
|
||
} else if params[0] == 0 {
|
||
(0, (params[1] as usize).saturating_sub(1))
|
||
} else {
|
||
(
|
||
(params[0] as usize).saturating_sub(1),
|
||
(params[1] as usize).saturating_sub(1),
|
||
)
|
||
};
|
||
let pad_character = EMPTY_TERMINAL_CHARACTER;
|
||
self.move_cursor_to(col, row, pad_character);
|
||
} else if c == 'A' {
|
||
// move cursor up until edge of screen
|
||
let move_up_count = if params[0] == 0 { 1 } else { params[0] };
|
||
self.move_cursor_up(move_up_count as usize);
|
||
} else if c == 'B' {
|
||
// move cursor down until edge of screen
|
||
let move_down_count = if params[0] == 0 { 1 } else { params[0] };
|
||
let pad_character = EMPTY_TERMINAL_CHARACTER;
|
||
self.move_cursor_down(move_down_count as usize, pad_character);
|
||
} else if c == 'D' {
|
||
let move_back_count = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
self.move_cursor_back(move_back_count);
|
||
} else if c == 'l' {
|
||
let first_intermediate_is_questionmark = match _intermediates.get(0) {
|
||
Some(b'?') => true,
|
||
None => false,
|
||
_ => false,
|
||
};
|
||
if first_intermediate_is_questionmark {
|
||
match params.get(0) {
|
||
Some(&1049) => {
|
||
if let Some((
|
||
alternative_lines_above,
|
||
alternative_viewport,
|
||
alternative_cursor,
|
||
)) = self.alternative_lines_above_viewport_and_cursor.as_mut()
|
||
{
|
||
std::mem::swap(&mut self.lines_above, alternative_lines_above);
|
||
std::mem::swap(&mut self.viewport, alternative_viewport);
|
||
std::mem::swap(&mut self.cursor, alternative_cursor);
|
||
}
|
||
self.alternative_lines_above_viewport_and_cursor = None;
|
||
self.clear_viewport_before_rendering = true;
|
||
self.change_size(self.height, self.width); // the alternative_viewport might have been of a different size...
|
||
self.mark_for_rerender();
|
||
}
|
||
Some(&25) => {
|
||
self.hide_cursor();
|
||
self.mark_for_rerender();
|
||
}
|
||
Some(&1) => {
|
||
self.cursor_key_mode = false;
|
||
}
|
||
_ => {}
|
||
};
|
||
}
|
||
} else if c == 'h' {
|
||
let first_intermediate_is_questionmark = match _intermediates.get(0) {
|
||
Some(b'?') => true,
|
||
None => false,
|
||
_ => false,
|
||
};
|
||
if first_intermediate_is_questionmark {
|
||
match params.get(0) {
|
||
Some(&25) => {
|
||
self.show_cursor();
|
||
self.mark_for_rerender();
|
||
}
|
||
Some(&1049) => {
|
||
let current_lines_above = std::mem::replace(
|
||
&mut self.lines_above,
|
||
VecDeque::with_capacity(SCROLL_BACK),
|
||
);
|
||
let current_viewport =
|
||
std::mem::replace(&mut self.viewport, vec![Row::new().canonical()]);
|
||
let current_cursor = std::mem::replace(&mut self.cursor, Cursor::new(0, 0));
|
||
self.alternative_lines_above_viewport_and_cursor =
|
||
Some((current_lines_above, current_viewport, current_cursor));
|
||
self.clear_viewport_before_rendering = true;
|
||
}
|
||
Some(&1) => {
|
||
self.cursor_key_mode = true;
|
||
}
|
||
_ => {}
|
||
};
|
||
}
|
||
} else if c == 'r' {
|
||
if params.len() > 1 {
|
||
// minus 1 because these are 1 indexed
|
||
let top_line_index = (params[0] as usize).saturating_sub(1);
|
||
let bottom_line_index = (params[1] as usize).saturating_sub(1);
|
||
self.set_scroll_region(top_line_index, bottom_line_index);
|
||
self.show_cursor();
|
||
} else {
|
||
self.clear_scroll_region();
|
||
}
|
||
} else if c == 'M' {
|
||
// delete lines if currently inside scroll region
|
||
let line_count_to_delete = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
let pad_character = EMPTY_TERMINAL_CHARACTER;
|
||
self.delete_lines_in_scroll_region(line_count_to_delete, pad_character);
|
||
} else if c == 'L' {
|
||
// insert blank lines if inside scroll region
|
||
let line_count_to_add = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
let pad_character = EMPTY_TERMINAL_CHARACTER;
|
||
self.add_empty_lines_in_scroll_region(line_count_to_add, pad_character);
|
||
} else if c == 'q' {
|
||
// ignore for now to run on mac
|
||
} else if c == 'G' {
|
||
let column = if params[0] == 0 {
|
||
0
|
||
} else {
|
||
params[0] as usize - 1
|
||
};
|
||
self.move_cursor_to_column(column);
|
||
} else if c == 'd' {
|
||
// goto line
|
||
let line = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
// minus 1 because this is 1 indexed
|
||
params[0] as usize - 1
|
||
};
|
||
let pad_character = EMPTY_TERMINAL_CHARACTER;
|
||
self.move_cursor_to_line(line, pad_character);
|
||
} else if c == 'P' {
|
||
// erase characters
|
||
let count = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
self.erase_characters(count, self.pending_styles);
|
||
} else if c == 'X' {
|
||
// erase characters and replace with empty characters of current style
|
||
let count = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
self.replace_with_empty_chars(count, self.pending_styles);
|
||
} else if c == 'T' {
|
||
/*
|
||
* 124 54 T SD
|
||
* Scroll down, new lines inserted at top of screen
|
||
* [4T = Scroll down 4, bring previous lines back into view
|
||
*/
|
||
let line_count: i64 = *params.get(0).expect("A number of lines was expected.");
|
||
|
||
if line_count >= 0 {
|
||
self.rotate_scroll_region_up(line_count as usize);
|
||
} else {
|
||
// TODO: can this actually happen?
|
||
self.rotate_scroll_region_down(line_count.abs() as usize);
|
||
}
|
||
} else if c == 'S' {
|
||
// move scroll up
|
||
let count = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
self.rotate_scroll_region_down(count);
|
||
} else if c == '@' {
|
||
let count = if params[0] == 0 {
|
||
1
|
||
} else {
|
||
params[0] as usize
|
||
};
|
||
for _ in 0..count {
|
||
// TODO: should this be styled?
|
||
self.insert_character_at_cursor_position(EMPTY_TERMINAL_CHARACTER);
|
||
}
|
||
} else {
|
||
let _ = debug_log_to_file(format!("Unhandled csi: {}->{:?}", c, params));
|
||
}
|
||
}
|
||
|
||
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
|
||
match (byte, intermediates.get(0)) {
|
||
(b'M', None) => {
|
||
self.move_cursor_up_with_scrolling(1);
|
||
}
|
||
(b'c', None) => {
|
||
self.reset_terminal_state();
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct Row {
|
||
pub columns: Vec<TerminalCharacter>,
|
||
pub is_canonical: bool,
|
||
}
|
||
|
||
impl Debug for Row {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||
for character in &self.columns {
|
||
write!(f, "{:?}", character)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Row {
|
||
pub fn new() -> Self {
|
||
Row {
|
||
columns: vec![],
|
||
is_canonical: false,
|
||
}
|
||
}
|
||
pub fn from_columns(columns: Vec<TerminalCharacter>) -> Self {
|
||
Row {
|
||
columns,
|
||
is_canonical: false,
|
||
}
|
||
}
|
||
pub fn from_rows(mut rows: Vec<Row>) -> Self {
|
||
if rows.is_empty() {
|
||
Row::new()
|
||
} else {
|
||
let mut first_row = rows.remove(0);
|
||
for row in rows.iter_mut() {
|
||
first_row.append(&mut row.columns);
|
||
}
|
||
first_row
|
||
}
|
||
}
|
||
pub fn with_character(mut self, terminal_character: TerminalCharacter) -> Self {
|
||
self.columns.push(terminal_character);
|
||
self
|
||
}
|
||
pub fn canonical(mut self) -> Self {
|
||
self.is_canonical = true;
|
||
self
|
||
}
|
||
pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
|
||
match self.columns.len().cmp(&x) {
|
||
Ordering::Equal => self.columns.push(terminal_character),
|
||
Ordering::Less => {
|
||
self.columns.resize(x, EMPTY_TERMINAL_CHARACTER);
|
||
self.columns.push(terminal_character);
|
||
}
|
||
Ordering::Greater => {
|
||
// this is much more performant than remove/insert
|
||
self.columns.push(terminal_character);
|
||
self.columns.swap_remove(x);
|
||
}
|
||
}
|
||
}
|
||
pub fn insert_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
|
||
match self.columns.len().cmp(&x) {
|
||
Ordering::Equal => self.columns.push(terminal_character),
|
||
Ordering::Less => {
|
||
self.columns.resize(x, EMPTY_TERMINAL_CHARACTER);
|
||
self.columns.push(terminal_character);
|
||
}
|
||
Ordering::Greater => {
|
||
self.columns.insert(x, terminal_character);
|
||
}
|
||
}
|
||
}
|
||
pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
|
||
// this is much more performant than remove/insert
|
||
if x < self.columns.len() {
|
||
self.columns.push(terminal_character);
|
||
self.columns.swap_remove(x);
|
||
}
|
||
}
|
||
pub fn replace_columns(&mut self, columns: Vec<TerminalCharacter>) {
|
||
self.columns = columns;
|
||
}
|
||
pub fn push(&mut self, terminal_character: TerminalCharacter) {
|
||
self.columns.push(terminal_character);
|
||
}
|
||
pub fn truncate(&mut self, x: usize) {
|
||
self.columns.truncate(x);
|
||
}
|
||
pub fn append(&mut self, to_append: &mut Vec<TerminalCharacter>) {
|
||
self.columns.append(to_append);
|
||
}
|
||
pub fn replace_beginning_with(&mut self, mut line_part: Vec<TerminalCharacter>) {
|
||
if line_part.len() > self.columns.len() {
|
||
self.columns.clear();
|
||
} else {
|
||
drop(self.columns.drain(0..line_part.len()));
|
||
}
|
||
line_part.append(&mut self.columns);
|
||
self.columns = line_part;
|
||
}
|
||
pub fn len(&self) -> usize {
|
||
self.columns.len()
|
||
}
|
||
pub fn delete_character(&mut self, x: usize) {
|
||
if x < self.columns.len() {
|
||
self.columns.remove(x);
|
||
}
|
||
}
|
||
pub fn split_to_rows_of_length(&mut self, max_row_length: usize) -> Vec<Row> {
|
||
let mut parts: Vec<Row> = vec![];
|
||
let mut current_part: Vec<TerminalCharacter> = vec![];
|
||
for character in self.columns.drain(..) {
|
||
if current_part.len() == max_row_length {
|
||
parts.push(Row::from_columns(current_part));
|
||
current_part = vec![];
|
||
}
|
||
current_part.push(character);
|
||
}
|
||
if !current_part.is_empty() {
|
||
parts.push(Row::from_columns(current_part))
|
||
};
|
||
if !parts.is_empty() && self.is_canonical {
|
||
parts.get_mut(0).unwrap().is_canonical = true;
|
||
}
|
||
parts
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct Cursor {
|
||
x: usize,
|
||
y: usize,
|
||
is_hidden: bool,
|
||
}
|
||
|
||
impl Cursor {
|
||
pub fn new(x: usize, y: usize) -> Self {
|
||
Cursor {
|
||
x,
|
||
y,
|
||
is_hidden: false,
|
||
}
|
||
}
|
||
}
|