migrated polling logic from virtterm branch to main

This commit is contained in:
2026-02-16 18:28:57 -05:00
parent d04dd4bc1e
commit 142194c100
7 changed files with 537 additions and 176 deletions

107
Cargo.lock generated
View File

@@ -61,6 +61,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.10.0"
@@ -149,6 +155,29 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "env_filter"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@@ -171,13 +200,16 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"clap", "clap",
"env_logger",
"glob", "glob",
"insta", "insta",
"log",
"nix", "nix",
"pretty_assertions", "pretty_assertions",
"regex", "regex",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
"vte",
] ]
[[package]] [[package]]
@@ -222,6 +254,30 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "jiff"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.180" version = "0.2.180"
@@ -234,6 +290,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"
@@ -264,6 +326,21 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "1.4.1" version = "1.4.1"
@@ -340,6 +417,26 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.7.0" version = "2.7.0"
@@ -400,6 +497,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vte"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
dependencies = [
"arrayvec",
"memchr",
]
[[package]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.2+wasi-0.2.9" version = "1.0.2+wasi-0.2.9"

View File

@@ -12,11 +12,14 @@ debug = true
[dependencies] [dependencies]
bitflags = "2.8.0" bitflags = "2.8.0"
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"] }
env_logger = "0.11.9"
glob = "0.3.2" glob = "0.3.2"
log = "0.4.29"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] }
regex = "1.11.1" regex = "1.11.1"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"
vte = "0.15"
[dev-dependencies] [dev-dependencies]
insta = "1.42.2" insta = "1.42.2"

View File

@@ -795,14 +795,32 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
} }
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
let status = waitpid(child, Some(WtFlag::WSTOPPED))?;
// Read output first (before waiting) to avoid deadlock if child fills pipe buffer
flog!(DEBUG, "filling buffer");
loop {
match io_buf.fill_buffer() {
Ok(()) => break,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
flog!(DEBUG, "done");
// Wait for child with EINTR retry
let status = loop {
match waitpid(child, Some(WtFlag::WSTOPPED)) {
Ok(status) => break status,
Err(Errno::EINTR) => continue,
Err(e) => return Err(e.into()),
}
};
// Reclaim terminal foreground in case child changed it // Reclaim terminal foreground in case child changed it
crate::jobs::take_term()?; crate::jobs::take_term()?;
match status { match status {
WtStat::Exited(_, _) => { WtStat::Exited(_, _) => {
flog!(DEBUG, "filling buffer");
io_buf.fill_buffer()?;
flog!(DEBUG, "done");
Ok(io_buf.as_str()?.trim_end().to_string()) Ok(io_buf.as_str()?.trim_end().to_string())
} }
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")), _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),

View File

@@ -18,18 +18,25 @@ pub mod state;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
use std::os::fd::BorrowedFd;
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::libsh::error::ShErrKind; use nix::libc::STDIN_FILENO;
use crate::libsh::sys::TermiosGuard; use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::unistd::read;
use nix::errno::Errno;
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::prompt::get_prompt;
use crate::prompt::readline::term::raw_mode;
use crate::prompt::readline::{FernVi, ReadlineEvent};
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{source_rc, write_meta}; use crate::state::{source_rc, write_meta};
use clap::Parser; use clap::Parser;
use shopt::FernEditMode; use state::{read_vars, write_vars};
use state::{read_vars, write_shopts, write_vars};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FernArgs { struct FernArgs {
@@ -57,6 +64,7 @@ fn kickstart_lazy_evals() {
} }
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
let args = FernArgs::parse(); let args = FernArgs::parse();
if args.version { if args.version {
@@ -64,26 +72,28 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args); run_script(path, args.script_args)
} else { } else {
fern_interactive(); fern_interactive()
} } {
eprintln!("fern: {e}");
};
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display()); eprintln!("fern: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return; return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found"));
} }
let Ok(input) = fs::read_to_string(path) else { let Ok(input) = fs::read_to_string(path) else {
eprintln!("fern: Failed to read input file: {}", path.display()); eprintln!("fern: Failed to read input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return; return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file"));
}; };
write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string())); write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string()));
@@ -91,105 +101,125 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
} }
if let Err(e) = exec_input(input, None) { exec_input(input, None)
eprintln!("{e}");
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
}
_ => {
QUIT_CODE.store(1, Ordering::SeqCst);
}
}
}
} }
fn fern_interactive() { fn fern_interactive() -> ShResult<()> {
let _termios_guard = TermiosGuard::default(); // sets raw mode, restores termios on drop let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
let mut readline_err_count: u32 = 0; // Create readline instance with initial prompt
let mut readline = match FernVi::new(get_prompt().ok()) {
Ok(rl) => rl,
Err(e) => {
eprintln!("Failed to initialize readline: {e}");
QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed"));
}
};
// Initialize a new string, we will use this to store // Main poll loop
// partial line inputs when read() calls are interrupted by EINTR loop {
let mut partial_input = String::new(); // Handle any pending signals
while signals_pending() {
if let Err(e) = check_signals() {
match e.kind() {
ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt
readline.reset(get_prompt().ok());
}
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
}
_ => eprintln!("{e}"),
}
}
}
'outer: loop { readline.print_line()?;
while signals_pending() {
if let Err(e) = check_signals() { // Poll for stdin input
if let ShErrKind::ClearReadline = e.kind() { let mut fds = [PollFd::new(
partial_input.clear(); unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
if !signals_pending() { PollFlags::POLLIN,
continue 'outer; )];
}
}; match poll(&mut fds, PollTimeout::MAX) {
eprintln!("{e}"); Ok(_) => {}
} Err(Errno::EINTR) => {
} // Interrupted by signal, loop back to handle it
// Main loop continue;
let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
.unwrap()
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
.unwrap();
let input = match prompt::readline(edit_mode, Some(&partial_input)) {
Ok(line) => {
readline_err_count = 0;
partial_input.clear();
line
} }
Err(e) => { Err(e) => {
match e.kind() { eprintln!("poll error: {e}");
ShErrKind::ReadlineIntr(partial) => { break;
// Did we get signaled? Check signal flags }
// If nothing to worry about, retry the readline with the unfinished input }
while signals_pending() {
if let Err(e) = check_signals() { // Check if stdin has data
if let ShErrKind::ClearReadline = e.kind() { if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
partial_input.clear(); let mut buffer = [0u8; 1024];
if !signals_pending() { match read(STDIN_FILENO, &mut buffer) {
continue 'outer; Ok(0) => {
} // EOF
}; break;
eprintln!("{e}"); }
} Ok(n) => {
} readline.feed_bytes(&buffer[..n]);
partial_input = partial.to_string(); }
continue; Err(Errno::EINTR) => {
} // Interrupted, continue to handle signals
ShErrKind::CleanExit(code) => { continue;
QUIT_CODE.store(*code, Ordering::SeqCst); }
return; Err(e) => {
} eprintln!("read error: {e}");
_ => { break;
eprintln!("{e}"); }
readline_err_count += 1; }
if readline_err_count == 20 { }
eprintln!("reached maximum readline error count, exiting");
break; // Process any available input
} else { match readline.process_input() {
continue; Ok(ReadlineEvent::Line(input)) => {
} write_meta(|m| m.start_timer());
} if let Err(e) = exec_input(input, None) {
} match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
}
_ => eprintln!("{e}"),
}
}
write_meta(|m| m.stop_timer());
// Reset for next command with fresh prompt
readline.reset(get_prompt().ok());
}
Ok(ReadlineEvent::Eof) => {
// Ctrl+D on empty line
QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(());
}
Ok(ReadlineEvent::Pending) => {
// No complete input yet, keep polling
}
Err(e) => {
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
}
_ => eprintln!("{e}"),
}
} }
};
write_meta(|m| m.start_timer());
if let Err(e) = exec_input(input, None) {
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return;
}
_ => {
eprintln!("{e}");
}
}
} }
write_meta(|m| m.stop_timer());
} }
Ok(())
} }

View File

@@ -3,14 +3,10 @@ pub mod readline;
pub mod statusline; pub mod statusline;
use readline::{FernVi, Readline}; use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
use crate::{
expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode,
};
/// Initialize the line editor /// Initialize the line editor
fn get_prompt() -> ShResult<String> { pub fn get_prompt() -> ShResult<String> {
let Ok(prompt) = env::var("PS1") else { let Ok(prompt) = env::var("PS1") else {
// default prompt expands to: // default prompt expands to:
// //
@@ -26,18 +22,3 @@ fn get_prompt() -> ShResult<String> {
expand_prompt(&sanitized) 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()
}

View File

@@ -2,12 +2,12 @@ use history::History;
use keys::{KeyCode, KeyEvent, ModKeys}; use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode}; use linebuf::{LineBuf, SelectAnchor, SelectMode};
use nix::libc::STDOUT_FILENO; 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 vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::libsh::{ use crate::libsh::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErrKind, ShResult},
term::{Style, Styled}, term::{Style, Styled},
}; };
use crate::prelude::*; use crate::prelude::*;
@@ -21,15 +21,18 @@ pub mod term;
pub mod vicmd; pub mod vicmd;
pub mod vimode; pub mod vimode;
// Very useful for testing /// Non-blocking readline result
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 enum ReadlineEvent {
/// A complete line was entered
pub trait Readline { Line(String),
fn readline(&mut self) -> ShResult<String>; /// Ctrl+D on empty line - request to exit
Eof,
/// No complete input yet, need more bytes
Pending,
} }
pub struct FernVi { pub struct FernVi {
pub reader: Box<dyn KeyReader>, pub reader: PollReader,
pub writer: Box<dyn LineWriter>, pub writer: Box<dyn LineWriter>,
pub prompt: String, pub prompt: String,
pub mode: Box<dyn ViMode>, pub mode: Box<dyn ViMode>,
@@ -38,40 +41,76 @@ pub struct FernVi {
pub repeat_motion: Option<MotionCmd>, pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf, pub editor: LineBuf,
pub history: History, pub history: History,
needs_redraw: bool,
} }
impl Readline for FernVi { impl FernVi {
fn readline(&mut self) -> ShResult<String> { pub fn new(prompt: Option<String>) -> ShResult<Self> {
let raw_mode_guard = raw_mode(); // Restores termios state on drop let mut new = Self {
reader: PollReader::new(),
loop { writer: Box::new(TermWriter::new(STDOUT_FILENO)),
raw_mode_guard.disable_for(|| self.print_line())?; prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
mode: Box::new(ViInsert::new()),
let key = match self.reader.read_key() { old_layout: None,
Ok(Some(key)) => key, repeat_action: None,
Err(e) if matches!(e.kind(), ShErrKind::IoErr(std::io::ErrorKind::Interrupted)) => { repeat_motion: None,
flog!(DEBUG, "readline interrupted"); editor: LineBuf::new(),
let partial: String = self.editor.as_str().to_string(); history: History::new()?,
return Err(ShErr::simple(ShErrKind::ReadlineIntr(partial), "")); needs_redraw: true,
}
Err(_) | Ok(None) => {
flog!(DEBUG, "EOF detected");
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
}
}; };
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); flog!(DEBUG, key);
if self.should_accept_hint(&key) { if self.should_accept_hint(&key) {
self.editor.accept_hint(); self.editor.accept_hint();
self.history.update_pending_cmd(self.editor.as_str()); self.history.update_pending_cmd(self.editor.as_str());
self.needs_redraw = true;
continue; continue;
} }
let Some(mut cmd) = self.mode.handle_key(key) else { let Some(mut cmd) = self.mode.handle_key(key) else {
flog!(DEBUG, "got none??");
continue; continue;
}; };
flog!(DEBUG, cmd); flog!(DEBUG, cmd);
@@ -79,29 +118,30 @@ impl Readline for FernVi {
if self.should_grab_history(&cmd) { if self.should_grab_history(&cmd) {
self.scroll_history(cmd); self.scroll_history(cmd);
self.needs_redraw = true;
continue; continue;
} }
if cmd.should_submit() { 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(); let buf = self.editor.take_buf();
// Save command to history // Save command to history
self.history.push(buf.clone()); self.history.push(buf.clone());
if let Err(e) = self.history.save() { if let Err(e) = self.history.save() {
eprintln!("Failed to save history: {e}"); 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 cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() { if self.editor.buffer.is_empty() {
return Err(ShErr::simple(ShErrKind::CleanExit(0), "exit")); return Ok(ReadlineEvent::Eof);
} else { } else {
self.editor.buffer.clear(); self.editor.buffer.clear();
self.needs_redraw = true;
continue; continue;
} }
} }
flog!(DEBUG, cmd);
let before = self.editor.buffer.clone(); let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?; self.exec_cmd(cmd)?;
@@ -113,30 +153,17 @@ impl Readline for FernVi {
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
self.needs_redraw = true;
} }
}
}
impl FernVi { // Redraw if we processed any input
pub fn new(prompt: Option<String>) -> ShResult<Self> { if self.needs_redraw {
Ok(Self { self.print_line()?;
reader: Box::new(TermReader::new()), self.needs_redraw = false;
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()?,
})
}
pub fn with_initial(mut self, initial: &str) -> Self { Ok(ReadlineEvent::Pending)
self.editor = LineBuf::new().with_initial(initial, 0); }
self.history.update_pending_cmd(self.editor.as_str());
self
}
pub fn get_layout(&mut self) -> Layout { pub fn get_layout(&mut self) -> Layout {
let line = self.editor.to_string(); let line = self.editor.to_string();

View File

@@ -1,4 +1,5 @@
use std::{ use std::{
collections::VecDeque,
env, env,
fmt::{Debug, Write}, fmt::{Debug, Write},
io::{BufRead, BufReader, Read}, io::{BufRead, BufReader, Read},
@@ -14,6 +15,7 @@ use nix::{
}; };
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform};
use crate::prelude::*; use crate::prelude::*;
use crate::{ use crate::{
@@ -30,6 +32,8 @@ pub fn raw_mode() -> RawModeGuard {
termios::cfmakeraw(&mut raw); termios::cfmakeraw(&mut raw);
// Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals
raw.local_flags |= termios::LocalFlags::ISIG; 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( termios::tcsetattr(
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
termios::SetArg::TCSANOW, termios::SetArg::TCSANOW,
@@ -271,6 +275,8 @@ impl RawModeGuard {
termios::cfmakeraw(&mut raw); termios::cfmakeraw(&mut raw);
// Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals
raw.local_flags |= termios::LocalFlags::ISIG; 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"); termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode");
result 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 { pub struct TermReader {
buffer: BufReader<TermBuffer>, buffer: BufReader<TermBuffer>,
} }