From 3d4fea2cec4700deb0ded23e890fac42cddd4f4d Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Mon, 19 May 2025 16:08:21 -0400 Subject: [PATCH] Fully implemented vi-style editor commands --- src/libsh/error.rs | 2 + src/prompt/readline/keys.rs | 111 +++++++ src/prompt/readline/line.rs | 303 +++++++++++++++++++ src/prompt/readline/linecmd.rs | 364 +++++++++++++++++++++++ src/prompt/readline/mod.rs | 513 ++++++++++++++++----------------- src/prompt/readline/term.rs | 107 ++++--- 6 files changed, 1076 insertions(+), 324 deletions(-) create mode 100644 src/prompt/readline/keys.rs create mode 100644 src/prompt/readline/linecmd.rs diff --git a/src/libsh/error.rs b/src/libsh/error.rs index c0ec7f0..4d73854 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -325,6 +325,7 @@ pub enum ShErrKind { FuncReturn(i32), LoopContinue(i32), LoopBreak(i32), + ReadlineErr, Null } @@ -345,6 +346,7 @@ impl Display for ShErrKind { ShErrKind::FuncReturn(_) => "", ShErrKind::LoopContinue(_) => "", ShErrKind::LoopBreak(_) => "", + ShErrKind::ReadlineErr => "Line Read Error", ShErrKind::Null => "", }; write!(f,"{output}") diff --git a/src/prompt/readline/keys.rs b/src/prompt/readline/keys.rs new file mode 100644 index 0000000..eb9d60f --- /dev/null +++ b/src/prompt/readline/keys.rs @@ -0,0 +1,111 @@ +// Credit to Rustyline for the design ideas in this module +// https://github.com/kkawakam/rustyline +#[derive(Clone,Debug)] +pub struct KeyEvent(pub KeyCode, pub ModKeys); + +impl KeyEvent { + pub fn new(ch: char, mut mods: ModKeys) -> Self { + use {KeyCode as K, KeyEvent as E, ModKeys as M}; + + if !ch.is_control() { + if !mods.is_empty() { + mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if + // `c` is uppercase + } + return E(K::Char(ch), mods); + } + match ch { + '\x00' => E(K::Char('@'), mods | M::CTRL), // '\0' + '\x01' => E(K::Char('A'), mods | M::CTRL), + '\x02' => E(K::Char('B'), mods | M::CTRL), + '\x03' => E(K::Char('C'), mods | M::CTRL), + '\x04' => E(K::Char('D'), mods | M::CTRL), + '\x05' => E(K::Char('E'), mods | M::CTRL), + '\x06' => E(K::Char('F'), mods | M::CTRL), + '\x07' => E(K::Char('G'), mods | M::CTRL), // '\a' + '\x08' => E(K::Backspace, mods), // '\b' + '\x09' => { + // '\t' + if mods.contains(M::SHIFT) { + mods.remove(M::SHIFT); + E(K::BackTab, mods) + } else { + E(K::Tab, mods) + } + } + '\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10) + '\x0b' => E(K::Char('K'), mods | M::CTRL), + '\x0c' => E(K::Char('L'), mods | M::CTRL), + '\x0d' => E(K::Enter, mods), // '\r' (13) + '\x0e' => E(K::Char('N'), mods | M::CTRL), + '\x0f' => E(K::Char('O'), mods | M::CTRL), + '\x10' => E(K::Char('P'), mods | M::CTRL), + '\x11' => E(K::Char('Q'), mods | M::CTRL), + '\x12' => E(K::Char('R'), mods | M::CTRL), + '\x13' => E(K::Char('S'), mods | M::CTRL), + '\x14' => E(K::Char('T'), mods | M::CTRL), + '\x15' => E(K::Char('U'), mods | M::CTRL), + '\x16' => E(K::Char('V'), mods | M::CTRL), + '\x17' => E(K::Char('W'), mods | M::CTRL), + '\x18' => E(K::Char('X'), mods | M::CTRL), + '\x19' => E(K::Char('Y'), mods | M::CTRL), + '\x1a' => E(K::Char('Z'), mods | M::CTRL), + '\x1b' => E(K::Esc, mods), // Ctrl-[, '\e' + '\x1c' => E(K::Char('\\'), mods | M::CTRL), + '\x1d' => E(K::Char(']'), mods | M::CTRL), + '\x1e' => E(K::Char('^'), mods | M::CTRL), + '\x1f' => E(K::Char('_'), mods | M::CTRL), + '\x7f' => E(K::Backspace, mods), // Rubout, Ctrl-? + '\u{9b}' => E(K::Esc, mods | M::SHIFT), + _ => E(K::Null, mods), + } + } +} + +#[derive(Clone,Debug)] +pub enum KeyCode { + UnknownEscSeq, + Backspace, + BackTab, + BracketedPasteStart, + BracketedPasteEnd, + Char(char), + Delete, + Down, + End, + Enter, + Esc, + F(u8), + Home, + Insert, + Left, + Null, + PageDown, + PageUp, + Right, + Tab, + Up, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct ModKeys: u8 { + /// Control modifier + const CTRL = 1<<3; + /// Escape or Alt modifier + const ALT = 1<<2; + /// Shift modifier + const SHIFT = 1<<1; + + /// No modifier + const NONE = 0; + /// Ctrl + Shift + const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits(); + /// Alt + Shift + const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits(); + /// Ctrl + Alt + const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits(); + /// Ctrl + Alt + Shift + const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits(); + } +} diff --git a/src/prompt/readline/line.rs b/src/prompt/readline/line.rs index f2c78de..2f9c7e6 100644 --- a/src/prompt/readline/line.rs +++ b/src/prompt/readline/line.rs @@ -1,3 +1,9 @@ +use std::ops::Range; + +use crate::{libsh::error::ShResult, prompt::readline::linecmd::Anchor}; + +use super::linecmd::{At, CharSearch, MoveCmd, Movement, Verb, VerbCmd, Word}; + #[derive(Default,Debug)] pub struct LineBuf { @@ -9,6 +15,10 @@ impl LineBuf { pub fn new() -> Self { Self::default() } + pub fn with_initial(mut self, init: S) -> Self { + self.buffer = init.to_string().chars().collect(); + self + } pub fn count_lines(&self) -> usize { self.buffer.iter().filter(|&&c| c == '\n').count() } @@ -19,6 +29,27 @@ impl LineBuf { self.buffer.clear(); self.cursor = 0; } + pub fn backspace(&mut self) { + if self.cursor() == 0 { + return + } + self.delete_pos(self.cursor() - 1); + } + pub fn delete(&mut self) { + if self.cursor() >= self.buffer.len() { + return + } + self.delete_pos(self.cursor()); + } + pub fn delete_pos(&mut self, pos: usize) { + self.buffer.remove(pos); + if pos < self.cursor() { + self.cursor = self.cursor.saturating_sub(1) + } + if self.cursor() >= self.buffer.len() { + self.cursor = self.buffer.len().saturating_sub(1) + } + } pub fn insert_at_cursor(&mut self, ch: char) { self.buffer.insert(self.cursor, ch); self.move_cursor_right(); @@ -74,8 +105,280 @@ impl LineBuf { self.buffer.drain(start..end); self.cursor = start; } + pub fn len(&self) -> usize { + self.buffer.len() + } + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + pub fn cursor_char(&self) -> char { + self.buffer[self.cursor] + } + fn backward_until bool>(&self, mut start: usize, cond: F) -> usize { + while start > 0 && !cond(start) { + start -= 1; + } + start + } + fn forward_until bool>(&self, mut start: usize, cond: F) -> usize { + while start < self.len() && !cond(start) { + start += 1; + } + start + } + pub fn calc_range(&self, movement: &Movement) -> Range { + let mut start = self.cursor(); + let mut end = self.cursor(); + + match movement { + Movement::WholeLine => { + start = self.backward_until(start, |pos| self.buffer[pos] == '\n'); + if self.buffer.get(start) == Some(&'\n') { + start += 1; // Exclude the previous newline + } + end = self.forward_until(end, |pos| self.buffer[pos] == '\n'); + } + Movement::BeginningOfLine => { + start = self.backward_until(start, |pos| self.buffer[pos] == '\n'); + } + Movement::BeginningOfFirstWord => { + let start_of_line = self.backward_until(start, |pos| self.buffer[pos] == '\n'); + start = self.forward_until(start_of_line, |pos| !self.buffer[pos].is_whitespace()); + } + Movement::EndOfLine => { + end = self.forward_until(end, |pos| self.buffer[pos] == '\n'); + } + Movement::BackwardWord(word) => { + let cur_char = self.cursor_char(); + match word { + Word::Big => { + if cur_char.is_whitespace() { + start = self.backward_until(start, |pos| !self.buffer[pos].is_whitespace()) + } + start = self.backward_until(start, |pos| self.buffer[pos].is_whitespace()); + start += 1; + } + Word::Normal => { + if cur_char.is_alphanumeric() || cur_char == '_' { + start = self.backward_until(start, |pos| !(self.buffer[pos].is_alphanumeric() || self.buffer[pos] == '_')); + start += 1; + } else { + start = self.backward_until(start, |pos| (self.buffer[pos].is_alphanumeric() || self.buffer[pos] == '_')); + start += 1; + } + } + } + } + Movement::ForwardWord(at, word) => { + let cur_char = self.cursor_char(); + let is_ws = |pos: usize| self.buffer[pos].is_whitespace(); + let not_ws = |pos: usize| !self.buffer[pos].is_whitespace(); + + match word { + Word::Big => { + if cur_char.is_whitespace() { + end = self.forward_until(end, not_ws); + } else { + end = self.forward_until(end, is_ws); + end = self.forward_until(end, not_ws); + } + + match at { + At::Start => {/* Done */} + At::AfterEnd => { + end = self.forward_until(end, is_ws); + } + At::BeforeEnd => { + end = self.forward_until(end, is_ws); + end = end.saturating_sub(1); + } + } + } + Word::Normal => { + let ch_class = CharClass::from(self.buffer[end]); + if cur_char.is_whitespace() { + end = self.forward_until(end, not_ws); + } else { + end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])) + } + + match at { + At::Start => {/* Done */ } + At::AfterEnd => { + end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])); + } + At::BeforeEnd => { + end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])); + end = end.saturating_sub(1); + } + } + } + } + } + Movement::BackwardChar => { + start = start.saturating_sub(1); + } + Movement::ForwardChar => { + end = end.saturating_add(1); + } + Movement::TextObj(text_obj, bound) => todo!(), + Movement::CharSearch(char_search) => { + match char_search { + CharSearch::FindFwd(ch) => { + let search = self.forward_until(end, |pos| self.buffer[pos] == *ch); + + // we check anyway because it may have reached the end without finding anything + if self.buffer[search] == *ch { + end = search; + } + } + CharSearch::FwdTo(ch) => { + let search = self.forward_until(end, |pos| self.buffer[pos] == *ch); + + // we check anyway because it may have reached the end without finding anything + if self.buffer[search] == *ch { + end = search.saturating_sub(1); + } + } + CharSearch::FindBkwd(ch) => { + let search = self.forward_until(start, |pos| self.buffer[pos] == *ch); + + // we check anyway because it may have reached the end without finding anything + if self.buffer[search] == *ch { + start = search; + } + } + CharSearch::BkwdTo(ch) => { + let search = self.forward_until(start, |pos| self.buffer[pos] == *ch); + + // we check anyway because it may have reached the end without finding anything + if self.buffer[search] == *ch { + start = search.saturating_add(1); + } + } + } + } + Movement::ViFirstPrint => todo!(), + Movement::LineUp => todo!(), + Movement::LineDown => todo!(), + Movement::WholeBuffer => { + start = 0; + end = self.len().saturating_sub(1); + } + Movement::BeginningOfBuffer => { + start = 0; + } + Movement::EndOfBuffer => { + end = self.len().saturating_sub(1); + } + Movement::Null => {/* nothing */} + } + + end = end.min(self.len()); + + start..end + } + pub fn exec_vi_cmd(&mut self, verb: Option, move_cmd: Option) -> ShResult<()> { + match (verb, move_cmd) { + (Some(v), None) => self.exec_vi_verb(v), + (None, Some(m)) => self.exec_vi_movement(m), + (Some(v), Some(m)) => self.exec_vi_moveverb(v,m), + (None, None) => unreachable!() + } + } + pub fn exec_vi_verb(&mut self, verb: Verb) -> ShResult<()> { + assert!(!verb.needs_movement()); + match verb { + Verb::DeleteOne(anchor) => { + match anchor { + Anchor::After => { + self.delete(); + } + Anchor::Before => { + self.backspace(); + } + } + } + Verb::InsertChar(ch) => self.insert_at_cursor(ch), + Verb::InsertMode => todo!(), + Verb::JoinLines => todo!(), + Verb::ToggleCase => todo!(), + Verb::OverwriteMode => todo!(), + Verb::Substitute => todo!(), + Verb::Put(_) => todo!(), + Verb::Undo => todo!(), + Verb::RepeatLast => todo!(), + Verb::Dedent => todo!(), + Verb::Indent => todo!(), + Verb::ReplaceChar(_) => todo!(), + _ => unreachable!() + } + Ok(()) + } + pub fn exec_vi_movement(&mut self, move_cmd: MoveCmd) -> ShResult<()> { + let MoveCmd { move_count, movement } = move_cmd; + for _ in 0..move_count { + let range = self.calc_range(&movement); + if range.start != self.cursor() { + self.cursor = range.start.max(0); + } else { + self.cursor = range.end.min(self.len()); + } + } + Ok(()) + } + pub fn exec_vi_moveverb(&mut self, verb: Verb, move_cmd: MoveCmd) -> ShResult<()> { + let MoveCmd { move_count, movement } = move_cmd; + match verb { + Verb::Delete => { + (0..move_count).for_each(|_| { + let range = self.calc_range(&movement); + }); + } + Verb::DeleteOne(anchor) => todo!(), + Verb::Change => todo!(), + Verb::Yank => todo!(), + Verb::ReplaceChar(_) => todo!(), + Verb::Substitute => todo!(), + Verb::ToggleCase => todo!(), + Verb::Undo => todo!(), + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => todo!(), + Verb::OverwriteMode => todo!(), + Verb::InsertMode => todo!(), + Verb::JoinLines => todo!(), + Verb::InsertChar(_) => todo!(), + Verb::Indent => todo!(), + Verb::Dedent => todo!(), + } + Ok(()) + } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum CharClass { + AlphaNum, + Symbol +} + +impl CharClass { + pub fn is_opposite(&self, ch: char) -> bool { + let opp_class = CharClass::from(ch); + opp_class != *self + } +} + +impl From for CharClass { + fn from(value: char) -> Self { + if value.is_alphanumeric() || value == '_' { + CharClass::AlphaNum + } else { + CharClass::Symbol + } + } +} + + pub fn strip_ansi_codes(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut chars = s.chars().peekable(); diff --git a/src/prompt/readline/linecmd.rs b/src/prompt/readline/linecmd.rs new file mode 100644 index 0000000..c8ed00d --- /dev/null +++ b/src/prompt/readline/linecmd.rs @@ -0,0 +1,364 @@ +// Credit to Rustyline for enumerating these editor commands +// https://github.com/kkawakam/rustyline + +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +use crate::prelude::*; + +pub type RepeatCount = u16; + +#[derive(Default, Debug, Clone, Eq, PartialEq)] +pub struct ViCmdBuilder { + verb_count: Option, + verb: Option, + move_count: Option, + movement: Option, +} + +impl ViCmdBuilder { + pub fn new() -> Self { + Self::default() + } + pub fn with_verb_count(self, verb_count: u16) -> Self { + let Self { verb_count: _, verb, move_count, movement } = self; + Self { verb_count: Some(verb_count), verb, move_count, movement } + } + pub fn with_verb(self, verb: Verb) -> Self { + let Self { verb_count, verb: _, move_count, movement } = self; + Self { verb_count, verb: Some(verb), move_count, movement } + } + pub fn with_move_count(self, move_count: u16) -> Self { + let Self { verb_count, verb, move_count: _, movement } = self; + Self { verb_count, verb, move_count: Some(move_count), movement } + } + pub fn with_movement(self, movement: Movement) -> Self { + let Self { verb_count, verb, move_count, movement: _ } = self; + Self { verb_count, verb, move_count, movement: Some(movement) } + } + pub fn append_digit(&mut self, digit: char) { + // Convert char digit to a number (assuming ASCII '0'..'9') + let digit_val = digit.to_digit(10).expect("digit must be 0-9") as u16; + + if self.verb.is_none() { + // Append to verb_count + self.verb_count = Some(match self.verb_count { + Some(count) => count * 10 + digit_val, + None => digit_val, + }); + } else { + // Append to move_count + self.move_count = Some(match self.move_count { + Some(count) => count * 10 + digit_val, + None => digit_val, + }); + } + } + pub fn is_unfinished(&self) -> bool { + (self.verb.is_none() && self.movement.is_none()) || + (self.verb.is_none() && self.movement.as_ref().is_some_and(|m| m.needs_verb())) || + (self.movement.is_none() && self.verb.as_ref().is_some_and(|v| v.needs_movement())) + } + pub fn build(self) -> ShResult { + if self.is_unfinished() { + flog!(ERROR, "Unfinished Builder: {:?}", self); + return Err( + ShErr::simple(ShErrKind::ReadlineErr, "called ViCmdBuilder::build() with an unfinished builder") + ) + } + let Self { verb_count, verb, move_count, movement } = self; + let verb_count = verb_count.unwrap_or(1); + let move_count = move_count.unwrap_or(if verb.is_none() { verb_count } else { 1 }); + let verb = verb.map(|v| VerbCmd { verb_count, verb: v }); + let movement = movement.map(|m| MoveCmd { move_count, movement: m }); + Ok(match (verb, movement) { + (Some(v), Some(m)) => ViCmd::MoveVerb(v, m), + (Some(v), None) => ViCmd::Verb(v), + (None, Some(m)) => ViCmd::Move(m), + (None, None) => unreachable!(), + }) + } +} + + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ViCmd { + MoveVerb(VerbCmd, MoveCmd), + Verb(VerbCmd), + Move(MoveCmd) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct VerbCmd { + pub verb_count: u16, + pub verb: Verb +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MoveCmd { + pub move_count: u16, + pub movement: Movement +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum Verb { + /// `d`, `D` — delete motion or line + Delete, + /// `x`, `X` — delete one char, forward or back + DeleteOne(Anchor), + /// `c`, `C` — change (delete + insert) + Change, + /// `y`, `Y` — yank (copy) + Yank, + /// `r` — replace a single character + ReplaceChar(char), + /// `s` or `S` — substitute (change + single char or line) + Substitute, + /// `~` — swap case + ToggleCase, + /// `u` — undo + Undo, + /// `.` — repeat last edit + RepeatLast, + /// `p`, `P` — paste + Put(Anchor), + /// `R` — overwrite characters + OverwriteMode, + /// `i`, `a`, `I`, `A`, `o`, `O` — insert/append text + InsertMode, + /// `J` — join lines + JoinLines, + InsertChar(char), + Indent, + Dedent +} + +impl Verb { + pub fn needs_movement(&self) -> bool { + match self { + Verb::DeleteOne(_) | + Verb::InsertMode | + Verb::JoinLines | + Verb::ToggleCase | + Verb::OverwriteMode | + Verb::Substitute | + Verb::Put(_) | + Verb::Undo | + Verb::RepeatLast | + Verb::Dedent | + Verb::Indent | + Verb::InsertChar(_) | + Verb::ReplaceChar(_) => false, + Verb::Delete | + Verb::Change | + Verb::Yank => true + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Movement { + /// Whole current line (not really a movement but a range) + WholeLine, + TextObj(TextObj, Bound), + BeginningOfFirstWord, + /// beginning-of-line + BeginningOfLine, + /// end-of-line + EndOfLine, + /// backward-word, vi-prev-word + BackwardWord(Word), // Backward until start of word + /// forward-word, vi-end-word, vi-next-word + ForwardWord(At, Word), // Forward until start/end of word + /// character-search, character-search-backward, vi-char-search + CharSearch(CharSearch), + /// vi-first-print + ViFirstPrint, + /// backward-char + BackwardChar, + /// forward-char + ForwardChar, + /// move to the same column on the previous line + LineUp, + /// move to the same column on the next line + LineDown, + /// Whole user input (not really a movement but a range) + WholeBuffer, + /// beginning-of-buffer + BeginningOfBuffer, + /// end-of-buffer + EndOfBuffer, + Null +} + +impl Movement { + pub fn needs_verb(&self) -> bool { + match self { + Self::WholeLine | + Self::BeginningOfLine | + Self::BeginningOfFirstWord | + Self::EndOfLine | + Self::BackwardWord(_) | + Self::ForwardWord(_, _) | + Self::CharSearch(_) | + Self::ViFirstPrint | + Self::BackwardChar | + Self::ForwardChar | + Self::LineUp | + Self::LineDown | + Self::WholeBuffer | + Self::BeginningOfBuffer | + Self::EndOfBuffer => false, + Self::Null | + Self::TextObj(_, _) => true + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TextObj { + /// `iw`, `aw` — inner word, around word + Word, + + /// `is`, `as` — inner sentence, around sentence + Sentence, + + /// `ip`, `ap` — inner paragraph, around paragraph + Paragraph, + + /// `i"`, `a"` — inner/around double quotes + DoubleQuote, + /// `i'`, `a'` + SingleQuote, + /// `i\``, `a\`` + BacktickQuote, + + /// `i)`, `a)` — round parens + Paren, + /// `i]`, `a]` + Bracket, + /// `i}`, `a}` + Brace, + /// `i<`, `a<` + Angle, + + /// `it`, `at` — HTML/XML tags (if you support it) + Tag, + + /// Custom user-defined objects maybe? + Custom(char), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Bound { + Inside, + Around +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum LineCmd { + Abort, + AcceptLine, + BeginningOfHistory, + CapitalizeWord, + ClearScreen, + Complete, + CompleteBackward, + CompleteHint, + DowncaseWord, + EndOfFile, + EndOfHistory, + ForwardSearchHistory, + HistorySearchBackward, + HistorySearchForward, + Insert(String), + Interrupt, + Move(Movement), + NextHistory, + Noop, + Overwrite(char), + PreviousHistory, + QuotedInsert, + Repaint, + ReverseSearchHistory, + Suspend, + TransposeChars, + TransposeWords, + YankPop, + LineUpOrPreviousHistory, + LineDownOrNextHistory, + Newline, + AcceptOrInsertLine { accept_in_the_middle: bool }, + /// 🧵 New: vi-style editing command + ViCmd(ViCmd), + /// unknown/unmapped key + Unknown, + Null, +} +impl LineCmd { + pub fn backspace() -> Self { + let cmd = ViCmdBuilder::new() + .with_verb(Verb::DeleteOne(Anchor::Before)) + .build() + .unwrap(); + Self::ViCmd(cmd) + } + const fn is_repeatable_change(&self) -> bool { + matches!( + *self, + Self::Insert(..) + | Self::ViCmd(..) + ) + } + + const fn is_repeatable(&self) -> bool { + match *self { + Self::Move(_) => true, + _ => self.is_repeatable_change(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +pub enum At { + Start, + BeforeEnd, + AfterEnd +} + +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +pub enum Anchor { + After, + Before +} + +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +pub enum CharSearch { + FindFwd(char), + FwdTo(char), + FindBkwd(char), + BkwdTo(char) +} + +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +pub enum Word { + Big, + Normal +} + + +const fn repeat_count(previous: RepeatCount, new: Option) -> RepeatCount { + match new { + Some(n) => n, + None => previous, + } +} + +#[derive(Default,Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord)] +pub enum InputMode { + Normal, + #[default] + Insert, + Visual, + Replace +} diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 8fe7c44..f0dd33d 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,94 +1,40 @@ use std::{arch::asm, os::fd::BorrowedFd}; +use keys::KeyEvent; use line::{strip_ansi_codes, LineBuf}; +use linecmd::{Anchor, At, InputMode, LineCmd, MoveCmd, Movement, Verb, VerbCmd, ViCmd, ViCmdBuilder, Word}; use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read}; use term::Terminal; use unicode_width::UnicodeWidthStr; -use crate::{libsh::{error::ShResult, sys::sh_quit}, prelude::*}; +use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit}, prelude::*}; pub mod term; pub mod line; - -#[derive(Clone,Copy,Debug)] -pub enum Key { - Char(char), - Enter, - Backspace, - Delete, - Esc, - Up, - Down, - Left, - Right, - Ctrl(char), - Unknown, -} - -#[derive(Clone,Debug)] -pub enum EditAction { - Return, - Exit(i32), - ClearTerm, - ClearLine, - Signal(i32), - MoveCursorStart, - MoveCursorEnd, - MoveCursorLeft, // Ctrl + B - MoveCursorRight, // Ctrl + F - DelWordBack, - DelFromCursor, - Backspace, // The Ctrl+H version - RedrawScreen, - HistNext, - HistPrev, - InsMode(InsAction), - NormMode(NormAction), -} - -#[derive(Clone,Debug)] -pub enum InsAction { - InsChar(char), - Backspace, // The backspace version - Delete, - Esc, - MoveLeft, // Left Arrow - MoveRight, // Right Arrow - MoveUp, - MoveDown -} - -#[derive(Clone,Debug)] -pub enum NormAction { - Count(usize), - Motion(Motion), -} - -#[derive(Clone,Debug)] -pub enum Motion { -} - -impl EditAction { - pub fn is_return(&self) -> bool { - matches!(self, Self::Return) - } -} - +pub mod keys; +pub mod linecmd; #[derive(Default,Debug)] pub struct FernReader { pub term: Terminal, pub prompt: String, pub line: LineBuf, - pub edit_mode: EditMode + pub edit_mode: InputMode, + pub count_arg: u16, + pub last_effect: Option, + pub last_movement: Option } impl FernReader { pub fn new(prompt: String) -> Self { + let line = LineBuf::new().with_initial("The quick brown fox jumped over the lazy dog."); Self { term: Terminal::new(), prompt, - line: Default::default(), - edit_mode: Default::default() + line, + edit_mode: Default::default(), + count_arg: Default::default(), + last_effect: Default::default(), + last_movement: Default::default(), } } fn pack_line(&mut self) -> String { @@ -100,167 +46,14 @@ impl FernReader { pub fn readline(&mut self) -> ShResult { self.display_line(/*refresh: */ false); loop { - let cmds = self.get_cmds(); - for cmd in &cmds { - if cmd.is_return() { - self.term.write_bytes(b"\r\n"); - return Ok(self.pack_line()) - } + let cmd = self.next_cmd()?; + if cmd == LineCmd::AcceptLine { + return Ok(self.pack_line()) } - self.process_cmds(cmds)?; + self.execute_cmd(cmd)?; self.display_line(/* refresh: */ true); } } - pub fn process_cmds(&mut self, cmds: Vec) -> ShResult<()> { - for cmd in cmds { - match cmd { - EditAction::Exit(code) => { - self.term.write_bytes(b"\r\n"); - sh_quit(code) - } - EditAction::ClearTerm => self.term.clear(), - EditAction::ClearLine => self.line.clear(), - EditAction::Signal(sig) => todo!(), - EditAction::MoveCursorStart => self.line.move_cursor_start(), - EditAction::MoveCursorEnd => self.line.move_cursor_end(), - EditAction::MoveCursorLeft => self.line.move_cursor_left(), - EditAction::MoveCursorRight => self.line.move_cursor_right(), - EditAction::DelWordBack => self.line.del_word_back(), - EditAction::DelFromCursor => self.line.del_from_cursor(), - EditAction::Backspace => self.line.backspace_at_cursor(), - EditAction::RedrawScreen => self.term.clear(), - EditAction::HistNext => todo!(), - EditAction::HistPrev => todo!(), - EditAction::InsMode(ins_action) => self.process_ins_cmd(ins_action)?, - EditAction::NormMode(norm_action) => self.process_norm_cmd(norm_action)?, - EditAction::Return => unreachable!(), // handled earlier - } - } - - Ok(()) - } - pub fn process_ins_cmd(&mut self, cmd: InsAction) -> ShResult<()> { - match cmd { - InsAction::InsChar(ch) => self.line.insert_at_cursor(ch), - InsAction::Backspace => self.line.backspace_at_cursor(), - InsAction::Delete => self.line.del_at_cursor(), - InsAction::Esc => todo!(), - InsAction::MoveLeft => self.line.move_cursor_left(), - InsAction::MoveRight => self.line.move_cursor_right(), - InsAction::MoveUp => todo!(), - InsAction::MoveDown => todo!(), - } - Ok(()) - } - pub fn process_norm_cmd(&mut self, cmd: NormAction) -> ShResult<()> { - match cmd { - NormAction::Count(num) => todo!(), - NormAction::Motion(motion) => todo!(), - } - Ok(()) - } - pub fn get_cmds(&mut self) -> Vec { - match self.edit_mode { - EditMode::Normal => { - let keys = self.read_keys_normal_mode(); - self.process_keys_normal_mode(keys) - } - EditMode::Insert => { - let key = self.read_key().unwrap(); - self.process_key_insert_mode(key) - } - } - } - pub fn read_keys_normal_mode(&mut self) -> Vec { - todo!() - } - pub fn process_keys_normal_mode(&mut self, keys: Vec) -> Vec { - todo!() - } - pub fn process_key_insert_mode(&mut self, key: Key) -> Vec { - match key { - Key::Char(ch) => { - vec![EditAction::InsMode(InsAction::InsChar(ch))] - } - Key::Enter => { - vec![EditAction::Return] - } - Key::Backspace => { - vec![EditAction::InsMode(InsAction::Backspace)] - } - Key::Delete => { - vec![EditAction::InsMode(InsAction::Delete)] - } - Key::Esc => { - vec![EditAction::InsMode(InsAction::Esc)] - } - Key::Up => { - vec![EditAction::InsMode(InsAction::MoveUp)] - } - Key::Down => { - vec![EditAction::InsMode(InsAction::MoveDown)] - } - Key::Left => { - vec![EditAction::InsMode(InsAction::MoveLeft)] - } - Key::Right => { - vec![EditAction::InsMode(InsAction::MoveRight)] - } - Key::Ctrl(ctrl) => self.process_ctrl(ctrl), - Key::Unknown => unimplemented!("Unknown key received: {key:?}") - } - } - pub fn process_ctrl(&mut self, ctrl: char) -> Vec { - match ctrl { - 'D' => { - if self.line.buffer.is_empty() { - vec![EditAction::Exit(0)] - } else { - vec![EditAction::Return] - } - } - 'C' => { - vec![EditAction::ClearLine] - } - 'Z' => { - vec![EditAction::Signal(20)] // SIGTSTP - } - 'A' => { - vec![EditAction::MoveCursorStart] - } - 'E' => { - vec![EditAction::MoveCursorEnd] - } - 'B' => { - vec![EditAction::MoveCursorLeft] - } - 'F' => { - vec![EditAction::MoveCursorRight] - } - 'U' => { - vec![EditAction::ClearLine] - } - 'W' => { - vec![EditAction::DelWordBack] - } - 'K' => { - vec![EditAction::DelFromCursor] - } - 'H' => { - vec![EditAction::Backspace] - } - 'L' => { - vec![EditAction::RedrawScreen] - } - 'N' => { - vec![EditAction::HistNext] - } - 'P' => { - vec![EditAction::HistPrev] - } - _ => unimplemented!("Unhandled control character: {ctrl}") - } - } fn clear_line(&self) { let prompt_lines = self.prompt.lines().count(); let buf_lines = self.line.count_lines().saturating_sub(1); // One of the buffer's lines will overlap with the prompt. probably. @@ -291,53 +84,237 @@ impl FernReader { let cursor_offset = self.line.cursor() + last_line_len; self.term.write(&format!("\r\x1b[{}C", cursor_offset)); } - fn read_key(&mut self) -> Option { - let mut buf = [0; 4]; - - let n = self.term.read_byte(&mut buf); - if n == 0 { - return None; + pub fn next_cmd(&mut self) -> ShResult { + let vi_cmd = ViCmdBuilder::new(); + match self.edit_mode { + InputMode::Normal => self.get_normal_cmd(vi_cmd), + InputMode::Insert => self.get_insert_cmd(vi_cmd), + InputMode::Visual => todo!(), + InputMode::Replace => todo!(), } - match buf[0] { - b'\x1b' => { - if n == 3 { - match (buf[1], buf[2]) { - (b'[', b'A') => Some(Key::Up), - (b'[', b'B') => Some(Key::Down), - (b'[', b'C') => Some(Key::Right), - (b'[', b'D') => Some(Key::Left), - _ => { - flog!(WARN, "unhandled control seq: {},{}", buf[1] as char, buf[2] as char); - Some(Key::Esc) - } - } - } else if n == 4 { - match (buf[1], buf[2], buf[3]) { - (b'[', b'3', b'~') => Some(Key::Delete), - _ => { - flog!(WARN, "unhandled control seq: {},{},{}", buf[1] as char, buf[2] as char, buf[3] as char); - Some(Key::Esc) - } - } - } else { - Some(Key::Esc) + } + pub fn get_insert_cmd(&mut self, pending_cmd: ViCmdBuilder) -> ShResult { + use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; + let key = self.term.read_key(); + let cmd = match key { + E(K::Char(ch), M::NONE) => { + let cmd = pending_cmd + .with_verb(Verb::InsertChar(ch)) + .build()?; + LineCmd::ViCmd(cmd) + } + + E(K::Char('H'), M::CTRL) | + E(K::Backspace, M::NONE) => LineCmd::backspace(), + + E(K::BackTab, M::NONE) => LineCmd::CompleteBackward, + + E(K::Char('I'), M::CTRL) | + E(K::Tab, M::NONE) => LineCmd::Complete, + + E(K::Esc, M::NONE) => { + self.edit_mode = InputMode::Normal; + let cmd = pending_cmd + .with_movement(Movement::BackwardChar) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('D'), M::CTRL) => LineCmd::EndOfFile, + _ => { + flog!(INFO, "unhandled key in get_insert_cmd, trying common_cmd..."); + return self.common_cmd(key, pending_cmd) + } + }; + Ok(cmd) + } + + pub fn get_normal_cmd(&mut self, mut pending_cmd: ViCmdBuilder) -> ShResult { + use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; + let key = self.term.read_key(); + if let E(K::Char(digit @ '0'..='9'), M::NONE) = key { + pending_cmd.append_digit(digit); + return self.get_normal_cmd(pending_cmd); + } + let cmd = match key { + E(K::Char('h'), M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::BackwardChar) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('j'), M::NONE) => LineCmd::LineDownOrNextHistory, + E(K::Char('k'), M::NONE) => LineCmd::LineUpOrPreviousHistory, + E(K::Char('l'), M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::ForwardChar) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('w'), M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::ForwardWord(At::Start, Word::Normal)) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('W'), M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::ForwardWord(At::Start, Word::Big)) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('b'), M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::BackwardWord(Word::Normal)) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('B'), M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::BackwardWord(Word::Big)) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('x'), M::NONE) => { + let cmd = pending_cmd + .with_verb(Verb::DeleteOne(Anchor::After)) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('i'), M::NONE) => { + self.edit_mode = InputMode::Insert; + let cmd = pending_cmd + .with_movement(Movement::BackwardChar) + .build()?; + LineCmd::ViCmd(cmd) + } + E(K::Char('I'), M::NONE) => { + self.edit_mode = InputMode::Insert; + let cmd = pending_cmd + .with_movement(Movement::BeginningOfFirstWord) + .build()?; + LineCmd::ViCmd(cmd) + } + _ => { + flog!(INFO, "unhandled key in get_normal_cmd, trying common_cmd..."); + return self.common_cmd(key, pending_cmd) + } + }; + Ok(cmd) + } + + pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult { + use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; + match key { + E(K::Home, M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::BeginningOfLine) + .build()?; + Ok(LineCmd::ViCmd(cmd)) + } + E(K::End, M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::EndOfLine) + .build()?; + Ok(LineCmd::ViCmd(cmd)) + } + E(K::Left, M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::BackwardChar) + .build()?; + Ok(LineCmd::ViCmd(cmd)) + } + E(K::Right, M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::ForwardChar) + .build()?; + Ok(LineCmd::ViCmd(cmd)) + } + E(K::Delete, M::NONE) => { + let cmd = pending_cmd + .with_movement(Movement::ForwardChar) + .with_verb(Verb::Delete) + .build()?; + Ok(LineCmd::ViCmd(cmd)) + } + E(K::Backspace, M::NONE) | + E(K::Char('h'), M::CTRL) => { + Ok(LineCmd::backspace()) + } + E(K::Up, M::NONE) => Ok(LineCmd::LineUpOrPreviousHistory), + E(K::Down, M::NONE) => Ok(LineCmd::LineDownOrNextHistory), + E(K::Enter, M::NONE) => Ok(LineCmd::AcceptLine), + _ => Err(ShErr::simple(ShErrKind::ReadlineErr,format!("Unhandled common key event: {key:?}"))) + } + } + pub fn exec_vi_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { + match cmd { + ViCmd::MoveVerb(verb_cmd, move_cmd) => { + self.last_effect = Some(verb_cmd.clone()); + self.last_movement = Some(move_cmd.clone()); + let VerbCmd { verb_count, verb } = verb_cmd; + for _ in 0..verb_count { + self.line.exec_vi_cmd(Some(verb.clone()), Some(move_cmd.clone()))?; } } - b'\r' | b'\n' => Some(Key::Enter), - 0x7f => Some(Key::Backspace), - c if (c as char).is_ascii_control() => { - let ctrl = (c ^ 0x40) as char; - Some(Key::Ctrl(ctrl)) + ViCmd::Verb(verb_cmd) => { + self.last_effect = Some(verb_cmd.clone()); + let VerbCmd { verb_count, verb } = verb_cmd; + for _ in 0..verb_count { + self.line.exec_vi_cmd(Some(verb.clone()), None)?; + } + } + ViCmd::Move(move_cmd) => { + self.last_movement = Some(move_cmd.clone()); + self.line.exec_vi_cmd(None, Some(move_cmd))?; } - c => Some(Key::Char(c as char)) } + Ok(()) + } + pub fn execute_cmd(&mut self, cmd: LineCmd) -> ShResult<()> { + match cmd { + LineCmd::ViCmd(cmd) => self.exec_vi_cmd(cmd)?, + LineCmd::Abort => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::BeginningOfHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::CapitalizeWord => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::ClearScreen => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Complete => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::CompleteBackward => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::CompleteHint => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::DowncaseWord => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::EndOfFile => { + if self.line.buffer.is_empty() { + sh_quit(0); + } else { + self.line.clear(); + } + } + LineCmd::EndOfHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::ForwardSearchHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::HistorySearchBackward => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::HistorySearchForward => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Insert(_) => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Interrupt => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Move(_) => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::NextHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Noop => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Repaint => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Overwrite(ch) => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::PreviousHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::QuotedInsert => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::ReverseSearchHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Suspend => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::TransposeChars => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::TransposeWords => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Unknown => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::YankPop => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::LineUpOrPreviousHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::LineDownOrNextHistory => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Newline => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::AcceptOrInsertLine { .. } => todo!("Unhandled cmd: {cmd:?}"), + LineCmd::Null => { /* Pass */ } + _ => todo!("Unhandled cmd: {cmd:?}"), + } + Ok(()) } } -#[derive(Default,Debug)] -pub enum EditMode { - Normal, - #[default] - Insert, -} - diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 7398038..2dfe977 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,7 +1,7 @@ -use std::{arch::asm, os::fd::{BorrowedFd, RawFd}}; - -use nix::{libc::STDIN_FILENO, sys::termios, unistd::isatty}; +use std::os::fd::{BorrowedFd, RawFd}; +use nix::{libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}}; +use super::keys::{KeyCode, KeyEvent, ModKeys}; #[derive(Debug)] pub struct Terminal { @@ -11,92 +11,87 @@ pub struct Terminal { impl Terminal { pub fn new() -> Self { - assert!(isatty(0).unwrap()); + assert!(isatty(STDIN_FILENO).unwrap()); Self { - stdin: 0, + stdin: STDIN_FILENO, stdout: 1, } } + fn raw_mode() -> termios::Termios { - // Get the current terminal attributes - let orig_termios = unsafe { termios::tcgetattr(BorrowedFd::borrow_raw(STDIN_FILENO)).expect("Failed to get terminal attributes") }; - - // Make a mutable copy - let mut raw = orig_termios.clone(); - - // Apply raw mode flags - termios::cfmakeraw(&mut raw); - - // Set the attributes immediately - unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &raw) } - .expect("Failed to set terminal to raw mode"); - - // Return original attributes so they can be restored later - orig_termios + let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes"); + let mut raw = orig.clone(); + termios::cfmakeraw(&mut raw); + termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw) + .expect("Failed to set terminal to raw mode"); + orig } + pub fn restore_termios(termios: termios::Termios) { - unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &termios) } - .expect("Failed to restore terminal settings"); + termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &termios) + .expect("Failed to restore terminal settings"); } - pub fn with_raw_mode R,R>(func: F) -> R { + + pub fn with_raw_mode R, R>(func: F) -> R { let saved = Self::raw_mode(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func)); Self::restore_termios(saved); - match result { Ok(r) => r, - Err(e) => std::panic::resume_unwind(e) + Err(e) => std::panic::resume_unwind(e), } } + pub fn read_byte(&self, buf: &mut [u8]) -> usize { Self::with_raw_mode(|| { - let ret: usize; - unsafe { - let buf_ptr = buf.as_mut_ptr(); - let len = buf.len(); - asm! ( - "syscall", - in("rax") 0, - in("rdi") self.stdin, - in("rsi") buf_ptr, - in("rdx") len, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - ret + read(self.stdin, buf).expect("Failed to read from stdin") }) } + pub fn write_bytes(&self, buf: &[u8]) { Self::with_raw_mode(|| { - let _ret: usize; - unsafe { - let buf_ptr = buf.as_ptr(); - let len = buf.len(); - asm!( - "syscall", - in("rax") 1, - in("rdi") self.stdout, - in("rsi") buf_ptr, - in("rdx") len, - lateout("rax") _ret, - out("rcx") _, - out("r11") _, - ); - } + write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout"); }); } + + pub fn write(&self, s: &str) { self.write_bytes(s.as_bytes()); } + pub fn writeln(&self, s: &str) { self.write(s); self.write_bytes(b"\r\n"); } + pub fn clear(&self) { self.write_bytes(b"\x1b[2J\x1b[H"); } + + pub fn read_key(&self) -> KeyEvent { + let mut buf = [0;8]; + let n = self.read_byte(&mut buf); + + if buf[0] == 0x1b { + if n >= 3 && buf[1] == b'[' { + return match buf[2] { + b'A' => KeyEvent(KeyCode::Up, ModKeys::empty()), + b'B' => KeyEvent(KeyCode::Down, ModKeys::empty()), + b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()), + b'D' => KeyEvent(KeyCode::Left, ModKeys::empty()), + _ => KeyEvent(KeyCode::Esc, ModKeys::empty()), + }; + } + return KeyEvent(KeyCode::Esc, ModKeys::empty()); + } + + if let Ok(s) = core::str::from_utf8(&buf[..n]) { + if let Some(ch) = s.chars().next() { + return KeyEvent::new(ch, ModKeys::NONE); + } + } + KeyEvent(KeyCode::Null, ModKeys::empty()) + } } impl Default for Terminal {