migrated polling logic from virtterm branch to main
This commit is contained in:
@@ -3,14 +3,10 @@ pub mod readline;
|
||||
pub mod statusline;
|
||||
|
||||
|
||||
use readline::{FernVi, Readline};
|
||||
|
||||
use crate::{
|
||||
expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode,
|
||||
};
|
||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
||||
|
||||
/// Initialize the line editor
|
||||
fn get_prompt() -> ShResult<String> {
|
||||
pub fn get_prompt() -> ShResult<String> {
|
||||
let Ok(prompt) = env::var("PS1") else {
|
||||
// default prompt expands to:
|
||||
//
|
||||
@@ -26,18 +22,3 @@ fn get_prompt() -> ShResult<String> {
|
||||
|
||||
expand_prompt(&sanitized)
|
||||
}
|
||||
|
||||
pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult<String> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => {
|
||||
let mut fern_vi = FernVi::new(Some(prompt))?;
|
||||
if let Some(input) = initial {
|
||||
fern_vi = fern_vi.with_initial(input)
|
||||
}
|
||||
Box::new(fern_vi) as Box<dyn Readline>
|
||||
}
|
||||
FernEditMode::Emacs => todo!(), // idk if I'm ever gonna do this one actually, I don't use emacs
|
||||
};
|
||||
reader.readline()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ use history::History;
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||
use nix::libc::STDOUT_FILENO;
|
||||
use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter};
|
||||
use term::{get_win_size, KeyReader, Layout, LineWriter, PollReader, TermWriter};
|
||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
error::{ShErrKind, ShResult},
|
||||
term::{Style, Styled},
|
||||
};
|
||||
use crate::prelude::*;
|
||||
@@ -21,15 +21,18 @@ pub mod term;
|
||||
pub mod vicmd;
|
||||
pub mod vimode;
|
||||
|
||||
// Very useful for testing
|
||||
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis 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.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
|
||||
|
||||
pub trait Readline {
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
/// Non-blocking readline result
|
||||
pub enum ReadlineEvent {
|
||||
/// A complete line was entered
|
||||
Line(String),
|
||||
/// Ctrl+D on empty line - request to exit
|
||||
Eof,
|
||||
/// No complete input yet, need more bytes
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub struct FernVi {
|
||||
pub reader: Box<dyn KeyReader>,
|
||||
pub reader: PollReader,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
pub prompt: String,
|
||||
pub mode: Box<dyn ViMode>,
|
||||
@@ -38,40 +41,76 @@ pub struct FernVi {
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
pub history: History,
|
||||
needs_redraw: bool,
|
||||
}
|
||||
|
||||
impl Readline for FernVi {
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
let raw_mode_guard = raw_mode(); // Restores termios state on drop
|
||||
|
||||
loop {
|
||||
raw_mode_guard.disable_for(|| self.print_line())?;
|
||||
|
||||
let key = match self.reader.read_key() {
|
||||
Ok(Some(key)) => key,
|
||||
Err(e) if matches!(e.kind(), ShErrKind::IoErr(std::io::ErrorKind::Interrupted)) => {
|
||||
flog!(DEBUG, "readline interrupted");
|
||||
let partial: String = self.editor.as_str().to_string();
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineIntr(partial), ""));
|
||||
}
|
||||
Err(_) | Ok(None) => {
|
||||
flog!(DEBUG, "EOF detected");
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
let mut new = Self {
|
||||
reader: PollReader::new(),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new(),
|
||||
history: History::new()?,
|
||||
needs_redraw: true,
|
||||
};
|
||||
new.print_line()?;
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
}
|
||||
|
||||
/// Feed raw bytes from stdin into the reader's buffer
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
log::info!("Feeding bytes: {:?}", bytes.iter().map(|b| *b as char).collect::<String>());
|
||||
self.reader.feed_bytes(bytes);
|
||||
}
|
||||
|
||||
/// Mark that the display needs to be redrawn (e.g., after SIGWINCH)
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// Reset readline state for a new prompt
|
||||
pub fn reset(&mut self, prompt: Option<String>) {
|
||||
if let Some(p) = prompt {
|
||||
self.prompt = p;
|
||||
}
|
||||
self.editor.buffer.clear();
|
||||
self.editor.cursor = Default::default();
|
||||
self.old_layout = None;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// Process any available input and return readline event
|
||||
/// This is non-blocking - returns Pending if no complete line yet
|
||||
pub fn process_input(&mut self) -> ShResult<ReadlineEvent> {
|
||||
// Redraw if needed
|
||||
if self.needs_redraw {
|
||||
self.print_line()?;
|
||||
self.needs_redraw = false;
|
||||
}
|
||||
|
||||
// Process all available keys
|
||||
while let Some(key) = self.reader.read_key()? {
|
||||
flog!(DEBUG, key);
|
||||
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
flog!(DEBUG, "got none??");
|
||||
continue;
|
||||
};
|
||||
flog!(DEBUG, cmd);
|
||||
@@ -79,29 +118,30 @@ impl Readline for FernVi {
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd.should_submit() {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
self.writer.flush_write("\n")?;
|
||||
let buf = self.editor.take_buf();
|
||||
// Save command to history
|
||||
self.history.push(buf.clone());
|
||||
if let Err(e) = self.history.save() {
|
||||
eprintln!("Failed to save history: {e}");
|
||||
}
|
||||
return Ok(buf);
|
||||
return Ok(ReadlineEvent::Line(buf));
|
||||
}
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
return Err(ShErr::simple(ShErrKind::CleanExit(0), "exit"));
|
||||
return Ok(ReadlineEvent::Eof);
|
||||
} else {
|
||||
self.editor.buffer.clear();
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
flog!(DEBUG, cmd);
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
@@ -113,30 +153,17 @@ impl Readline for FernVi {
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
Ok(Self {
|
||||
reader: Box::new(TermReader::new()),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new(),
|
||||
history: History::new()?,
|
||||
})
|
||||
}
|
||||
// Redraw if we processed any input
|
||||
if self.needs_redraw {
|
||||
self.print_line()?;
|
||||
self.needs_redraw = false;
|
||||
}
|
||||
|
||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
}
|
||||
Ok(ReadlineEvent::Pending)
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.to_string();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
env,
|
||||
fmt::{Debug, Write},
|
||||
io::{BufRead, BufReader, Read},
|
||||
@@ -14,6 +15,7 @@ use nix::{
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
use vte::{Parser, Perform};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
@@ -30,6 +32,8 @@ pub fn raw_mode() -> RawModeGuard {
|
||||
termios::cfmakeraw(&mut raw);
|
||||
// Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals
|
||||
raw.local_flags |= termios::LocalFlags::ISIG;
|
||||
// Keep OPOST enabled so \n is translated to \r\n on output
|
||||
raw.output_flags |= termios::OutputFlags::OPOST;
|
||||
termios::tcsetattr(
|
||||
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
||||
termios::SetArg::TCSANOW,
|
||||
@@ -271,6 +275,8 @@ impl RawModeGuard {
|
||||
termios::cfmakeraw(&mut raw);
|
||||
// Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals
|
||||
raw.local_flags |= termios::LocalFlags::ISIG;
|
||||
// Keep OPOST enabled so \n is translated to \r\n on output
|
||||
raw.output_flags |= termios::OutputFlags::OPOST;
|
||||
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode");
|
||||
|
||||
result
|
||||
@@ -290,6 +296,195 @@ impl Drop for RawModeGuard {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PollReader - non-blocking key reader using vte parser
|
||||
// ============================================================================
|
||||
|
||||
struct KeyCollector {
|
||||
events: VecDeque<KeyEvent>,
|
||||
}
|
||||
|
||||
impl KeyCollector {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
events: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, event: KeyEvent) {
|
||||
self.events.push_back(event);
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<KeyEvent> {
|
||||
self.events.pop_front()
|
||||
}
|
||||
|
||||
/// Parse modifier bits from CSI parameter (e.g., 1;5A means Ctrl+Up)
|
||||
fn parse_modifiers(param: u16) -> ModKeys {
|
||||
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
||||
let bits = param.saturating_sub(1);
|
||||
let mut mods = ModKeys::empty();
|
||||
if bits & 1 != 0 { mods |= ModKeys::SHIFT; }
|
||||
if bits & 2 != 0 { mods |= ModKeys::ALT; }
|
||||
if bits & 4 != 0 { mods |= ModKeys::CTRL; }
|
||||
mods
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KeyCollector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform for KeyCollector {
|
||||
fn print(&mut self, c: char) {
|
||||
// vte routes 0x7f (DEL) to print instead of execute
|
||||
if c == '\x7f' {
|
||||
self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty()));
|
||||
} else {
|
||||
self.push(KeyEvent(KeyCode::Char(c), ModKeys::empty()));
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(&mut self, byte: u8) {
|
||||
let event = match byte {
|
||||
0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@
|
||||
0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I)
|
||||
0x0a => KeyEvent(KeyCode::Char('j'), ModKeys::CTRL), // Ctrl+J (linefeed)
|
||||
0x0d => KeyEvent(KeyCode::Enter, ModKeys::empty()), // Carriage return (Ctrl+M)
|
||||
0x1b => KeyEvent(KeyCode::Esc, ModKeys::empty()),
|
||||
0x7f => KeyEvent(KeyCode::Backspace, ModKeys::empty()),
|
||||
0x01..=0x1a => {
|
||||
// Ctrl+A through Ctrl+Z (excluding special cases above)
|
||||
let c = (b'A' + byte - 1) as char;
|
||||
KeyEvent(KeyCode::Char(c), ModKeys::CTRL)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
self.push(event);
|
||||
}
|
||||
|
||||
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) {
|
||||
let params: Vec<u16> = params.iter()
|
||||
.map(|p| p.first().copied().unwrap_or(0))
|
||||
.collect();
|
||||
|
||||
let event = match (intermediates, action) {
|
||||
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
||||
([], 'A') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Up, mods)
|
||||
}
|
||||
([], 'B') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Down, mods)
|
||||
}
|
||||
([], 'C') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Right, mods)
|
||||
}
|
||||
([], 'D') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Left, mods)
|
||||
}
|
||||
// Home/End: CSI H/F or CSI 1;mod H/F
|
||||
([], 'H') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Home, mods)
|
||||
}
|
||||
([], 'F') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::End, mods)
|
||||
}
|
||||
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
||||
([], '~') => {
|
||||
let key_num = params.first().copied().unwrap_or(0);
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let key = match key_num {
|
||||
1 | 7 => KeyCode::Home,
|
||||
2 => KeyCode::Insert,
|
||||
3 => KeyCode::Delete,
|
||||
4 | 8 => KeyCode::End,
|
||||
5 => KeyCode::PageUp,
|
||||
6 => KeyCode::PageDown,
|
||||
15 => KeyCode::F(5),
|
||||
17 => KeyCode::F(6),
|
||||
18 => KeyCode::F(7),
|
||||
19 => KeyCode::F(8),
|
||||
20 => KeyCode::F(9),
|
||||
21 => KeyCode::F(10),
|
||||
23 => KeyCode::F(11),
|
||||
24 => KeyCode::F(12),
|
||||
_ => return,
|
||||
};
|
||||
KeyEvent(key, mods)
|
||||
}
|
||||
// SGR mouse: CSI < button;x;y M/m (ignore mouse events for now)
|
||||
([b'<'], 'M') | ([b'<'], 'm') => {
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
self.push(event);
|
||||
}
|
||||
|
||||
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
|
||||
// SS3 sequences (ESC O P/Q/R/S for F1-F4)
|
||||
if intermediates == [b'O'] {
|
||||
let key = match byte {
|
||||
b'P' => KeyCode::F(1),
|
||||
b'Q' => KeyCode::F(2),
|
||||
b'R' => KeyCode::F(3),
|
||||
b'S' => KeyCode::F(4),
|
||||
_ => return,
|
||||
};
|
||||
self.push(KeyEvent(key, ModKeys::empty()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PollReader {
|
||||
parser: Parser,
|
||||
collector: KeyCollector,
|
||||
}
|
||||
|
||||
impl PollReader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
parser: Parser::new(),
|
||||
collector: KeyCollector::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
if bytes == [b'\x1b'] {
|
||||
// Single escape byte - user pressed ESC key
|
||||
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Feed all bytes through vte parser
|
||||
self.parser.advance(&mut self.collector, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PollReader {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyReader for PollReader {
|
||||
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
|
||||
Ok(self.collector.pop())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TermReader - blocking key reader (original implementation)
|
||||
// ============================================================================
|
||||
|
||||
pub struct TermReader {
|
||||
buffer: BufReader<TermBuffer>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user