Fully implemented vi-style editor commands
This commit is contained in:
@@ -325,6 +325,7 @@ pub enum ShErrKind {
|
|||||||
FuncReturn(i32),
|
FuncReturn(i32),
|
||||||
LoopContinue(i32),
|
LoopContinue(i32),
|
||||||
LoopBreak(i32),
|
LoopBreak(i32),
|
||||||
|
ReadlineErr,
|
||||||
Null
|
Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +346,7 @@ impl Display for ShErrKind {
|
|||||||
ShErrKind::FuncReturn(_) => "",
|
ShErrKind::FuncReturn(_) => "",
|
||||||
ShErrKind::LoopContinue(_) => "",
|
ShErrKind::LoopContinue(_) => "",
|
||||||
ShErrKind::LoopBreak(_) => "",
|
ShErrKind::LoopBreak(_) => "",
|
||||||
|
ShErrKind::ReadlineErr => "Line Read Error",
|
||||||
ShErrKind::Null => "",
|
ShErrKind::Null => "",
|
||||||
};
|
};
|
||||||
write!(f,"{output}")
|
write!(f,"{output}")
|
||||||
|
|||||||
111
src/prompt/readline/keys.rs
Normal file
111
src/prompt/readline/keys.rs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)]
|
#[derive(Default,Debug)]
|
||||||
pub struct LineBuf {
|
pub struct LineBuf {
|
||||||
@@ -9,6 +15,10 @@ impl LineBuf {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
pub fn with_initial<S: ToString>(mut self, init: S) -> Self {
|
||||||
|
self.buffer = init.to_string().chars().collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn count_lines(&self) -> usize {
|
pub fn count_lines(&self) -> usize {
|
||||||
self.buffer.iter().filter(|&&c| c == '\n').count()
|
self.buffer.iter().filter(|&&c| c == '\n').count()
|
||||||
}
|
}
|
||||||
@@ -19,6 +29,27 @@ impl LineBuf {
|
|||||||
self.buffer.clear();
|
self.buffer.clear();
|
||||||
self.cursor = 0;
|
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) {
|
pub fn insert_at_cursor(&mut self, ch: char) {
|
||||||
self.buffer.insert(self.cursor, ch);
|
self.buffer.insert(self.cursor, ch);
|
||||||
self.move_cursor_right();
|
self.move_cursor_right();
|
||||||
@@ -74,7 +105,279 @@ impl LineBuf {
|
|||||||
self.buffer.drain(start..end);
|
self.buffer.drain(start..end);
|
||||||
self.cursor = start;
|
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<F: Fn(usize) -> bool>(&self, mut start: usize, cond: F) -> usize {
|
||||||
|
while start > 0 && !cond(start) {
|
||||||
|
start -= 1;
|
||||||
|
}
|
||||||
|
start
|
||||||
|
}
|
||||||
|
fn forward_until<F: Fn(usize) -> 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<usize> {
|
||||||
|
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<Verb>, move_cmd: Option<MoveCmd>) -> 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<char> 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 {
|
pub fn strip_ansi_codes(s: &str) -> String {
|
||||||
let mut out = String::with_capacity(s.len());
|
let mut out = String::with_capacity(s.len());
|
||||||
|
|||||||
364
src/prompt/readline/linecmd.rs
Normal file
364
src/prompt/readline/linecmd.rs
Normal file
@@ -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<u16>,
|
||||||
|
verb: Option<Verb>,
|
||||||
|
move_count: Option<u16>,
|
||||||
|
movement: Option<Movement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ViCmd> {
|
||||||
|
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>) -> 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
|
||||||
|
}
|
||||||
@@ -1,94 +1,40 @@
|
|||||||
use std::{arch::asm, os::fd::BorrowedFd};
|
use std::{arch::asm, os::fd::BorrowedFd};
|
||||||
|
|
||||||
|
use keys::KeyEvent;
|
||||||
use line::{strip_ansi_codes, LineBuf};
|
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 nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read};
|
||||||
use term::Terminal;
|
use term::Terminal;
|
||||||
use unicode_width::UnicodeWidthStr;
|
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 term;
|
||||||
pub mod line;
|
pub mod line;
|
||||||
|
pub mod keys;
|
||||||
#[derive(Clone,Copy,Debug)]
|
pub mod linecmd;
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
#[derive(Default,Debug)]
|
||||||
pub struct FernReader {
|
pub struct FernReader {
|
||||||
pub term: Terminal,
|
pub term: Terminal,
|
||||||
pub prompt: String,
|
pub prompt: String,
|
||||||
pub line: LineBuf,
|
pub line: LineBuf,
|
||||||
pub edit_mode: EditMode
|
pub edit_mode: InputMode,
|
||||||
|
pub count_arg: u16,
|
||||||
|
pub last_effect: Option<VerbCmd>,
|
||||||
|
pub last_movement: Option<MoveCmd>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FernReader {
|
impl FernReader {
|
||||||
pub fn new(prompt: String) -> Self {
|
pub fn new(prompt: String) -> Self {
|
||||||
|
let line = LineBuf::new().with_initial("The quick brown fox jumped over the lazy dog.");
|
||||||
Self {
|
Self {
|
||||||
term: Terminal::new(),
|
term: Terminal::new(),
|
||||||
prompt,
|
prompt,
|
||||||
line: Default::default(),
|
line,
|
||||||
edit_mode: Default::default()
|
edit_mode: Default::default(),
|
||||||
|
count_arg: Default::default(),
|
||||||
|
last_effect: Default::default(),
|
||||||
|
last_movement: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn pack_line(&mut self) -> String {
|
fn pack_line(&mut self) -> String {
|
||||||
@@ -100,167 +46,14 @@ impl FernReader {
|
|||||||
pub fn readline(&mut self) -> ShResult<String> {
|
pub fn readline(&mut self) -> ShResult<String> {
|
||||||
self.display_line(/*refresh: */ false);
|
self.display_line(/*refresh: */ false);
|
||||||
loop {
|
loop {
|
||||||
let cmds = self.get_cmds();
|
let cmd = self.next_cmd()?;
|
||||||
for cmd in &cmds {
|
if cmd == LineCmd::AcceptLine {
|
||||||
if cmd.is_return() {
|
|
||||||
self.term.write_bytes(b"\r\n");
|
|
||||||
return Ok(self.pack_line())
|
return Ok(self.pack_line())
|
||||||
}
|
}
|
||||||
}
|
self.execute_cmd(cmd)?;
|
||||||
self.process_cmds(cmds)?;
|
|
||||||
self.display_line(/* refresh: */ true);
|
self.display_line(/* refresh: */ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn process_cmds(&mut self, cmds: Vec<EditAction>) -> 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<EditAction> {
|
|
||||||
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<Key> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
pub fn process_keys_normal_mode(&mut self, keys: Vec<Key>) -> Vec<EditAction> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
pub fn process_key_insert_mode(&mut self, key: Key) -> Vec<EditAction> {
|
|
||||||
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<EditAction> {
|
|
||||||
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) {
|
fn clear_line(&self) {
|
||||||
let prompt_lines = self.prompt.lines().count();
|
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.
|
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;
|
let cursor_offset = self.line.cursor() + last_line_len;
|
||||||
self.term.write(&format!("\r\x1b[{}C", cursor_offset));
|
self.term.write(&format!("\r\x1b[{}C", cursor_offset));
|
||||||
}
|
}
|
||||||
fn read_key(&mut self) -> Option<Key> {
|
pub fn next_cmd(&mut self) -> ShResult<LineCmd> {
|
||||||
let mut buf = [0; 4];
|
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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_insert_cmd(&mut self, pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
let n = self.term.read_byte(&mut buf);
|
E(K::Char('H'), M::CTRL) |
|
||||||
if n == 0 {
|
E(K::Backspace, M::NONE) => LineCmd::backspace(),
|
||||||
return None;
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
match buf[0] {
|
E(K::Char('D'), M::CTRL) => LineCmd::EndOfFile,
|
||||||
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);
|
flog!(INFO, "unhandled key in get_insert_cmd, trying common_cmd...");
|
||||||
Some(Key::Esc)
|
return self.common_cmd(key, pending_cmd)
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_normal_cmd(&mut self, mut pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
} 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);
|
flog!(INFO, "unhandled key in get_normal_cmd, trying common_cmd...");
|
||||||
Some(Key::Esc)
|
return self.common_cmd(key, pending_cmd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult<LineCmd> {
|
||||||
|
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()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
Some(Key::Esc)
|
self.line.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'\r' | b'\n' => Some(Key::Enter),
|
LineCmd::EndOfHistory => todo!("Unhandled cmd: {cmd:?}"),
|
||||||
0x7f => Some(Key::Backspace),
|
LineCmd::ForwardSearchHistory => todo!("Unhandled cmd: {cmd:?}"),
|
||||||
c if (c as char).is_ascii_control() => {
|
LineCmd::HistorySearchBackward => todo!("Unhandled cmd: {cmd:?}"),
|
||||||
let ctrl = (c ^ 0x40) as char;
|
LineCmd::HistorySearchForward => todo!("Unhandled cmd: {cmd:?}"),
|
||||||
Some(Key::Ctrl(ctrl))
|
LineCmd::Insert(_) => todo!("Unhandled cmd: {cmd:?}"),
|
||||||
}
|
LineCmd::Interrupt => todo!("Unhandled cmd: {cmd:?}"),
|
||||||
c => Some(Key::Char(c as char))
|
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{arch::asm, os::fd::{BorrowedFd, RawFd}};
|
use std::os::fd::{BorrowedFd, RawFd};
|
||||||
|
use nix::{libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}};
|
||||||
use nix::{libc::STDIN_FILENO, sys::termios, unistd::isatty};
|
|
||||||
|
|
||||||
|
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Terminal {
|
pub struct Terminal {
|
||||||
@@ -11,92 +11,87 @@ pub struct Terminal {
|
|||||||
|
|
||||||
impl Terminal {
|
impl Terminal {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
assert!(isatty(0).unwrap());
|
assert!(isatty(STDIN_FILENO).unwrap());
|
||||||
Self {
|
Self {
|
||||||
stdin: 0,
|
stdin: STDIN_FILENO,
|
||||||
stdout: 1,
|
stdout: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn raw_mode() -> termios::Termios {
|
fn raw_mode() -> termios::Termios {
|
||||||
// Get the current terminal attributes
|
let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes");
|
||||||
let orig_termios = unsafe { termios::tcgetattr(BorrowedFd::borrow_raw(STDIN_FILENO)).expect("Failed to get terminal attributes") };
|
let mut raw = orig.clone();
|
||||||
|
|
||||||
// Make a mutable copy
|
|
||||||
let mut raw = orig_termios.clone();
|
|
||||||
|
|
||||||
// Apply raw mode flags
|
|
||||||
termios::cfmakeraw(&mut raw);
|
termios::cfmakeraw(&mut raw);
|
||||||
|
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &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");
|
.expect("Failed to set terminal to raw mode");
|
||||||
|
orig
|
||||||
// Return original attributes so they can be restored later
|
|
||||||
orig_termios
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_termios(termios: termios::Termios) {
|
pub fn restore_termios(termios: termios::Termios) {
|
||||||
unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &termios) }
|
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &termios)
|
||||||
.expect("Failed to restore terminal settings");
|
.expect("Failed to restore terminal settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_raw_mode<F: FnOnce() -> R, R>(func: F) -> R {
|
pub fn with_raw_mode<F: FnOnce() -> R, R>(func: F) -> R {
|
||||||
let saved = Self::raw_mode();
|
let saved = Self::raw_mode();
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
||||||
Self::restore_termios(saved);
|
Self::restore_termios(saved);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(r) => r,
|
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 {
|
pub fn read_byte(&self, buf: &mut [u8]) -> usize {
|
||||||
Self::with_raw_mode(|| {
|
Self::with_raw_mode(|| {
|
||||||
let ret: usize;
|
read(self.stdin, buf).expect("Failed to read from stdin")
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_bytes(&self, buf: &[u8]) {
|
pub fn write_bytes(&self, buf: &[u8]) {
|
||||||
Self::with_raw_mode(|| {
|
Self::with_raw_mode(|| {
|
||||||
let _ret: usize;
|
write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout");
|
||||||
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") _,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn write(&self, s: &str) {
|
pub fn write(&self, s: &str) {
|
||||||
self.write_bytes(s.as_bytes());
|
self.write_bytes(s.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn writeln(&self, s: &str) {
|
pub fn writeln(&self, s: &str) {
|
||||||
self.write(s);
|
self.write(s);
|
||||||
self.write_bytes(b"\r\n");
|
self.write_bytes(b"\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
self.write_bytes(b"\x1b[2J\x1b[H");
|
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 {
|
impl Default for Terminal {
|
||||||
|
|||||||
Reference in New Issue
Block a user