shell: add basic tab completion (single-option)
This commit is contained in:
Generated
+4
@@ -2801,6 +2801,7 @@ dependencies = [
|
||||
"logsink",
|
||||
"nom",
|
||||
"runtime",
|
||||
"stuff",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
@@ -2984,6 +2985,9 @@ name = "stuff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cross",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -268,7 +268,7 @@ fn convert_key_event(raw: yggdrasil_abi::io::KeyboardKeyEvent) -> Option<Keyboar
|
||||
KeyboardKey::Delete => return None,
|
||||
KeyboardKey::Unknown => return None,
|
||||
KeyboardKey::CapsLock => return None,
|
||||
KeyboardKey::Tab => return None,
|
||||
KeyboardKey::Tab => Key::Tab,
|
||||
KeyboardKey::F(_) => return None,
|
||||
};
|
||||
|
||||
|
||||
@@ -308,6 +308,10 @@ impl Terminal {
|
||||
pty_master.write_all(&[termios.erase_char()]).ok();
|
||||
need_redraw = s.scroll_end();
|
||||
}
|
||||
(KeyModifiers::NONE, Key::Tab) => {
|
||||
pty_master.write_all(b"\t").ok();
|
||||
need_redraw = s.scroll_end();
|
||||
}
|
||||
(KeyModifiers::CTRL, Key::Char(ch)) if ch.is_ascii_lowercase() => {
|
||||
let byte = ch - 0x60;
|
||||
pty_master.write_all(&[byte]).unwrap();
|
||||
|
||||
@@ -20,4 +20,5 @@ pub enum Key {
|
||||
Home,
|
||||
End,
|
||||
Backspace,
|
||||
Tab,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cross.workspace = true
|
||||
|
||||
bytemuck.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod readline;
|
||||
pub mod tar;
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Stdout, Write},
|
||||
};
|
||||
|
||||
use cross::term::{TermKey, TerminalInput};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ReadlineError {
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
pub trait ReadlineProvider {
|
||||
fn prompt(&mut self, stdout: &mut Stdout);
|
||||
fn clear_screen(&mut self, stdout: &mut Stdout);
|
||||
fn completions(&mut self, raw_command: &str) -> Vec<String>;
|
||||
}
|
||||
|
||||
struct CompletionState {}
|
||||
|
||||
pub struct Readline<'a, P: ReadlineProvider = ()> {
|
||||
stdin: &'a mut TerminalInput,
|
||||
stdout: &'a mut Stdout,
|
||||
buffer: &'a mut String,
|
||||
provider: &'a mut P,
|
||||
|
||||
pos: usize,
|
||||
|
||||
completion: CompletionState,
|
||||
}
|
||||
|
||||
enum Outcome {
|
||||
Interrupt,
|
||||
Clear,
|
||||
Data(usize),
|
||||
}
|
||||
|
||||
impl ReadlineProvider for () {
|
||||
fn prompt(&mut self, stdout: &mut Stdout) {
|
||||
stdout.write_all(b"# ").ok();
|
||||
}
|
||||
|
||||
fn clear_screen(&mut self, stdout: &mut Stdout) {
|
||||
stdout.write_all(b"\x1B[H\x1B[J").ok();
|
||||
}
|
||||
|
||||
fn completions(&mut self, _raw_command: &str) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionState {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn tab<P: ReadlineProvider>(&mut self, buffer: &String, provider: &mut P) -> Option<String> {
|
||||
let completions = provider.completions(buffer.as_ref());
|
||||
|
||||
if completions.len() == 1 {
|
||||
let completion = &completions[0];
|
||||
Some(completion.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: ReadlineProvider> Readline<'a, P> {
|
||||
fn new(
|
||||
stdin: &'a mut TerminalInput,
|
||||
stdout: &'a mut Stdout,
|
||||
buffer: &'a mut String,
|
||||
provider: &'a mut P,
|
||||
) -> Self {
|
||||
Self {
|
||||
pos: buffer.chars().count(),
|
||||
completion: CompletionState::new(),
|
||||
|
||||
stdin,
|
||||
stdout,
|
||||
buffer,
|
||||
provider,
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_completion(&mut self, completion: &str) {
|
||||
match self.buffer.rsplit_once(' ') {
|
||||
Some((head, tail)) => {
|
||||
let new_buffer = format!("{head} {completion}");
|
||||
for _ in 0..tail.chars().count() {
|
||||
self.stdout.write_all(b"\x08").ok();
|
||||
}
|
||||
self.stdout.write_all(completion.as_bytes()).ok();
|
||||
self.stdout.flush().ok();
|
||||
self.pos = new_buffer.chars().count();
|
||||
*self.buffer = new_buffer;
|
||||
}
|
||||
None => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char(&mut self, key: TermKey) -> Result<Option<Outcome>, ReadlineError> {
|
||||
let mut ch_buffer = [0; 8];
|
||||
|
||||
match key {
|
||||
TermKey::Eof => {
|
||||
if self.pos == 0 {
|
||||
return Ok(Some(Outcome::Data(self.pos)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
TermKey::Char(ch) => match ch {
|
||||
// ^C
|
||||
'\x03' => return Ok(Some(Outcome::Interrupt)),
|
||||
// ^L
|
||||
'\x0C' => return Ok(Some(Outcome::Clear)),
|
||||
// Backspace
|
||||
'\x7F' => {
|
||||
// backspace
|
||||
if self.pos != 0 {
|
||||
self.stdout.write_all(b"\x08 \x08").ok();
|
||||
self.stdout.flush().ok();
|
||||
|
||||
self.buffer.pop();
|
||||
self.pos -= 1;
|
||||
}
|
||||
}
|
||||
// Tab
|
||||
'\t' => {
|
||||
// TODO parse the buffer into a command struct and do ops on the command
|
||||
// or at least check the token/word before current to see if it's a
|
||||
// separator like ';', '&&' etc
|
||||
if let Some(completion) = self.completion.tab(&self.buffer, self.provider) {
|
||||
self.accept_completion(&completion);
|
||||
}
|
||||
}
|
||||
ch if ch.is_whitespace() || ch.is_ascii_graphic() => {
|
||||
let bytes = ch.encode_utf8(&mut ch_buffer);
|
||||
self.stdout.write_all(bytes.as_bytes()).ok();
|
||||
self.stdout.flush().ok();
|
||||
|
||||
if ch != '\r' {
|
||||
self.buffer.push(ch);
|
||||
self.pos += 1;
|
||||
}
|
||||
|
||||
if ch == '\r' || ch == '\n' {
|
||||
if ch == '\r' {
|
||||
self.buffer.push('\n');
|
||||
self.pos += 1;
|
||||
self.stdout.write_all(b"\n").ok();
|
||||
self.stdout.flush().ok();
|
||||
}
|
||||
return Ok(Some(Outcome::Data(self.pos)));
|
||||
}
|
||||
}
|
||||
ch => {
|
||||
log::warn!("TODO: handle char: {ch:?}");
|
||||
}
|
||||
},
|
||||
TermKey::Up => {
|
||||
log::warn!("TODO: shell history up");
|
||||
}
|
||||
TermKey::Down => {
|
||||
log::warn!("TODO: shell history down");
|
||||
}
|
||||
TermKey::Left | TermKey::Right => {
|
||||
log::warn!("TODO: readline cursor");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn try_readline(&mut self) -> Result<Outcome, ReadlineError> {
|
||||
// let mut pos = self.buffer.chars().count();
|
||||
|
||||
self.pos = self.buffer.chars().count();
|
||||
self.stdout.write_all(self.buffer.as_bytes()).ok();
|
||||
self.stdout.flush().ok();
|
||||
|
||||
loop {
|
||||
let key = self.stdin.read_key()?;
|
||||
match self.handle_char(key)? {
|
||||
Some(outcome) => break Ok(outcome),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
// loop {
|
||||
// let key = self.stdin.read_key()?;
|
||||
|
||||
// }
|
||||
|
||||
// Ok(Outcome::Data(pos))
|
||||
}
|
||||
|
||||
pub fn readline(&mut self) -> Result<usize, ReadlineError> {
|
||||
loop {
|
||||
self.provider.prompt(self.stdout);
|
||||
self.stdout.flush().ok();
|
||||
|
||||
match self.try_readline()? {
|
||||
Outcome::Data(n) => break Ok(n),
|
||||
Outcome::Clear => {
|
||||
self.provider.clear_screen(self.stdout);
|
||||
}
|
||||
Outcome::Interrupt => {
|
||||
self.stdout.write_all(b"\r\n").ok();
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
// loop {
|
||||
// prompt(stdout);
|
||||
// stdout.flush().ok();
|
||||
//
|
||||
// match readline_inner(stdin, stdout, buffer)? {
|
||||
// Outcome::Data(n) => break Ok(n),
|
||||
// Outcome::Clear => {
|
||||
// stdout.write_all(b"\x1B[H\x1B[J").ok();
|
||||
// }
|
||||
// Outcome::Interrupt => {
|
||||
// stdout.write_all(b"\r\n").ok();
|
||||
// buffer.clear();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// fn readline_inner(
|
||||
// stdin: &mut TerminalInput,
|
||||
// stdout: &mut Stdout,
|
||||
// buffer: &mut String,
|
||||
// ) -> Result<Outcome, ReadlineError> {
|
||||
// }
|
||||
|
||||
pub fn readline<P: ReadlineProvider>(
|
||||
stdin: &mut TerminalInput,
|
||||
stdout: &mut Stdout,
|
||||
buffer: &mut String,
|
||||
provider: &mut P,
|
||||
) -> Result<usize, ReadlineError> {
|
||||
let mut readline = Readline::new(stdin, stdout, buffer, provider);
|
||||
readline.readline()
|
||||
}
|
||||
|
||||
// pub fn readline<P: Fn(&mut Stdout)>(
|
||||
// stdin: &mut TerminalInput,
|
||||
// stdout: &mut Stdout,
|
||||
// buffer: &mut String,
|
||||
// prompt: P,
|
||||
// ) -> Result<usize, ReadlineError> {
|
||||
// }
|
||||
@@ -6,6 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
cross.workspace = true
|
||||
logsink.workspace = true
|
||||
stuff.workspace = true
|
||||
|
||||
clap.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::io;
|
||||
|
||||
use stuff::readline;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("{0}")]
|
||||
Readline(#[from] readline::ReadlineError),
|
||||
#[error("")]
|
||||
InvalidUsage,
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
#![allow(clippy::new_without_default, clippy::should_implement_trait)]
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{stdout, BufRead, BufReader, Write},
|
||||
fs::{self, File},
|
||||
io::{self, stdout, BufRead, BufReader, Write},
|
||||
path::Path,
|
||||
process::ExitCode,
|
||||
};
|
||||
@@ -24,12 +24,12 @@ use cross::{
|
||||
};
|
||||
use error::Error;
|
||||
use exec::Outcome;
|
||||
use stuff::readline::{self, ReadlineError, ReadlineProvider};
|
||||
|
||||
pub mod builtin;
|
||||
pub mod command;
|
||||
pub mod error;
|
||||
pub mod exec;
|
||||
pub mod readline;
|
||||
pub mod syntax;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -47,26 +47,93 @@ pub enum ShellInput {
|
||||
Interactive(TerminalInput),
|
||||
}
|
||||
|
||||
pub struct ReadlineProviderImpl {}
|
||||
|
||||
fn list_cwd_files() -> io::Result<Vec<String>> {
|
||||
let dir = fs::read_dir(".")?;
|
||||
let mut names = vec![];
|
||||
for entry in dir {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let filename = entry.file_name();
|
||||
let filename = filename.to_str().unwrap();
|
||||
if filename.starts_with(".") {
|
||||
continue;
|
||||
}
|
||||
|
||||
names.push(filename.into());
|
||||
}
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
impl ReadlineProvider for ReadlineProviderImpl {
|
||||
fn prompt(&mut self, stdout: &mut io::Stdout) {
|
||||
let cwd = match std::env::current_dir() {
|
||||
Ok(cwd) => format!("{}", cwd.display()),
|
||||
Err(_) => "???".into(),
|
||||
};
|
||||
|
||||
stdout.write_all(format!("{cwd} $ ").as_bytes()).ok();
|
||||
}
|
||||
|
||||
fn clear_screen(&mut self, stdout: &mut io::Stdout) {
|
||||
stdout.write_all(b"\x1B[H\x1B[J").ok();
|
||||
}
|
||||
|
||||
fn completions(&mut self, raw_command: &str) -> Vec<String> {
|
||||
let (is_command, last_word) = match raw_command.rsplit_once(' ') {
|
||||
Some((_, last)) => (false, last),
|
||||
None => (true, raw_command.trim()),
|
||||
};
|
||||
|
||||
if is_command {
|
||||
// Lookup command completion
|
||||
vec![]
|
||||
} else {
|
||||
match list_cwd_files() {
|
||||
Ok(list) => list
|
||||
.into_iter()
|
||||
.filter(|e| e.starts_with(last_word))
|
||||
.collect(),
|
||||
Err(error) => {
|
||||
log::error!("Completion error: {error}");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellInput {
|
||||
pub fn read_line(&mut self, line: &mut String, continuation: bool) -> Result<usize, Error> {
|
||||
pub fn read_line(
|
||||
&mut self,
|
||||
readline_provider: &mut ReadlineProviderImpl,
|
||||
line: &mut String,
|
||||
continuation: bool,
|
||||
) -> Result<usize, Error> {
|
||||
match self {
|
||||
Self::File(file) => Ok(file.read_line(line)?),
|
||||
Self::Interactive(stdin) => {
|
||||
let mut stdout = stdout();
|
||||
|
||||
readline::readline(stdin, &mut stdout, line, |stdout| {
|
||||
let prompt = if !continuation {
|
||||
let cwd = std::env::current_dir();
|
||||
let cwd = match cwd {
|
||||
Ok(cwd) => format!("{}", cwd.display()),
|
||||
Err(_) => "???".into(),
|
||||
};
|
||||
format!("{cwd} $ ")
|
||||
} else {
|
||||
"> ".to_owned()
|
||||
};
|
||||
stdout.write_all(prompt.as_bytes()).ok();
|
||||
})
|
||||
readline::readline(stdin, &mut stdout, line, readline_provider).map_err(Error::from)
|
||||
// readline::readline(stdin, &mut stdout, line, |stdout| {
|
||||
// let prompt = if !continuation {
|
||||
// let cwd = std::env::current_dir();
|
||||
// let cwd = match cwd {
|
||||
// Ok(cwd) => format!("{}", cwd.display()),
|
||||
// Err(_) => "???".into(),
|
||||
// };
|
||||
// format!("{cwd} $ ")
|
||||
// } else {
|
||||
// "> ".to_owned()
|
||||
// };
|
||||
// stdout.write_all(prompt.as_bytes()).ok();
|
||||
// })
|
||||
// .map_err(Error::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,9 +176,10 @@ fn run_single(_env: &Environment, _command: &str) -> Outcome {
|
||||
fn run(mut input: ShellInput, env: &mut Environment) -> Result<(), Error> {
|
||||
let mut command_text = String::new();
|
||||
let mut line = String::new();
|
||||
let mut readline_provider = ReadlineProviderImpl {};
|
||||
loop {
|
||||
line.clear();
|
||||
let len = input.read_line(&mut line, !command_text.is_empty())?;
|
||||
let len = input.read_line(&mut readline_provider, &mut line, !command_text.is_empty())?;
|
||||
if len == 0 {
|
||||
break Ok(());
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
use std::io::{Stdout, Write};
|
||||
|
||||
use cross::term::{TermKey, TerminalInput};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
enum Outcome {
|
||||
Interrupt,
|
||||
Clear,
|
||||
Data(usize),
|
||||
}
|
||||
|
||||
struct Readline<'a> {
|
||||
stdin: &'a mut TerminalInput,
|
||||
stdout: &'a mut Stdout,
|
||||
buffer: &'a mut String,
|
||||
}
|
||||
|
||||
impl<'a> Readline<'a> {
|
||||
fn new(stdin: &'a mut TerminalInput, stdout: &'a mut Stdout, buffer: &'a mut String) -> Self {
|
||||
Self {
|
||||
stdin,
|
||||
stdout,
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char(&mut self, ch: char) {}
|
||||
}
|
||||
|
||||
fn readline_inner(
|
||||
stdin: &mut TerminalInput,
|
||||
stdout: &mut Stdout,
|
||||
buffer: &mut String,
|
||||
) -> Result<Outcome, Error> {
|
||||
let mut ch_buffer = [0; 8];
|
||||
let mut pos = buffer.chars().count();
|
||||
|
||||
stdout.write_all(buffer.as_bytes()).ok();
|
||||
stdout.flush().ok();
|
||||
|
||||
loop {
|
||||
let key = stdin.read_key()?;
|
||||
|
||||
match key {
|
||||
TermKey::Eof => {
|
||||
if pos == 0 {
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
TermKey::Char(ch) => match ch {
|
||||
// ^C
|
||||
'\x03' => return Ok(Outcome::Interrupt),
|
||||
// ^L
|
||||
'\x0C' => return Ok(Outcome::Clear),
|
||||
// Backspace
|
||||
'\x7F' => {
|
||||
// backspace
|
||||
if pos != 0 {
|
||||
stdout.write_all(b"\x08 \x08").ok();
|
||||
stdout.flush().ok();
|
||||
|
||||
buffer.pop();
|
||||
pos -= 1;
|
||||
}
|
||||
}
|
||||
// Tab
|
||||
'\t' => {
|
||||
// TODO parse the buffer into a command struct and do ops on the command
|
||||
// or at least check the token/word before current to see if it's a
|
||||
// separator like ';', '&&' etc
|
||||
let (is_command, last_word) = match buffer.rsplit_once(' ') {
|
||||
Some((_, last)) => (true, last),
|
||||
None => (false, buffer.trim()),
|
||||
};
|
||||
log::info!("Tab {last_word:?}");
|
||||
}
|
||||
ch if ch.is_whitespace() || ch.is_ascii_graphic() => {
|
||||
let bytes = ch.encode_utf8(&mut ch_buffer);
|
||||
stdout.write_all(bytes.as_bytes()).ok();
|
||||
stdout.flush().ok();
|
||||
|
||||
if ch != '\r' {
|
||||
buffer.push(ch);
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
if ch == '\r' || ch == '\n' {
|
||||
if ch == '\r' {
|
||||
buffer.push('\n');
|
||||
pos += 1;
|
||||
stdout.write_all(b"\n").ok();
|
||||
stdout.flush().ok();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
ch => {
|
||||
log::warn!("TODO: handle char: {ch:?}");
|
||||
}
|
||||
},
|
||||
TermKey::Up => {
|
||||
log::warn!("TODO: shell history up");
|
||||
}
|
||||
TermKey::Down => {
|
||||
log::warn!("TODO: shell history down");
|
||||
}
|
||||
TermKey::Left | TermKey::Right => {
|
||||
log::warn!("TODO: readline cursor");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Outcome::Data(pos))
|
||||
}
|
||||
|
||||
pub fn readline<P: Fn(&mut Stdout)>(
|
||||
stdin: &mut TerminalInput,
|
||||
stdout: &mut Stdout,
|
||||
buffer: &mut String,
|
||||
prompt: P,
|
||||
) -> Result<usize, Error> {
|
||||
loop {
|
||||
prompt(stdout);
|
||||
stdout.flush().ok();
|
||||
|
||||
match readline_inner(stdin, stdout, buffer)? {
|
||||
Outcome::Data(n) => break Ok(n),
|
||||
Outcome::Clear => {
|
||||
stdout.write_all(b"\x1B[H\x1B[J").ok();
|
||||
}
|
||||
Outcome::Interrupt => {
|
||||
stdout.write_all(b"\r\n").ok();
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user