copied rustyline's homework
This commit is contained in:
@@ -22,11 +22,11 @@ fn get_prompt() -> ShResult<String> {
|
||||
expand_prompt(&prompt)
|
||||
}
|
||||
|
||||
pub fn read_line(edit_mode: FernEditMode) -> ShResult<String> {
|
||||
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
|
||||
FernEditMode::Vi => Box::new(FernVi::new()),
|
||||
FernEditMode::Emacs => todo!()
|
||||
};
|
||||
reader.readline()
|
||||
reader.readline(Some(prompt))
|
||||
}
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}};
|
||||
|
||||
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}};
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::vicmd::Direction; // surprisingly useful
|
||||
|
||||
#[derive(Default,Clone,Copy,Debug)]
|
||||
pub enum SearchKind {
|
||||
Fuzzy,
|
||||
#[default]
|
||||
Prefix
|
||||
}
|
||||
|
||||
#[derive(Default,Clone,Debug)]
|
||||
pub struct SearchConstraint {
|
||||
kind: SearchKind,
|
||||
term: String,
|
||||
}
|
||||
|
||||
impl SearchConstraint {
|
||||
pub fn new(kind: SearchKind, term: String) -> Self {
|
||||
Self { kind, term }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct HistEntry {
|
||||
id: u32,
|
||||
timestamp: SystemTime,
|
||||
command: String,
|
||||
new: bool
|
||||
}
|
||||
|
||||
impl HistEntry {
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
pub fn timestamp(&self) -> &SystemTime {
|
||||
&self.timestamp
|
||||
}
|
||||
pub fn command(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
fn with_escaped_newlines(&self) -> String {
|
||||
let mut escaped = String::new();
|
||||
let mut chars = self.command.chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
escaped.push(ch);
|
||||
if let Some(ch) = chars.next() {
|
||||
escaped.push(ch)
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
escaped.push_str("\\\n");
|
||||
}
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
pub fn is_new(&self) -> bool {
|
||||
self.new
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HistEntry {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let err = Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] }
|
||||
);
|
||||
|
||||
//: 248972349;148;echo foo; echo bar
|
||||
let Some(cleaned) = s.strip_prefix(": ") else { return err };
|
||||
//248972349;148;echo foo; echo bar
|
||||
let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err };
|
||||
//("248972349","148;echo foo; echo bar")
|
||||
let Some((id,command)) = id_and_command.split_once(';') else { return err };
|
||||
//("148","echo foo; echo bar")
|
||||
let Ok(ts_seconds) = timestamp.parse::<u64>() else { return err };
|
||||
let Ok(id) = id.parse::<u32>() else { return err };
|
||||
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
|
||||
let command = command.to_string();
|
||||
Ok(Self { id, timestamp, command, new: false })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HistEntry {
|
||||
/// Similar to zsh's history format, but not entirely
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let command = self.with_escaped_newlines();
|
||||
let HistEntry { id, timestamp, command: _, new: _ } = self;
|
||||
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
writeln!(f, ": {timestamp};{id};{command}")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HistEntries(Vec<HistEntry>);
|
||||
|
||||
|
||||
impl FromStr for HistEntries {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut entries = vec![];
|
||||
|
||||
let mut lines = s.lines().enumerate().peekable();
|
||||
let mut cur_line = String::new();
|
||||
|
||||
while let Some((i,line)) = lines.next() {
|
||||
if !line.starts_with(": ") {
|
||||
return Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
|
||||
)
|
||||
}
|
||||
let mut chars = line.chars().peekable();
|
||||
let mut feeding_lines = true;
|
||||
while feeding_lines {
|
||||
feeding_lines = false;
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(esc_ch) = chars.next() {
|
||||
cur_line.push(esc_ch);
|
||||
} else {
|
||||
cur_line.push('\n');
|
||||
feeding_lines = true;
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
break
|
||||
}
|
||||
_ => {
|
||||
cur_line.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if feeding_lines {
|
||||
let Some((_,line)) = lines.next() else {
|
||||
return Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
|
||||
)
|
||||
};
|
||||
chars = line.chars().peekable();
|
||||
}
|
||||
}
|
||||
let entry = cur_line.parse::<HistEntry>()?;
|
||||
entries.push(entry);
|
||||
cur_line.clear();
|
||||
}
|
||||
|
||||
|
||||
Ok(Self(entries))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
||||
if !path.exists() {
|
||||
fs::File::create(path)?;
|
||||
}
|
||||
let raw = fs::read_to_string(path)?;
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
}
|
||||
|
||||
pub struct History {
|
||||
path: PathBuf,
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
cursor: usize,
|
||||
search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
max_size: Option<u32>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn new() -> ShResult<Self> {
|
||||
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
||||
let home = env::var("HOME").unwrap();
|
||||
format!("{home}/.fern_history")
|
||||
}));
|
||||
let mut entries = read_hist_file(&path)?;
|
||||
{
|
||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||
let timestamp = SystemTime::now();
|
||||
let command = "".into();
|
||||
entries.push(HistEntry { id, timestamp, command, new: true })
|
||||
}
|
||||
let search_mask = entries.clone();
|
||||
let cursor = entries.len() - 1;
|
||||
let mut new = Self {
|
||||
path,
|
||||
entries,
|
||||
search_mask,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups: true,
|
||||
max_size: None,
|
||||
};
|
||||
new.push_empty_entry(); // Current pending command
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[HistEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn push_empty_entry(&mut self) {
|
||||
}
|
||||
|
||||
pub fn cursor_entry(&self) -> Option<&HistEntry> {
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let Some(ent) = self.last_mut() else {
|
||||
return
|
||||
};
|
||||
let cmd = command.to_string();
|
||||
let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() };
|
||||
|
||||
|
||||
ent.command = cmd;
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
|
||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||
self.entries.last_mut()
|
||||
}
|
||||
|
||||
pub fn get_new_id(&self) -> u32 {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return 0
|
||||
};
|
||||
ent.id + 1
|
||||
}
|
||||
|
||||
pub fn ignore_dups(&mut self, yn: bool) {
|
||||
self.ignore_dups = yn
|
||||
}
|
||||
|
||||
pub fn max_hist_size(&mut self, size: Option<u32>) {
|
||||
self.max_size = size
|
||||
}
|
||||
|
||||
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
|
||||
let SearchConstraint { kind, term } = constraint;
|
||||
match kind {
|
||||
SearchKind::Prefix => {
|
||||
if term.is_empty() {
|
||||
self.search_mask = self.entries.clone();
|
||||
} else {
|
||||
let filtered = self.entries
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|ent| ent.command().starts_with(&term));
|
||||
|
||||
self.search_mask = filtered.collect();
|
||||
}
|
||||
self.cursor = self.search_mask.len().saturating_sub(1);
|
||||
}
|
||||
SearchKind::Fuzzy => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
let second_to_last = self.search_mask.len().checked_sub(2)?;
|
||||
self.search_mask.get(second_to_last)
|
||||
}
|
||||
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) {
|
||||
let entry = self.hint_entry()?;
|
||||
let prefix = self.cursor_entry()?.command();
|
||||
Some(entry.command().strip_prefix(prefix)?.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
let new_idx = self.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len().saturating_sub(1));
|
||||
let ent = self.search_mask.get(new_idx)?;
|
||||
|
||||
self.cursor = new_idx;
|
||||
|
||||
Some(ent)
|
||||
}
|
||||
|
||||
pub fn push(&mut self, command: String) {
|
||||
let timestamp = SystemTime::now();
|
||||
let id = self.get_new_id();
|
||||
if self.ignore_dups && self.is_dup(&command) {
|
||||
return
|
||||
}
|
||||
self.entries.push(HistEntry { id, timestamp, command, new: true });
|
||||
}
|
||||
|
||||
pub fn is_dup(&self, other: &str) -> bool {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return false
|
||||
};
|
||||
let ent_cmd = &ent.command;
|
||||
ent_cmd == other
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> ShResult<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)?;
|
||||
|
||||
let last_file_entry = self.entries
|
||||
.iter()
|
||||
.filter(|ent| !ent.new)
|
||||
.next_back()
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let entries = self.entries
|
||||
.iter_mut()
|
||||
.filter(|ent| {
|
||||
ent.new &&
|
||||
!ent.command.is_empty() &&
|
||||
if self.ignore_dups {
|
||||
ent.command() != last_file_entry
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
let mut data = String::new();
|
||||
for ent in entries {
|
||||
ent.new = false;
|
||||
write!(data, "{ent}").unwrap();
|
||||
}
|
||||
|
||||
file.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// 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: &str, mut mods: ModKeys) -> Self {
|
||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
|
||||
let first = match graphemes.next() {
|
||||
Some(g) => g,
|
||||
None => return E(K::Null, mods),
|
||||
};
|
||||
|
||||
// If more than one grapheme, it's not a single key event
|
||||
if graphemes.next().is_some() {
|
||||
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
|
||||
}
|
||||
|
||||
let mut chars = first.chars();
|
||||
|
||||
let single_char = chars.next();
|
||||
let is_single_char = chars.next().is_none();
|
||||
|
||||
match single_char {
|
||||
Some(c) if is_single_char && c.is_control() => {
|
||||
match c {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||
'\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),
|
||||
'\x08' => E(K::Backspace, mods),
|
||||
'\x09' => {
|
||||
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),
|
||||
'\x0b' => E(K::Char('K'), mods | M::CTRL),
|
||||
'\x0c' => E(K::Char('L'), mods | M::CTRL),
|
||||
'\x0d' => E(K::Enter, mods),
|
||||
'\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),
|
||||
'\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),
|
||||
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
||||
_ => E(K::Null, mods),
|
||||
}
|
||||
}
|
||||
Some(c) if is_single_char => {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Char(c), mods)
|
||||
}
|
||||
_ => {
|
||||
// multi-char grapheme (emoji, accented, etc)
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Grapheme(Arc::from(first)), mods)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum KeyCode {
|
||||
UnknownEscSeq,
|
||||
Backspace,
|
||||
BackTab,
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
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();
|
||||
}
|
||||
}
|
||||
0
src/prompt/readline/layout.rs
Normal file
0
src/prompt/readline/layout.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,462 +1,32 @@
|
||||
use std::time::Duration;
|
||||
use linebuf::LineBuf;
|
||||
use term::TermReader;
|
||||
|
||||
use history::{History, SearchConstraint, SearchKind};
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{strip_ansi_codes_and_escapes, LineBuf, SelectionAnchor, SelectionMode};
|
||||
use mode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
use term::Terminal;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
|
||||
use crate::libsh::error::ShResult;
|
||||
|
||||
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub mod keys;
|
||||
pub mod term;
|
||||
pub mod linebuf;
|
||||
pub mod vicmd;
|
||||
pub mod mode;
|
||||
pub mod register;
|
||||
pub mod history;
|
||||
pub mod layout;
|
||||
|
||||
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nmagna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\nconsequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||
|
||||
/*
|
||||
* Known issues:
|
||||
* If the line buffer scrolls past the terminal height, shit gets fucked
|
||||
* the cursor sometimes spazzes out during redraw, but ends up in the right place
|
||||
*/
|
||||
|
||||
/// Unified interface for different line editing methods
|
||||
pub trait Readline {
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
fn readline(&mut self, prompt: Option<String>) -> ShResult<String>;
|
||||
}
|
||||
|
||||
pub struct FernVi {
|
||||
term: Terminal,
|
||||
line: LineBuf,
|
||||
history: History,
|
||||
prompt: String,
|
||||
mode: Box<dyn ViMode>,
|
||||
last_action: Option<CmdReplay>,
|
||||
last_movement: Option<MotionCmd>,
|
||||
reader: TermReader,
|
||||
writer: TermWriter,
|
||||
editor: LineBuf
|
||||
}
|
||||
|
||||
impl Readline for FernVi {
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
/* a monument to the insanity of debugging this shit
|
||||
self.term.writeln("This is a line!");
|
||||
self.term.writeln("This is a line!");
|
||||
self.term.writeln("This is a line!");
|
||||
let prompt_thing = "prompt thing -> ";
|
||||
self.term.write(prompt_thing);
|
||||
let line = "And another!";
|
||||
let mut iters: usize = 0;
|
||||
let mut newlines_written = 0;
|
||||
loop {
|
||||
iters += 1;
|
||||
for i in 0..iters {
|
||||
self.term.writeln(line);
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
self.clear_lines(iters,prompt_thing.len() + 1);
|
||||
}
|
||||
panic!()
|
||||
*/
|
||||
self.print_buf(false)?;
|
||||
loop {
|
||||
let key = self.term.read_key();
|
||||
|
||||
if let KeyEvent(KeyCode::Char('V'), ModKeys::CTRL) = key {
|
||||
self.handle_verbatim()?;
|
||||
continue
|
||||
}
|
||||
if self.should_accept_hint(&key) {
|
||||
self.line.accept_hint();
|
||||
self.history.update_pending_cmd(self.line.as_str());
|
||||
self.print_buf(true)?;
|
||||
continue
|
||||
}
|
||||
|
||||
let Some(cmd) = self.mode.handle_key(key) else {
|
||||
continue
|
||||
};
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
flog!(DEBUG, "scrolling");
|
||||
self.scroll_history(cmd);
|
||||
self.print_buf(true)?;
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
|
||||
if cmd.should_submit() {
|
||||
self.term.unposition_cursor()?;
|
||||
self.term.write("\n");
|
||||
let command = std::mem::take(&mut self.line).pack_line();
|
||||
if !command.is_empty() {
|
||||
// We're just going to trim the command
|
||||
// reduces clutter in the case of two history commands whose only difference is insignificant whitespace
|
||||
self.history.update_pending_cmd(&command);
|
||||
self.history.save()?;
|
||||
}
|
||||
return Ok(command);
|
||||
}
|
||||
let line = self.line.to_string();
|
||||
self.exec_cmd(cmd.clone())?;
|
||||
let new_line = self.line.as_str();
|
||||
let has_changes = line != new_line;
|
||||
flog!(DEBUG, has_changes);
|
||||
|
||||
if has_changes {
|
||||
self.history.update_pending_cmd(self.line.as_str());
|
||||
}
|
||||
|
||||
self.print_buf(true)?;
|
||||
}
|
||||
fn readline(&mut self, prompt: Option<String>) -> ShResult<String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
|
||||
let line = LineBuf::new();//.with_initial(LOREM_IPSUM);
|
||||
let term = Terminal::new();
|
||||
let history = History::new()?;
|
||||
Ok(Self {
|
||||
term,
|
||||
line,
|
||||
history,
|
||||
prompt,
|
||||
mode: Box::new(ViInsert::new()),
|
||||
last_action: None,
|
||||
last_movement: None,
|
||||
})
|
||||
}
|
||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||
if self.line.at_end_of_buffer() && self.line.has_hint() {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Replace |
|
||||
ModeReport::Insert => {
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||
)
|
||||
}
|
||||
ModeReport::Visual |
|
||||
ModeReport::Normal => {
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||
) ||
|
||||
(
|
||||
self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() &&
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Char('l'), ModKeys::NONE)
|
||||
)
|
||||
)
|
||||
}
|
||||
_ => unimplemented!()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
/// Ctrl+V handler
|
||||
pub fn handle_verbatim(&mut self) -> ShResult<()> {
|
||||
let mut buf = [0u8; 8];
|
||||
let mut collected = Vec::new();
|
||||
|
||||
loop {
|
||||
let n = self.term.read_byte(&mut buf[..1]);
|
||||
if n == 0 {
|
||||
continue;
|
||||
}
|
||||
collected.push(buf[0]);
|
||||
|
||||
// If it starts with ESC, treat as escape sequence
|
||||
if collected[0] == 0x1b {
|
||||
loop {
|
||||
let n = self.term.peek_byte(&mut buf[..1]);
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
collected.push(buf[0]);
|
||||
// Ends a CSI sequence
|
||||
if (0x40..=0x7e).contains(&buf[0]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Ok(seq) = std::str::from_utf8(&collected) else {
|
||||
return Ok(())
|
||||
};
|
||||
let cmd = ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::Insert(seq.to_string()))),
|
||||
motion: None,
|
||||
raw_seq: seq.to_string(),
|
||||
};
|
||||
self.line.exec_cmd(cmd)?;
|
||||
}
|
||||
|
||||
// Optional: handle other edge cases, e.g., raw control codes
|
||||
if collected[0] < 0x20 || collected[0] == 0x7F {
|
||||
let ctrl_seq = std::str::from_utf8(&collected).unwrap();
|
||||
let cmd = ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::Insert(ctrl_seq.to_string()))),
|
||||
motion: None,
|
||||
raw_seq: ctrl_seq.to_string(),
|
||||
};
|
||||
self.line.exec_cmd(cmd)?;
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to parse as UTF-8 if it's a valid Unicode sequence
|
||||
if let Ok(s) = std::str::from_utf8(&collected) {
|
||||
if s.chars().count() == 1 {
|
||||
let ch = s.chars().next().unwrap();
|
||||
// You got a literal Unicode char
|
||||
eprintln!("Got char: {:?}", ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
|
||||
let constraint = SearchConstraint::new(SearchKind::Prefix, self.line.to_string());
|
||||
self.history.constrain_entries(constraint);
|
||||
}
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
flog!(DEBUG,count,motion);
|
||||
let entry = match motion {
|
||||
Motion::LineUp => {
|
||||
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
|
||||
return
|
||||
};
|
||||
flog!(DEBUG,"found entry");
|
||||
flog!(DEBUG,hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
Motion::LineDown => {
|
||||
let Some(hist_entry) = self.history.scroll(*count as isize) else {
|
||||
return
|
||||
};
|
||||
flog!(DEBUG,"found entry");
|
||||
flog!(DEBUG,hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
_ => unreachable!()
|
||||
};
|
||||
let col = self.line.saved_col().unwrap_or(self.line.cursor_column());
|
||||
let mut buf = LineBuf::new().with_initial(entry.command());
|
||||
let line_end = buf.end_of_line();
|
||||
if let Some(dest) = self.mode.hist_scroll_start_pos() {
|
||||
match dest {
|
||||
To::Start => {
|
||||
/* Already at 0 */
|
||||
}
|
||||
To::End => {
|
||||
// History entries cannot be empty
|
||||
// So this subtraction is safe (maybe)
|
||||
buf.cursor_fwd_to(line_end + 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let target = (col + 1).min(line_end + 1);
|
||||
buf.cursor_fwd_to(target);
|
||||
}
|
||||
|
||||
self.line = buf
|
||||
}
|
||||
|
||||
pub fn should_grab_history(&self, cmd: &ViCmd) -> bool {
|
||||
cmd.verb().is_none() &&
|
||||
(
|
||||
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUp))) &&
|
||||
self.line.start_of_line() == 0
|
||||
) ||
|
||||
(
|
||||
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown))) &&
|
||||
self.line.end_of_line() == self.line.byte_len()
|
||||
)
|
||||
}
|
||||
pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> {
|
||||
let (height,width) = self.term.get_dimensions()?;
|
||||
if refresh {
|
||||
self.term.unwrite()?;
|
||||
}
|
||||
let hint = self.history.get_hint();
|
||||
self.line.set_hint(hint);
|
||||
|
||||
let offset = self.calculate_prompt_offset();
|
||||
self.line.set_first_line_offset(offset);
|
||||
self.line.update_term_dims((height,width));
|
||||
let mut line_buf = self.prompt.clone();
|
||||
line_buf.push_str(&self.line.to_string());
|
||||
|
||||
self.term.recorded_write(&line_buf, offset)?;
|
||||
self.term.position_cursor(self.line.cursor_display_coords(width))?;
|
||||
|
||||
self.term.write(&self.mode.cursor_style());
|
||||
Ok(())
|
||||
}
|
||||
pub fn calculate_prompt_offset(&self) -> usize {
|
||||
if self.prompt.ends_with('\n') {
|
||||
return 0
|
||||
}
|
||||
strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width() + 1 // 1 indexed
|
||||
}
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||
Verb::Change |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::InsertMode => {
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
Verb::NormalMode => {
|
||||
Box::new(ViNormal::new())
|
||||
}
|
||||
Verb::ReplaceMode => {
|
||||
Box::new(ViReplace::new().with_count(count as u16))
|
||||
}
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.line.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
self.line.set_move_cursor_on_undo(self.mode.move_cursor_on_undo());
|
||||
self.term.write(&mode.cursor_style());
|
||||
return self.line.exec_cmd(cmd)
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
selecting = true;
|
||||
self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End));
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
flog!(DEBUG, self.mode.report_mode());
|
||||
flog!(DEBUG, mode.report_mode());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
|
||||
flog!(DEBUG, self.mode.report_mode());
|
||||
self.line.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
self.line.set_move_cursor_on_undo(self.mode.move_cursor_on_undo());
|
||||
self.term.write(&mode.cursor_style());
|
||||
|
||||
if mode.is_repeatable() {
|
||||
self.last_action = mode.as_replay();
|
||||
}
|
||||
self.line.exec_cmd(cmd)?;
|
||||
if selecting {
|
||||
self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End));
|
||||
} else {
|
||||
self.line.stop_selecting();
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.last_action.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let ViCmd { verb, .. } = cmd;
|
||||
let VerbCmd(count,_) = verb.unwrap();
|
||||
match replay {
|
||||
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.line.exec_cmd(cmd)?
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
if count > 1 {
|
||||
// Override the counts with the one passed to the '.' command
|
||||
if cmd.verb.is_some() {
|
||||
if let Some(v_mut) = cmd.verb.as_mut() {
|
||||
v_mut.0 = count
|
||||
}
|
||||
if let Some(m_mut) = cmd.motion.as_mut() {
|
||||
m_mut.0 = 1
|
||||
}
|
||||
} else {
|
||||
return Ok(()) // it has to have a verb to be repeatable, something weird happened
|
||||
}
|
||||
}
|
||||
self.line.exec_cmd(cmd)?;
|
||||
}
|
||||
_ => unreachable!("motions should be handled in the other branch")
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_motion_repeat() {
|
||||
match cmd.motion.as_ref().unwrap() {
|
||||
MotionCmd(count,Motion::RepeatMotion) => {
|
||||
let Some(motion) = self.last_movement.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(motion),
|
||||
raw_seq: format!("{count};")
|
||||
};
|
||||
return self.line.exec_cmd(repeat_cmd);
|
||||
}
|
||||
MotionCmd(count,Motion::RepeatMotionRev) => {
|
||||
let Some(motion) = self.last_movement.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let mut new_motion = motion.invert_char_motion();
|
||||
new_motion.0 = *count;
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(new_motion),
|
||||
raw_seq: format!("{count},")
|
||||
};
|
||||
return self.line.exec_cmd(repeat_cmd);
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.is_repeatable() {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to assign it here
|
||||
// in order to be able to repeat it
|
||||
let range = self.line.selected_range().unwrap();
|
||||
cmd.motion = Some(MotionCmd(1,Motion::Range(range.start, range.end)))
|
||||
}
|
||||
self.last_action = Some(CmdReplay::Single(cmd.clone()));
|
||||
}
|
||||
|
||||
if cmd.is_char_search() {
|
||||
self.last_movement = cmd.motion.clone()
|
||||
}
|
||||
|
||||
self.line.exec_cmd(cmd.clone())?;
|
||||
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
self.line.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,168 +0,0 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
||||
|
||||
pub fn read_register(ch: Option<char>) -> Option<String> {
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.buf().clone())
|
||||
}
|
||||
|
||||
pub fn write_register(ch: Option<char>, buf: String) {
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) }
|
||||
}
|
||||
|
||||
pub fn append_register(ch: Option<char>, buf: String) {
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) }
|
||||
}
|
||||
|
||||
#[derive(Default,Debug)]
|
||||
pub struct Registers {
|
||||
default: Register,
|
||||
a: Register,
|
||||
b: Register,
|
||||
c: Register,
|
||||
d: Register,
|
||||
e: Register,
|
||||
f: Register,
|
||||
g: Register,
|
||||
h: Register,
|
||||
i: Register,
|
||||
j: Register,
|
||||
k: Register,
|
||||
l: Register,
|
||||
m: Register,
|
||||
n: Register,
|
||||
o: Register,
|
||||
p: Register,
|
||||
q: Register,
|
||||
r: Register,
|
||||
s: Register,
|
||||
t: Register,
|
||||
u: Register,
|
||||
v: Register,
|
||||
w: Register,
|
||||
x: Register,
|
||||
y: Register,
|
||||
z: Register,
|
||||
}
|
||||
|
||||
impl Registers {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
default: Register(String::new()),
|
||||
a: Register(String::new()),
|
||||
b: Register(String::new()),
|
||||
c: Register(String::new()),
|
||||
d: Register(String::new()),
|
||||
e: Register(String::new()),
|
||||
f: Register(String::new()),
|
||||
g: Register(String::new()),
|
||||
h: Register(String::new()),
|
||||
i: Register(String::new()),
|
||||
j: Register(String::new()),
|
||||
k: Register(String::new()),
|
||||
l: Register(String::new()),
|
||||
m: Register(String::new()),
|
||||
n: Register(String::new()),
|
||||
o: Register(String::new()),
|
||||
p: Register(String::new()),
|
||||
q: Register(String::new()),
|
||||
r: Register(String::new()),
|
||||
s: Register(String::new()),
|
||||
t: Register(String::new()),
|
||||
u: Register(String::new()),
|
||||
v: Register(String::new()),
|
||||
w: Register(String::new()),
|
||||
x: Register(String::new()),
|
||||
y: Register(String::new()),
|
||||
z: Register(String::new()),
|
||||
}
|
||||
}
|
||||
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&self.default)
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&self.a),
|
||||
'b' => Some(&self.b),
|
||||
'c' => Some(&self.c),
|
||||
'd' => Some(&self.d),
|
||||
'e' => Some(&self.e),
|
||||
'f' => Some(&self.f),
|
||||
'g' => Some(&self.g),
|
||||
'h' => Some(&self.h),
|
||||
'i' => Some(&self.i),
|
||||
'j' => Some(&self.j),
|
||||
'k' => Some(&self.k),
|
||||
'l' => Some(&self.l),
|
||||
'm' => Some(&self.m),
|
||||
'n' => Some(&self.n),
|
||||
'o' => Some(&self.o),
|
||||
'p' => Some(&self.p),
|
||||
'q' => Some(&self.q),
|
||||
'r' => Some(&self.r),
|
||||
's' => Some(&self.s),
|
||||
't' => Some(&self.t),
|
||||
'u' => Some(&self.u),
|
||||
'v' => Some(&self.v),
|
||||
'w' => Some(&self.w),
|
||||
'x' => Some(&self.x),
|
||||
'y' => Some(&self.y),
|
||||
'z' => Some(&self.z),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&mut self.default)
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&mut self.a),
|
||||
'b' => Some(&mut self.b),
|
||||
'c' => Some(&mut self.c),
|
||||
'd' => Some(&mut self.d),
|
||||
'e' => Some(&mut self.e),
|
||||
'f' => Some(&mut self.f),
|
||||
'g' => Some(&mut self.g),
|
||||
'h' => Some(&mut self.h),
|
||||
'i' => Some(&mut self.i),
|
||||
'j' => Some(&mut self.j),
|
||||
'k' => Some(&mut self.k),
|
||||
'l' => Some(&mut self.l),
|
||||
'm' => Some(&mut self.m),
|
||||
'n' => Some(&mut self.n),
|
||||
'o' => Some(&mut self.o),
|
||||
'p' => Some(&mut self.p),
|
||||
'q' => Some(&mut self.q),
|
||||
'r' => Some(&mut self.r),
|
||||
's' => Some(&mut self.s),
|
||||
't' => Some(&mut self.t),
|
||||
'u' => Some(&mut self.u),
|
||||
'v' => Some(&mut self.v),
|
||||
'w' => Some(&mut self.w),
|
||||
'x' => Some(&mut self.x),
|
||||
'y' => Some(&mut self.y),
|
||||
'z' => Some(&mut self.z),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
pub struct Register(String);
|
||||
impl Register {
|
||||
pub fn buf(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
pub fn write(&mut self, buf: String) {
|
||||
self.0 = buf
|
||||
}
|
||||
pub fn append(&mut self, buf: String) {
|
||||
self.0.push_str(&buf)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,459 +1,463 @@
|
||||
use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
|
||||
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}};
|
||||
use nix::libc::{winsize, TIOCGWINSZ};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
use std::mem::zeroed;
|
||||
use std::io;
|
||||
use std::{env, fmt::Write, io::{BufRead, BufReader, Read}, ops::{Deref, DerefMut}, os::fd::{AsFd, BorrowedFd, RawFd}};
|
||||
|
||||
use crate::libsh::error::ShResult;
|
||||
use crate::prelude::*;
|
||||
use nix::{errno::Errno, libc, poll::{self, PollFlags, PollTimeout}, unistd::isatty};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
|
||||
#[derive(Default,Debug)]
|
||||
struct WriteMap {
|
||||
lines: usize,
|
||||
cols: usize,
|
||||
offset: usize
|
||||
use super::linebuf::LineBuf;
|
||||
|
||||
pub type Row = u16;
|
||||
pub type Col = u16;
|
||||
|
||||
#[derive(Default,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Debug)]
|
||||
pub struct Pos {
|
||||
col: Col,
|
||||
row: Row
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal {
|
||||
stdin: RawFd,
|
||||
stdout: RawFd,
|
||||
recording: bool,
|
||||
write_records: WriteMap,
|
||||
cursor_records: WriteMap
|
||||
// I'd like to thank rustyline for this idea
|
||||
nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize);
|
||||
|
||||
pub fn get_win_size(fd: RawFd) -> (Col,Row) {
|
||||
use std::mem::zeroed;
|
||||
|
||||
if cfg!(test) {
|
||||
return (80,24)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut size: libc::winsize = zeroed();
|
||||
match win_size(fd, &mut size) {
|
||||
Ok(0) => {
|
||||
/* rustyline code says:
|
||||
In linux pseudo-terminals are created with dimensions of
|
||||
zero. If host application didn't initialize the correct
|
||||
size before start we treat zero size as 80 columns and
|
||||
infinite rows
|
||||
*/
|
||||
let cols = if size.ws_col == 0 { 80 } else { size.ws_col };
|
||||
let rows = if size.ws_row == 0 {
|
||||
u16::MAX
|
||||
} else {
|
||||
size.ws_row
|
||||
};
|
||||
(cols.into(), rows.into())
|
||||
}
|
||||
_ => (80,24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn new() -> Self {
|
||||
assert!(isatty(STDIN_FILENO).unwrap());
|
||||
Self {
|
||||
stdin: STDIN_FILENO,
|
||||
stdout: 1,
|
||||
recording: false,
|
||||
// Records for buffer writes
|
||||
// Used to find the start of the buffer
|
||||
write_records: WriteMap::default(),
|
||||
// Records for cursor movements after writes
|
||||
// Used to find the end of the buffer
|
||||
cursor_records: WriteMap::default(),
|
||||
fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> {
|
||||
let mut bytes = buf.as_bytes();
|
||||
while !bytes.is_empty() {
|
||||
match nix::unistd::write(unsafe { BorrowedFd::borrow_raw(fd) }, bytes) {
|
||||
Ok(0) => return Err(Errno::EIO),
|
||||
Ok(n) => bytes = &bytes[n..],
|
||||
Err(Errno::EINTR) => {}
|
||||
Err(r) => return Err(r),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raw_mode() -> termios::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) {
|
||||
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &termios)
|
||||
.expect("Failed to restore terminal settings");
|
||||
}
|
||||
|
||||
|
||||
pub fn get_dimensions(&self) -> ShResult<(usize, usize)> {
|
||||
if !isatty(self.stdin).unwrap_or(false) {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Not a TTY"))?;
|
||||
}
|
||||
|
||||
let mut ws: winsize = unsafe { zeroed() };
|
||||
|
||||
let res = unsafe { libc::ioctl(self.stdin, TIOCGWINSZ, &mut ws) };
|
||||
if res == -1 {
|
||||
return Err(io::Error::last_os_error())?;
|
||||
}
|
||||
|
||||
Ok((ws.ws_row as usize, ws.ws_col as usize))
|
||||
}
|
||||
|
||||
pub fn start_recording(&mut self, offset: usize) {
|
||||
self.recording = true;
|
||||
self.write_records.offset = offset;
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
self.recording = false;
|
||||
}
|
||||
|
||||
pub fn save_cursor_pos(&mut self) {
|
||||
self.write("\x1b[s")
|
||||
}
|
||||
|
||||
pub fn restore_cursor_pos(&mut self) {
|
||||
self.write("\x1b[u")
|
||||
}
|
||||
|
||||
pub fn move_cursor_to(&mut self, (row,col): (usize,usize)) {
|
||||
self.write(&format!("\x1b[{row};{col}H",))
|
||||
}
|
||||
|
||||
pub fn with_raw_mode<F: FnOnce() -> 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),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_byte(&self, buf: &mut [u8]) -> usize {
|
||||
Self::with_raw_mode(|| {
|
||||
read(self.stdin, buf).expect("Failed to read from stdin")
|
||||
})
|
||||
}
|
||||
|
||||
fn read_blocks_then_read(&self, buf: &mut [u8], timeout: Duration) -> Option<usize> {
|
||||
Self::with_raw_mode(|| {
|
||||
self.read_blocks(false);
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match read(self.stdin, buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
self.read_blocks(true);
|
||||
return Some(n);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) if e == Errno::EAGAIN => {}
|
||||
Err(_) => return None,
|
||||
}
|
||||
if start.elapsed() > timeout {
|
||||
self.read_blocks(true);
|
||||
return None;
|
||||
}
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Same as read_byte(), only non-blocking with a very short timeout
|
||||
pub fn peek_byte(&self, buf: &mut [u8]) -> usize {
|
||||
const TIMEOUT_DUR: Duration = Duration::from_millis(50);
|
||||
Self::with_raw_mode(|| {
|
||||
self.read_blocks(false);
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match read(self.stdin, buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
self.read_blocks(true);
|
||||
return n
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(Errno::EAGAIN) => {}
|
||||
Err(e) => panic!("nonblocking read failed: {e}")
|
||||
}
|
||||
|
||||
if start.elapsed() >= TIMEOUT_DUR {
|
||||
self.read_blocks(true);
|
||||
return 0
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_blocks(&self, yn: bool) {
|
||||
let flags = OFlag::from_bits_truncate(fcntl(self.stdin, FcntlArg::F_GETFL).unwrap());
|
||||
let new_flags = if !yn {
|
||||
flags | OFlag::O_NONBLOCK
|
||||
// Big credit to rustyline for this
|
||||
fn width(s: &str, esc_seq: &mut u8) -> u16 {
|
||||
let w_calc = width_calculator();
|
||||
if *esc_seq == 1 {
|
||||
if s == "[" {
|
||||
// CSI
|
||||
*esc_seq = 2;
|
||||
} else {
|
||||
flags & !OFlag::O_NONBLOCK
|
||||
};
|
||||
fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap();
|
||||
}
|
||||
|
||||
pub fn reset_records(&mut self) {
|
||||
self.write_records = Default::default();
|
||||
self.cursor_records = Default::default();
|
||||
}
|
||||
|
||||
pub fn recorded_write(&mut self, buf: &str, offset: usize) -> ShResult<()> {
|
||||
self.start_recording(offset);
|
||||
self.write(buf);
|
||||
self.stop_recording();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewinds terminal writing, clears lines and lands on the anchor point of the prompt
|
||||
pub fn unwrite(&mut self) -> ShResult<()> {
|
||||
self.unposition_cursor()?;
|
||||
let WriteMap { lines, cols, offset } = self.write_records;
|
||||
for _ in 0..lines {
|
||||
self.write_unrecorded("\x1b[2K\x1b[A")
|
||||
// two-character sequence
|
||||
*esc_seq = 0;
|
||||
}
|
||||
let col = offset;
|
||||
self.write_unrecorded(&format!("\x1b[{col}G\x1b[0K"));
|
||||
self.reset_records();
|
||||
Ok(())
|
||||
0
|
||||
} else if *esc_seq == 2 {
|
||||
if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') {
|
||||
/*} else if s == "m" {
|
||||
// last
|
||||
*esc_seq = 0;*/
|
||||
} else {
|
||||
// not supported
|
||||
*esc_seq = 0;
|
||||
}
|
||||
|
||||
pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> {
|
||||
flog!(DEBUG,lines);
|
||||
flog!(DEBUG,col);
|
||||
self.cursor_records.lines = lines;
|
||||
self.cursor_records.cols = col;
|
||||
self.cursor_records.offset = self.cursor_pos().1;
|
||||
|
||||
for _ in 0..lines {
|
||||
self.write_unrecorded("\x1b[A")
|
||||
}
|
||||
|
||||
let (_, width) = self.get_dimensions().unwrap();
|
||||
// holy hack spongebob
|
||||
// basically if we've written to the edge of the terminal
|
||||
// and the cursor is at term_width + 1 (column 1 on the next line)
|
||||
// then we are going to manually write a newline
|
||||
// to position the cursor correctly
|
||||
if self.write_records.cols == width && self.cursor_records.cols == 1 {
|
||||
self.cursor_records.lines += 1;
|
||||
self.write_records.lines += 1;
|
||||
self.cursor_records.cols = 1;
|
||||
self.write_records.cols = 1;
|
||||
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, b"\n").expect("Failed to write to stdout");
|
||||
}
|
||||
|
||||
self.write_unrecorded(&format!("\x1b[{col}G"));
|
||||
|
||||
Ok(())
|
||||
0
|
||||
} else if s == "\x1b" {
|
||||
*esc_seq = 1;
|
||||
0
|
||||
} else if s == "\n" {
|
||||
0
|
||||
} else {
|
||||
w_calc.width(s) as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewinds cursor positioning, lands on the end of the buffer
|
||||
pub fn unposition_cursor(&mut self) ->ShResult<()> {
|
||||
let WriteMap { lines, cols, offset } = self.cursor_records;
|
||||
|
||||
for _ in 0..lines {
|
||||
self.write_unrecorded("\x1b[B")
|
||||
}
|
||||
|
||||
self.write_unrecorded(&format!("\x1b[{offset}G"));
|
||||
|
||||
Ok(())
|
||||
pub fn width_calculator() -> Box<dyn WidthCalculator> {
|
||||
match env::var("TERM_PROGRAM").as_deref() {
|
||||
Ok("Apple_Terminal") => Box::new(UnicodeWidth),
|
||||
Ok("iTerm.app") => Box::new(UnicodeWidth),
|
||||
Ok("WezTerm") => Box::new(UnicodeWidth),
|
||||
Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() {
|
||||
Ok("xterm-kitty") => Box::new(NoZwj),
|
||||
_ => Box::new(WcWidth)
|
||||
},
|
||||
_ => Box::new(WcWidth)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_bytes(&mut self, buf: &[u8], record: bool) {
|
||||
if self.recording && record { // The function parameter allows us to make sneaky writes while the terminal is recording
|
||||
let (_, width) = self.get_dimensions().unwrap();
|
||||
let mut bytes = buf.iter().map(|&b| b as char).peekable();
|
||||
while let Some(ch) = bytes.next() {
|
||||
match ch {
|
||||
'\n' => {
|
||||
self.write_records.lines += 1;
|
||||
self.write_records.cols = 0;
|
||||
}
|
||||
'\r' => {
|
||||
self.write_records.cols = 0;
|
||||
}
|
||||
// Consume escape sequences
|
||||
'\x1b' if bytes.peek() == Some(&'[') => {
|
||||
bytes.next();
|
||||
while let Some(&ch) = bytes.peek() {
|
||||
if ch.is_ascii_alphabetic() {
|
||||
bytes.next();
|
||||
break
|
||||
} else {
|
||||
bytes.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
'\t' => {
|
||||
let tab_size = 8;
|
||||
let next_tab = tab_size - (self.write_records.cols % tab_size);
|
||||
self.write_records.cols += next_tab;
|
||||
if self.write_records.cols > width {
|
||||
self.write_records.lines += 1;
|
||||
self.write_records.cols = 0;
|
||||
}
|
||||
}
|
||||
_ if ch.is_control() => {
|
||||
// ignore control characters for visual width
|
||||
}
|
||||
_ => {
|
||||
let ch_width = ch.width().unwrap_or(0);
|
||||
if self.write_records.cols + ch_width > width {
|
||||
flog!(DEBUG,ch_width,self.write_records.cols,width,self.write_records.lines);
|
||||
self.write_records.lines += 1;
|
||||
self.write_records.cols = ch_width;
|
||||
}
|
||||
self.write_records.cols += ch_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
flog!(DEBUG,self.write_records.cols);
|
||||
}
|
||||
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout");
|
||||
}
|
||||
|
||||
|
||||
pub fn write(&mut self, s: &str) {
|
||||
self.write_bytes(s.as_bytes(), true);
|
||||
}
|
||||
|
||||
pub fn write_unrecorded(&mut self, s: &str) {
|
||||
self.write_bytes(s.as_bytes(), false);
|
||||
}
|
||||
|
||||
pub fn writeln(&mut self, s: &str) {
|
||||
self.write(s);
|
||||
self.write_bytes(b"\n", true);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.write_bytes(b"\x1b[2J\x1b[H", false);
|
||||
}
|
||||
|
||||
pub fn read_key(&self) -> KeyEvent {
|
||||
use core::str;
|
||||
|
||||
let mut buf = [0u8; 8];
|
||||
let mut collected = Vec::with_capacity(5);
|
||||
|
||||
loop {
|
||||
let n = self.read_byte(&mut buf[..1]); // Read one byte at a time
|
||||
if n == 0 {
|
||||
fn read_digits_until(rdr: &mut TermReader, sep: char) -> ShResult<Option<u32>> {
|
||||
let mut num: u32 = 0;
|
||||
loop {
|
||||
match rdr.next_byte()? as char {
|
||||
digit @ '0'..='9' => {
|
||||
let digit = digit.to_digit(10).unwrap();
|
||||
num = append_digit(num, digit);
|
||||
continue;
|
||||
}
|
||||
collected.push(buf[0]);
|
||||
c if c == sep => break,
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(Some(num))
|
||||
}
|
||||
|
||||
// ESC sequences
|
||||
if collected[0] == 0x1b && collected.len() == 1 {
|
||||
if let Some(code) = self.parse_esc_seq(&mut buf) {
|
||||
return code
|
||||
pub fn append_digit(left: u32, right: u32) -> u32 {
|
||||
left.saturating_mul(10)
|
||||
.saturating_add(right)
|
||||
}
|
||||
|
||||
|
||||
pub trait WidthCalculator {
|
||||
fn width(&self, text: &str) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct UnicodeWidth;
|
||||
|
||||
impl WidthCalculator for UnicodeWidth {
|
||||
fn width(&self, text: &str) -> usize {
|
||||
text.width()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct WcWidth;
|
||||
|
||||
impl WcWidth {
|
||||
pub fn cwidth(&self, ch: char) -> usize {
|
||||
ch.width().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidthCalculator for WcWidth {
|
||||
fn width(&self, text: &str) -> usize {
|
||||
let mut width = 0;
|
||||
for ch in text.chars() {
|
||||
width += self.cwidth(ch)
|
||||
}
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
const ZWJ: char = '\u{200D}';
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct NoZwj;
|
||||
|
||||
impl WidthCalculator for NoZwj {
|
||||
fn width(&self, text: &str) -> usize {
|
||||
let mut width = 0;
|
||||
for slice in text.split(ZWJ) {
|
||||
width += UnicodeWidth.width(slice);
|
||||
}
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermBuffer {
|
||||
tty: RawFd
|
||||
}
|
||||
|
||||
impl TermBuffer {
|
||||
pub fn new(tty: RawFd) -> Self {
|
||||
assert!(isatty(tty).is_ok_and(|r| r == true));
|
||||
Self {
|
||||
tty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for TermBuffer {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
assert!(isatty(self.tty).is_ok_and(|r| r == true));
|
||||
loop {
|
||||
match nix::unistd::read(self.tty, buf) {
|
||||
Ok(n) => return Ok(n),
|
||||
Err(Errno::EINTR) => {}
|
||||
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermReader {
|
||||
buffer: BufReader<TermBuffer>
|
||||
}
|
||||
|
||||
impl TermReader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buffer: BufReader::new(TermBuffer::new(1))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, timeout: PollTimeout) -> ShResult<bool> {
|
||||
if self.buffer.buffer().len() > 0 {
|
||||
return Ok(true)
|
||||
}
|
||||
|
||||
let mut fds = [poll::PollFd::new(self.as_fd(),PollFlags::POLLIN)];
|
||||
let r = poll::poll(&mut fds, timeout);
|
||||
match r {
|
||||
Ok(n) => Ok(n != 0),
|
||||
Err(Errno::EINTR) => Ok(false),
|
||||
Err(e) => Err(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_byte(&mut self) -> std::io::Result<u8> {
|
||||
let mut buf = [0u8];
|
||||
self.buffer.read_exact(&mut buf)?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
pub fn peek_byte(&mut self) -> std::io::Result<u8> {
|
||||
let buf = self.buffer.fill_buf()?;
|
||||
if buf.is_empty() {
|
||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF"))
|
||||
} else {
|
||||
Ok(buf[0])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_byte(&mut self) {
|
||||
self.buffer.consume(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsFd for TermReader {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
let fd = self.buffer.get_ref().tty;
|
||||
unsafe { BorrowedFd::borrow_raw(fd) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Layout {
|
||||
pub w_calc: Box<dyn WidthCalculator>,
|
||||
pub prompt_end: Pos,
|
||||
pub cursor: Pos,
|
||||
pub end: Pos
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new() -> Self {
|
||||
let w_calc = width_calculator();
|
||||
Self {
|
||||
w_calc,
|
||||
prompt_end: Pos::default(),
|
||||
cursor: Pos::default(),
|
||||
end: Pos::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LineWriter {
|
||||
out: RawFd,
|
||||
t_cols: Col, // terminal width
|
||||
buffer: String,
|
||||
w_calc: Box<dyn WidthCalculator>,
|
||||
tab_stop: u16,
|
||||
}
|
||||
|
||||
impl LineWriter {
|
||||
pub fn new(out: RawFd) -> Self {
|
||||
let w_calc = width_calculator();
|
||||
let (t_cols,_) = get_win_size(out);
|
||||
Self {
|
||||
out,
|
||||
t_cols,
|
||||
buffer: String::new(),
|
||||
w_calc,
|
||||
tab_stop: 8 // TODO: add a way to configure this
|
||||
}
|
||||
}
|
||||
pub fn flush_write(&mut self, buf: &str) -> ShResult<()> {
|
||||
write_all(self.out, buf)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn clear_rows(&mut self, layout: &Layout) {
|
||||
let rows_to_clear = layout.end.row;
|
||||
let cursor_row = layout.cursor.row;
|
||||
|
||||
let cursor_motion = rows_to_clear.saturating_sub(cursor_row);
|
||||
if cursor_motion > 0 {
|
||||
write!(self.buffer, "\x1b[{cursor_motion}B").unwrap()
|
||||
}
|
||||
|
||||
for _ in 0..rows_to_clear {
|
||||
self.buffer.push_str("\x1b[K\x1b[A");
|
||||
}
|
||||
self.buffer.push_str("\x1b[K");
|
||||
}
|
||||
pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> {
|
||||
self.buffer.clear();
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
|
||||
|
||||
match new.row.cmp(&old.row) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
let shift = new.row - old.row;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[B"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}B").map_err(err)?
|
||||
}
|
||||
}
|
||||
|
||||
// Try parse valid UTF-8 from collected bytes
|
||||
if let Ok(s) = str::from_utf8(&collected) {
|
||||
return KeyEvent::new(s, ModKeys::empty());
|
||||
}
|
||||
|
||||
// If it's not valid UTF-8 yet, loop to collect more bytes
|
||||
if collected.len() >= 4 {
|
||||
// UTF-8 max char length is 4; if it's still invalid, give up
|
||||
break;
|
||||
std::cmp::Ordering::Less => {
|
||||
let shift = old.row - new.row;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[A"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}A").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||
}
|
||||
|
||||
KeyEvent(KeyCode::Null, ModKeys::empty())
|
||||
match new.col.cmp(&old.col) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
let shift = new.col - old.col;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[C"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}C").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
let shift = old.col - new.col;
|
||||
match shift {
|
||||
1 => self.buffer.push_str("\x1b[D"),
|
||||
_ => write!(self.buffer, "\x1b[{shift}D").map_err(err)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||
}
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_esc_seq(&self, buf: &mut [u8]) -> Option<KeyEvent> {
|
||||
let mut collected = vec![0x1b];
|
||||
pub fn redraw(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
line: &LineBuf,
|
||||
old_layout: &Layout,
|
||||
new_layout: &Layout,
|
||||
) -> ShResult<()> {
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
|
||||
self.buffer.clear();
|
||||
|
||||
// Peek next byte
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b1 = buf[0];
|
||||
collected.push(b1);
|
||||
self.clear_rows(old_layout);
|
||||
|
||||
match b1 {
|
||||
b'[' => {
|
||||
// Next byte(s) determine the sequence
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b2 = buf[0];
|
||||
collected.push(b2);
|
||||
let end = new_layout.end;
|
||||
let cursor = new_layout.cursor;
|
||||
|
||||
match b2 {
|
||||
b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())),
|
||||
b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())),
|
||||
b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())),
|
||||
b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())),
|
||||
b'1'..=b'9' => {
|
||||
// Might be Delete/Home/etc
|
||||
let mut digits = vec![b2];
|
||||
self.buffer.push_str(prompt);
|
||||
self.buffer.push_str(line.as_str());
|
||||
|
||||
// Keep reading until we hit `~` or `;` (modifiers)
|
||||
loop {
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b = buf[0];
|
||||
collected.push(b);
|
||||
if end.col == 0
|
||||
&& end.row > 0
|
||||
{
|
||||
// The line has wrapped. We need to use our own line break.
|
||||
self.buffer.push('\n')
|
||||
}
|
||||
|
||||
if b == b'~' {
|
||||
break;
|
||||
} else if b == b';' {
|
||||
// modifier-aware sequence, like `ESC [ 1 ; 5 ~`
|
||||
// You may want to parse the full thing
|
||||
break;
|
||||
} else if !b.is_ascii_digit() {
|
||||
break;
|
||||
} else {
|
||||
digits.push(b);
|
||||
}
|
||||
}
|
||||
let cursor_row_offset = end.row - cursor.row;
|
||||
|
||||
let key = match digits.as_slice() {
|
||||
[b'1'] => KeyCode::Home,
|
||||
[b'3'] => KeyCode::Delete,
|
||||
[b'4'] => KeyCode::End,
|
||||
[b'5'] => KeyCode::PageUp,
|
||||
[b'6'] => KeyCode::PageDown,
|
||||
[b'7'] => KeyCode::Home, // xterm alternate
|
||||
[b'8'] => KeyCode::End, // xterm alternate
|
||||
match cursor_row_offset {
|
||||
0 => { /* Do nothing */ }
|
||||
1 => self.buffer.push_str("\x1b[A"),
|
||||
_ => write!(self.buffer, "\x1b[{cursor_row_offset}A").map_err(err)?
|
||||
}
|
||||
|
||||
// Function keys
|
||||
[b'1',b'5'] => KeyCode::F(5),
|
||||
[b'1',b'7'] => KeyCode::F(6),
|
||||
[b'1',b'8'] => KeyCode::F(7),
|
||||
[b'1',b'9'] => KeyCode::F(8),
|
||||
[b'2',b'0'] => KeyCode::F(9),
|
||||
[b'2',b'1'] => KeyCode::F(10),
|
||||
[b'2',b'3'] => KeyCode::F(11),
|
||||
[b'2',b'4'] => KeyCode::F(12),
|
||||
_ => KeyCode::Esc,
|
||||
};
|
||||
let cursor_col = cursor.col;
|
||||
match cursor_col {
|
||||
0 => self.buffer.push('\r'),
|
||||
1 => self.buffer.push_str("\x1b[C"),
|
||||
_ => write!(self.buffer, "\x1b[{cursor_col}C").map_err(err)?
|
||||
}
|
||||
|
||||
Some(KeyEvent(key, ModKeys::empty()))
|
||||
}
|
||||
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
}
|
||||
b'O' => {
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b2 = buf[0];
|
||||
collected.push(b2);
|
||||
|
||||
let key = match b2 {
|
||||
b'P' => KeyCode::F(1),
|
||||
b'Q' => KeyCode::F(2),
|
||||
b'R' => KeyCode::F(3),
|
||||
b'S' => KeyCode::F(4),
|
||||
_ => KeyCode::Esc,
|
||||
};
|
||||
|
||||
Some(KeyEvent(key, ModKeys::empty()))
|
||||
}
|
||||
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&mut self) -> (usize, usize) {
|
||||
self.write_unrecorded("\x1b[6n");
|
||||
let mut buf = [0u8;32];
|
||||
let n = self.read_byte(&mut buf);
|
||||
|
||||
|
||||
let response = std::str::from_utf8(&buf[..n]).unwrap_or("");
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
if let Some(caps) = response.strip_prefix("\x1b[").and_then(|s| s.strip_suffix("R")) {
|
||||
let mut parts = caps.split(';');
|
||||
if let (Some(rowstr), Some(colstr)) = (parts.next(), parts.next()) {
|
||||
row = rowstr.parse().unwrap_or(1);
|
||||
col = colstr.parse().unwrap_or(1);
|
||||
pub fn calc_pos(&self, s: &str, orig: Pos) -> Pos {
|
||||
let mut pos = orig;
|
||||
let mut esc_seq = 0;
|
||||
for c in s.graphemes(true) {
|
||||
if c == "\n" {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
let c_width = if c == "\t" {
|
||||
self.tab_stop - (pos.col % self.tab_stop)
|
||||
} else {
|
||||
width(c, &mut esc_seq)
|
||||
};
|
||||
pos.col += c_width;
|
||||
if pos.col > self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = c_width;
|
||||
}
|
||||
}
|
||||
(row,col)
|
||||
}
|
||||
}
|
||||
if pos.col > self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
|
||||
impl Default for Terminal {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
pos
|
||||
}
|
||||
|
||||
pub fn update_t_cols(&mut self) {
|
||||
let (t_cols,_) = get_win_size(self.out);
|
||||
self.t_cols = t_cols;
|
||||
}
|
||||
|
||||
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader) -> ShResult<()> {
|
||||
if rdr.poll(PollTimeout::ZERO)? {
|
||||
// The terminals reply is going to be stuck behind the currently buffered output
|
||||
// So let's get out of here
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Ping the cursor's position
|
||||
self.flush_write("\x1b[6n")?;
|
||||
|
||||
// Excessively paranoid invariant checking
|
||||
if !rdr.poll(PollTimeout::from(100u8))?
|
||||
|| rdr.next_byte()? as char != '\x1b'
|
||||
|| rdr.next_byte()? as char != '['
|
||||
|| read_digits_until(rdr, ';')?.is_none() {
|
||||
// Invariant is broken, get out
|
||||
return Ok(())
|
||||
}
|
||||
// We just consumed everything up to the column number, so let's get that now
|
||||
let col = read_digits_until(rdr, 'R')?;
|
||||
|
||||
// The cursor is not at the leftmost, so let's fix that
|
||||
if col != Some(1) {
|
||||
// We use '\n' instead of '\r' because if there's a bunch of garbage on this line,
|
||||
// It might pollute the prompt/line buffer if those are shorter than said garbage
|
||||
self.flush_write("\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
use super::register::{append_register, read_register, write_register};
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct RegisterName {
|
||||
name: Option<char>,
|
||||
count: usize,
|
||||
append: bool
|
||||
}
|
||||
|
||||
impl RegisterName {
|
||||
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||
let Some(ch) = name else {
|
||||
return Self::default()
|
||||
};
|
||||
|
||||
let append = ch.is_uppercase();
|
||||
let name = ch.to_ascii_lowercase();
|
||||
Self {
|
||||
name: Some(name),
|
||||
count: count.unwrap_or(1),
|
||||
append
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> Option<char> {
|
||||
self.name
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
self.append
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
pub fn write_to_register(&self, buf: String) {
|
||||
if self.append {
|
||||
append_register(self.name, buf);
|
||||
} else {
|
||||
write_register(self.name, buf);
|
||||
}
|
||||
}
|
||||
pub fn read_from_register(&self) -> Option<String> {
|
||||
read_register(self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterName {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
count: 1,
|
||||
append: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
pub struct ViCmd {
|
||||
pub register: RegisterName,
|
||||
pub verb: Option<VerbCmd>,
|
||||
pub motion: Option<MotionCmd>,
|
||||
pub raw_seq: String,
|
||||
}
|
||||
|
||||
impl ViCmd {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||
self.motion = Some(motion)
|
||||
}
|
||||
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||
self.verb = Some(verb)
|
||||
}
|
||||
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||
self.verb.as_ref()
|
||||
}
|
||||
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||
self.motion.as_ref()
|
||||
}
|
||||
pub fn verb_count(&self) -> usize {
|
||||
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||
}
|
||||
pub fn motion_count(&self) -> usize {
|
||||
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
|
||||
}
|
||||
pub fn is_cmd_repeat(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast))
|
||||
}
|
||||
pub fn is_motion_repeat(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev))
|
||||
}
|
||||
pub fn is_char_search(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
|
||||
}
|
||||
pub fn should_submit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLine))
|
||||
}
|
||||
pub fn is_undo_op(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||
}
|
||||
pub fn is_line_motion(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown))
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(v.1,
|
||||
Verb::Change |
|
||||
Verb::InsertMode |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::NormalMode |
|
||||
Verb::VisualModeSelectLast |
|
||||
Verb::VisualMode |
|
||||
Verb::ReplaceMode
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct VerbCmd(pub usize,pub Verb);
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct MotionCmd(pub usize,pub Motion);
|
||||
|
||||
impl MotionCmd {
|
||||
pub fn invert_char_motion(self) -> Self {
|
||||
let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let new_dir = match dir {
|
||||
Direction::Forward => Direction::Backward,
|
||||
Direction::Backward => Direction::Forward,
|
||||
};
|
||||
MotionCmd(count,Motion::CharSearch(new_dir, dest, ch))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Verb {
|
||||
Delete,
|
||||
DeleteChar(Anchor),
|
||||
Change,
|
||||
Yank,
|
||||
ReplaceChar(char),
|
||||
Substitute,
|
||||
ToggleCase,
|
||||
ToLower,
|
||||
ToUpper,
|
||||
Complete,
|
||||
CompleteBackward,
|
||||
Undo,
|
||||
Redo,
|
||||
RepeatLast,
|
||||
Put(Anchor),
|
||||
ReplaceMode,
|
||||
InsertMode,
|
||||
InsertModeLineBreak(Anchor),
|
||||
NormalMode,
|
||||
VisualMode,
|
||||
VisualModeLine,
|
||||
VisualModeBlock, // dont even know if im going to implement this
|
||||
VisualModeSelectLast,
|
||||
SwapVisualAnchor,
|
||||
JoinLines,
|
||||
InsertChar(char),
|
||||
Insert(String),
|
||||
Breakline(Anchor),
|
||||
Indent,
|
||||
Dedent,
|
||||
Equalize,
|
||||
AcceptLine,
|
||||
Rot13, // lol
|
||||
Builder(VerbBuilder),
|
||||
EndOfFile
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum VerbBuilder {
|
||||
}
|
||||
|
||||
impl Verb {
|
||||
pub fn needs_motion(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Indent |
|
||||
Self::Dedent |
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::Yank
|
||||
)
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::DeleteChar(_) |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::Substitute |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::ToggleCase |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Breakline(_) |
|
||||
Self::Indent |
|
||||
Self::Dedent |
|
||||
Self::Equalize
|
||||
)
|
||||
}
|
||||
pub fn is_edit(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::DeleteChar(_) |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::Substitute |
|
||||
Self::ToggleCase |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::RepeatLast |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Breakline(_) |
|
||||
Self::Rot13 |
|
||||
Self::EndOfFile
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Change |
|
||||
Self::InsertChar(_) |
|
||||
Self::ReplaceChar(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Motion {
|
||||
WholeLine,
|
||||
TextObj(TextObj, Bound),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
BeginningOfLine,
|
||||
EndOfLine,
|
||||
BackwardWord(To, Word),
|
||||
ForwardWord(To, Word),
|
||||
CharSearch(Direction,Dest,char),
|
||||
BackwardChar,
|
||||
ForwardChar,
|
||||
LineUp,
|
||||
ScreenLineUp,
|
||||
LineDown,
|
||||
ScreenLineDown,
|
||||
BeginningOfScreenLine,
|
||||
FirstGraphicalOnScreenLine,
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn(usize),
|
||||
Range(usize,usize),
|
||||
Builder(MotionBuilder),
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum MotionBuilder {
|
||||
CharSearch(Option<Direction>,Option<Dest>,Option<char>),
|
||||
TextObj(Option<TextObj>,Option<Bound>)
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn needs_verb(&self) -> bool {
|
||||
matches!(self, Self::TextObj(_, _))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Anchor {
|
||||
After,
|
||||
Before
|
||||
}
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum TextObj {
|
||||
/// `iw`, `aw` — inner word, around word
|
||||
Word(Word),
|
||||
|
||||
/// for stuff like 'dd'
|
||||
Line,
|
||||
|
||||
/// `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
|
||||
Tag,
|
||||
|
||||
/// Custom user-defined objects maybe?
|
||||
Custom(char),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Word {
|
||||
Big,
|
||||
Normal
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Bound {
|
||||
Inside,
|
||||
Around
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
#[default]
|
||||
Forward,
|
||||
Backward
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Dest {
|
||||
On,
|
||||
Before,
|
||||
After
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum To {
|
||||
Start,
|
||||
End
|
||||
}
|
||||
Reference in New Issue
Block a user