diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/encoding.rs | 43 | ||||
| -rw-r--r-- | src/main.rs | 275 | ||||
| -rw-r--r-- | src/terminal.rs | 54 |
3 files changed, 319 insertions, 53 deletions
diff --git a/src/encoding.rs b/src/encoding.rs index 08ffb39..ccd2031 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -2,7 +2,13 @@ use crate::types::*; use smol::prelude::*; -use smol::io::BoxedReader; + +#[derive(Debug)] +pub struct Decode { + pub c: char, + pub skipped_bytes: usize, + pub found_bytes: usize, +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -31,9 +37,12 @@ pub fn get_utf8_byte_type(b: u8) -> UTF8ByteType { } -pub async fn read_utf8_char(input: &mut BoxedReader) -> Result<char> { +pub async fn read_utf8_char(input: &mut (impl AsyncRead + Unpin)) + -> Result<Decode> +{ let mut buf = vec![0; 4]; let mut unread_byte: Option<u8> = None; + let mut skipped_bytes = 0; loop { if let Some(byte) = unread_byte { @@ -43,56 +52,76 @@ pub async fn read_utf8_char(input: &mut BoxedReader) -> Result<char> { input.read_exact(&mut buf[0 .. 1]).await?; } - match get_utf8_byte_type(buf[0]) { - UTF8ByteType::Single => { }, + let found_bytes = match get_utf8_byte_type(buf[0]) { + UTF8ByteType::Single => { + 1 + }, + UTF8ByteType::Introducer(2) => { input.read_exact(&mut buf[1 .. 2]).await?; if get_utf8_byte_type(buf[1]) != UTF8ByteType::Continuation { unread_byte = Some(buf[1]); + skipped_bytes += 1; continue; } + + 2 }, + UTF8ByteType::Introducer(3) => { input.read_exact(&mut buf[1 .. 2]).await?; if get_utf8_byte_type(buf[1]) != UTF8ByteType::Continuation { unread_byte = Some(buf[1]); + skipped_bytes += 1; continue; } input.read_exact(&mut buf[2 .. 3]).await?; if get_utf8_byte_type(buf[2]) != UTF8ByteType::Continuation { unread_byte = Some(buf[2]); + skipped_bytes += 2; continue; } + + 3 }, + UTF8ByteType::Introducer(4) => { input.read_exact(&mut buf[1 .. 2]).await?; if get_utf8_byte_type(buf[1]) != UTF8ByteType::Continuation { unread_byte = Some(buf[1]); + skipped_bytes += 1; continue; } input.read_exact(&mut buf[2 .. 3]).await?; if get_utf8_byte_type(buf[2]) != UTF8ByteType::Continuation { unread_byte = Some(buf[2]); + skipped_bytes += 2; continue; } input.read_exact(&mut buf[3 .. 4]).await?; if get_utf8_byte_type(buf[3]) != UTF8ByteType::Continuation { unread_byte = Some(buf[3]); + skipped_bytes += 3; continue; } + + 4 }, /* If it's not the start of a valid character, ignore it. */ - _ => continue, - } + _ => { + skipped_bytes += 1; + continue; + } + }; if let Ok(string) = std::str::from_utf8(&buf) && let Some(c) = string.chars().next() { - return Ok(c); + return Ok(Decode { c, skipped_bytes, found_bytes }); } } } diff --git a/src/main.rs b/src/main.rs index f21096f..70bc0b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use crate::types::*; use smol::prelude::*; use smol::fs::File; +use smol::io::Cursor; use smol::lock::RwLock; use std::path::PathBuf; use std::process::ExitCode; @@ -41,6 +42,13 @@ struct Window { * land in, if it exists in the target row. */ cursor_neutral_column: RwLock<usize>, + scroll_top: RwLock<usize>, +} + +enum MovementColumnBehavior { + CurrentFromNeutral, + NeutralFromCurrent, + FirstNonBlank, } @@ -132,43 +140,70 @@ impl Ivy { Ok(()) } - async fn draw(&mut self) -> Result<()> { + async fn draw_all(&mut self) -> Result<()> { + let height = *self.terminal.read().await.height.read().await; + + self.draw_range(0 .. height - 1).await?; + self.draw_status_line().await?; + self.fix_cursor_position().await?; + + self.terminal.write().await.stdout.flush().await?; + + Ok(()) + } + + async fn draw_range(&mut self, range: Range<usize>) -> Result<()> { let mut terminal = self.terminal.write().await; + let window = self.window.read().await; let buffer = self.buffer.read().await; + let scroll_top = *window.scroll_top.read().await; - let height = *terminal.height.read().await; + terminal.do_cursor_position(0, range.start).await?; - let mut screen_y = 0; - for logical_y in 0 .. height - 1 { - if let Some(span) = buffer.line_span(logical_y).await { - if logical_y > 0 { + let mut screen_y = range.start; + for i in range.clone() { + if let Some(span) = buffer.line_span(i + scroll_top).await { + if i > range.start { terminal.stdout.write_all("\n".as_bytes()).await?; } terminal.stdout.write_all(&buffer.contents.read().await[span]).await?; - screen_y = logical_y + 1; + screen_y += 1; } else { break; } } - for _ in screen_y .. height - 1 { + for _ in screen_y .. range.end { terminal.stdout.write_all("\n~".as_bytes()).await?; } - terminal.stdout.write_all("\n status goes here".as_bytes()).await?; + Ok(()) + } + + async fn draw_status_line(&mut self) -> Result<()> { + let mut terminal = self.terminal.write().await; + let height = *terminal.height.read().await; + terminal.do_cursor_position(0, height).await?; + terminal.stdout.write_all(" status goes here".as_bytes()).await?; + + Ok(()) + } + + async fn fix_cursor_position(&mut self) -> Result<()> { + let mut terminal = self.terminal.write().await; let window = self.window.read().await; terminal.do_cursor_position(*window.cursor_column.read().await, - *window.cursor_row.read().await).await?; - terminal.stdout.flush().await?; + *window.cursor_row.read().await + - *window.scroll_top.read().await).await?; Ok(()) } async fn interact(&mut self) -> Result<()> { - self.draw().await?; + self.draw_all().await?; loop { let c = self.terminal.write().await.read_char().await?; @@ -186,10 +221,10 @@ impl Ivy { * the movement actually moves. Tentatively, it doesn't look * like POSIX has an opinion on this, but it matches Vim. */ - *window.cursor_neutral_column.write().await = *column; + Ok(MovementColumnBehavior::NeutralFromCurrent) + } else { + Ok(MovementColumnBehavior::CurrentFromNeutral) } - - Ok(()) }).await?; }, @@ -203,7 +238,7 @@ impl Ivy { *row += 1; }; - Ok(()) + Ok(MovementColumnBehavior::CurrentFromNeutral) }).await?; }, @@ -216,7 +251,7 @@ impl Ivy { *row -= 1; } - Ok(()) + Ok(MovementColumnBehavior::CurrentFromNeutral) }).await?; }, @@ -237,11 +272,13 @@ impl Ivy { * the movement actually moves. Tentatively, it doesn't look * like POSIX has an opinion on this, but it matches Vim. */ - *window.cursor_neutral_column.write().await = *column; + Ok(MovementColumnBehavior::NeutralFromCurrent) + } else { + Ok(MovementColumnBehavior::CurrentFromNeutral) } + } else { + Ok(MovementColumnBehavior::CurrentFromNeutral) } - - Ok(()) }).await?; }, @@ -255,9 +292,7 @@ impl Ivy { /* For this one, the neutral column changes regardless of * whether the column does. */ - *window.cursor_neutral_column.write().await = *column; - - Ok(()) + Ok(MovementColumnBehavior::NeutralFromCurrent) }).await?; }, @@ -276,14 +311,43 @@ impl Ivy { } else { *column = 0; } + } - /* For this one, the neutral column changes regardless of - * whether the column does. - */ - *window.cursor_neutral_column.write().await = *column; + /* For this one, the neutral column changes regardless of + * whether the column does. + */ + Ok(MovementColumnBehavior::NeutralFromCurrent) + }).await?; + }, + + 'H' => { + self.handle_movement(async |ivy: &mut Ivy| { + let window = ivy.window.write().await; + + let mut row = window.cursor_row.write().await; + *row = *window.scroll_top.read().await; + + Ok(MovementColumnBehavior::FirstNonBlank) + }).await?; + }, + + 'L' => { + self.handle_movement(async |ivy: &mut Ivy| { + let window = ivy.window.write().await; + let terminal = ivy.terminal.read().await; + let buffer = ivy.buffer.read().await; + + let scroll_top = *window.scroll_top.read().await; + let height = *terminal.height.read().await; + let total_lines = buffer.lines.read().await.len(); + + let mut row = window.cursor_row.write().await; + *row = scroll_top + height - 2; + if *row > total_lines - 1 { + *row = total_lines - 1; } - Ok(()) + Ok(MovementColumnBehavior::FirstNonBlank) }).await?; }, @@ -297,9 +361,13 @@ impl Ivy { /* A movement is an arbitrarily complicated action that will not change the * contents of the buffer, but may change other things, including the cursor * position. + * + * The return value of the action indicates whether to update the neutral + * column. */ async fn handle_movement(&mut self, - movement: impl AsyncFn(&mut Ivy) -> Result<()>) + movement: impl AsyncFn(&mut Ivy) + -> Result<MovementColumnBehavior>) -> Result<()> { let (old_row, old_column) = { @@ -309,15 +377,71 @@ impl Ivy { *window.cursor_column.read().await) }; - movement(self).await?; + let column_behavior = movement(self).await?; + + match column_behavior { + MovementColumnBehavior::NeutralFromCurrent => { + /* We clamp the destination column to the line width and use that as + * the neutral column. + */ + self.update_neutral_column().await?; + }, + MovementColumnBehavior::CurrentFromNeutral => { + /* We clamp the neutral column to the line width and use that as the + * destination column. + */ + self.clamp_cursor_column().await?; + }, + MovementColumnBehavior::FirstNonBlank => { + /* We compute the first non-blank column on the line and use that as + * both the destination and neutral columns. + */ + self.first_non_blank_column().await?; + }, + } - /* We clamp the column to the line width unconditionally, regardless of - * what kind of movement we did. This is always correct, because no - * intended behavior ever places the cursor beyond the end of the line. - * It does require taking a couple of locks that we might always need, but - * there's no strong reason to avoid that. - */ + self.scroll_to_cursor().await?; + + { + let window = self.window.read().await; + let row = *window.cursor_row.read().await; + let column = *window.cursor_column.read().await; + + if old_row != row || old_column != column { + let mut terminal = self.terminal.write().await; + let scroll_top = *window.scroll_top.read().await; + terminal.do_cursor_position(column, row - scroll_top).await?; + terminal.stdout.flush().await?; + } + } + + Ok(()) + } + + async fn update_neutral_column(&mut self) -> Result<()> { + let buffer = self.buffer.write().await; + let window = self.window.write().await; + let mut column = window.cursor_column.write().await; + let mut neutral_column = window.cursor_neutral_column.write().await; + let row = window.cursor_row.read().await; + + if let Some(span) = buffer.line_span(*row).await { + let width = span.end - span.start; + + if width == 0 { + *column = 0; + } else if *column > width - 1 { + *column = width - 1; + } + } + + *neutral_column = *column; + + Ok(()) + } + + async fn clamp_cursor_column(&mut self) -> Result<()> { let window = self.window.write().await; let row = window.cursor_row.read().await; let mut column = window.cursor_column.write().await; @@ -336,11 +460,81 @@ impl Ivy { } } - if old_row != *row || old_column != *column { - let mut terminal = self.terminal.write().await; + Ok(()) + } + + async fn first_non_blank_column(&mut self) -> Result<()> { + let row = *self.window.read().await.cursor_row.read().await; + let buffer = self.buffer.write().await; - terminal.do_cursor_position(*column, *row).await?; - terminal.stdout.flush().await?; + if let Some(row_span) = buffer.line_span(row).await { + let mut offset = 0; + loop { + let sub_span = row_span.start + offset .. row_span.end; + let mut contents = buffer.contents.write().await; + let mut cursor = Cursor::new(&mut contents[sub_span]); + + if let Ok(decode) = encoding::read_utf8_char(&mut cursor).await { + offset += decode.skipped_bytes; + + if decode.c.is_whitespace() { + offset += decode.found_bytes; + } else { + break; + } + } else { + break; + } + } + + let window = self.window.write().await; + *window.cursor_column.write().await = offset; + *window.cursor_neutral_column.write().await = offset; + } else { + let window = self.window.write().await; + *window.cursor_column.write().await = 0; + *window.cursor_neutral_column.write().await = 0; + } + + Ok(()) + } + + async fn scroll_to_cursor(&mut self) -> Result<()> { + let old_scroll_top = *self.window.read().await.scroll_top.read().await; + + { + let terminal = self.terminal.read().await; + let window = self.window.write().await; + + let mut scroll_top = window.scroll_top.write().await; + let row = *window.cursor_row.read().await; + let height = *terminal.height.read().await; + let last_visible = *scroll_top + height - 2; + + if row < *scroll_top { + *scroll_top -= *scroll_top - row; + } else if row > last_visible { + *scroll_top += row - last_visible; + } + } + + let new_scroll_top = *self.window.read().await.scroll_top.read().await; + let height = *self.terminal.read().await.height.read().await - 1; + if new_scroll_top != old_scroll_top { + let difference = new_scroll_top as isize - old_scroll_top as isize; + if (difference.abs() as usize) < height { + self.terminal.write().await.scroll(difference).await?; + + if difference > 0 { + self.draw_range((height as isize - difference) as usize + .. height).await?; + } else { + self.draw_range(0 .. difference.abs() as usize).await?; + } + } else { + self.terminal.write().await.clear().await?; + self.draw_all().await?; + } } Ok(()) @@ -436,6 +630,7 @@ impl Window { cursor_row: RwLock::new(0), cursor_column: RwLock::new(0), cursor_neutral_column: RwLock::new(0), + scroll_top: RwLock::new(0), } } } diff --git a/src/terminal.rs b/src/terminal.rs index 28e83b3..300fcfb 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -81,10 +81,18 @@ impl Terminal { *self.width.write().await = width.try_into().unwrap(); *self.height.write().await = height.try_into().unwrap(); + self.set_scroll_region(0, height - 1).await?; + Ok(()) } pub async fn zap_full_screen(&mut self) -> Result<()> { + let (width, height) = self.do_report_size().await?; + *self.width.write().await = width.try_into().unwrap(); + *self.height.write().await = height.try_into().unwrap(); + + self.set_scroll_region(0, height).await?; + self.do_end_alternate_screen().await?; self.stdout.flush().await?; @@ -92,7 +100,9 @@ impl Terminal { } pub async fn read_char(&mut self) -> Result<char> { - encoding::read_utf8_char(&mut self.stdin).await + let decode = encoding::read_utf8_char(&mut self.stdin).await?; + + Ok(decode.c) } pub async fn do_escape(&mut self, escape_type: EscapeType, code: &str, @@ -113,7 +123,7 @@ impl Terminal { } pub async fn read_report(&mut self, escape_type: EscapeType) - -> Result<(char, Vec<u64>)> + -> Result<(char, Vec<usize>)> { let mut expected = escape_type.intro(); @@ -126,7 +136,7 @@ impl Terminal { let mut values = Vec::new(); - let mut number: u64 = 0; + let mut number: usize = 0; let mut in_number = false; let mut after_semicolon = false; @@ -140,9 +150,11 @@ impl Terminal { if c.is_ascii_digit() { if in_number { number = number * 10 - + <u32 as Into<u64>>::into(c.to_digit(10).unwrap()); + + <u32 as TryInto<usize>>::try_into( + c.to_digit(10).unwrap()).unwrap(); } else { - number = c.to_digit(10).unwrap().into(); + number = <u32 as TryInto<usize>>::try_into( + c.to_digit(10).unwrap()).unwrap(); in_number = true; after_semicolon = false; @@ -195,7 +207,7 @@ impl Terminal { } // dtterm? xterm - pub async fn do_report_size(&mut self) -> Result<(u64, u64)> { + pub async fn do_report_size(&mut self) -> Result<(usize, usize)> { self.do_escape(EscapeType::CSI, "t", &[18]).await?; self.stdout.flush().await?; let (code, values) = self.read_report(EscapeType::CSI).await?; @@ -205,6 +217,36 @@ impl Terminal { Err(std::io::Error::other("Couldn't read terminal size.")) } } + + // vt100 + pub async fn clear(&mut self) -> Result<()> { + self.do_escape(EscapeType::CSI, "J", &[2]).await + } + + // vt420, ECMA-48 + pub async fn scroll_up(&mut self, distance: usize) -> Result<()> { + self.do_escape(EscapeType::CSI, "S", &[distance]).await + } + + // vt420, ECMA-48 + pub async fn scroll_down(&mut self, distance: usize) -> Result<()> { + self.do_escape(EscapeType::CSI, "T", &[distance]).await + } + + pub async fn scroll(&mut self, distance_up: isize) -> Result<()> { + if distance_up >= 0 { + self.scroll_up(distance_up as usize).await + } else { + self.scroll_down(-distance_up as usize).await + } + } + + // vt100 + pub async fn set_scroll_region(&mut self, top: usize, bottom: usize) + -> Result<()> + { + self.do_escape(EscapeType::CSI, "r", &[top, bottom]).await + } } |