aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonas Maier <jonas@x77.dev>2026-03-05 23:54:56 +0100
committerJonas Maier <jonas@x77.dev>2026-03-05 23:54:56 +0100
commitfb80e9c1cd4c2dcbb2d2ba1e2be8c7e19b9f0ce1 (patch)
treee6980a3ae362de33f1521c647e18dad16b02363e
parentf03a0863ba3da7cf34e938a7de1cf92675b09c41 (diff)
downloadpish-fb80e9c1cd4c2dcbb2d2ba1e2be8c7e19b9f0ce1.tar.gz
very buggy beginning of tab completion
-rw-r--r--src/completion.rs16
-rw-r--r--src/cursor.rs10
-rw-r--r--src/main.rs45
-rw-r--r--src/parse.rs169
4 files changed, 199 insertions, 41 deletions
diff --git a/src/completion.rs b/src/completion.rs
index 63ac866..cb030a1 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -6,7 +6,7 @@ pub struct Suggestion {
full: BString,
}
-pub fn _path_completion(mut prefix: BString) -> io::Result<Vec<Suggestion>> {
+fn _path_completion(mut prefix: BString) -> io::Result<Vec<Suggestion>> {
let mut partial_entry = BString::new();
while let Some(c) = prefix.last().cloned() {
if c == b'/' {
@@ -19,6 +19,10 @@ pub fn _path_completion(mut prefix: BString) -> io::Result<Vec<Suggestion>> {
let mut sugs = Vec::new();
+ if prefix.is_empty() {
+ prefix.push(b'.');
+ }
+
for entry in fs::read_dir(OsStr::from_bytes(&prefix))? {
let entry = entry?;
let name = entry.file_name().as_bytes().to_vec();
@@ -32,3 +36,13 @@ pub fn _path_completion(mut prefix: BString) -> io::Result<Vec<Suggestion>> {
Ok(sugs)
}
+
+pub fn path_completion(prefix: BString) -> Vec<Suggestion> {
+ match _path_completion(prefix) {
+ Ok(suggestions) => suggestions,
+ Err(err) => {
+ println!("path completion failed: {err:?}\r");
+ Vec::new()
+ }
+ }
+}
diff --git a/src/cursor.rs b/src/cursor.rs
index 7913d3c..fbaacbb 100644
--- a/src/cursor.rs
+++ b/src/cursor.rs
@@ -1,3 +1,5 @@
+use std::io::Write;
+
#[derive(Debug, Clone, Copy)]
pub enum Direction {
Up,
@@ -21,6 +23,14 @@ pub fn move_cursor(direction: Direction, n: usize) {
print!("\x1b[{n}{code}");
}
+pub fn save() {
+ std::io::stdout().lock().write_all(b"\x1b[s").unwrap();
+}
+
+pub fn restore() {
+ std::io::stdout().lock().write_all(b"\x1b[u").unwrap();
+}
+
/// Represents a cursor position
#[derive(Debug, Clone, Copy)]
pub struct CursorPos {
diff --git a/src/main.rs b/src/main.rs
index 5b195e4..a5820e7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,14 +5,14 @@ use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::process::{Command, Stdio};
+pub mod completion;
pub mod cursor;
pub mod linebuf;
pub mod panic;
pub mod parse;
pub mod raw;
-pub mod run;
pub mod reload;
-pub mod completion;
+pub mod run;
use linebuf::LineBuf;
use raw::*;
@@ -181,7 +181,40 @@ fn event_loop() {
}
b'\t' => {
- todo!()
+ let cmd = se.line.into_bytes();
+ let comp = parse::completion_context(&cmd);
+ match comp.kind {
+ parse::CompletionKind::Command => todo!(),
+ parse::CompletionKind::Argument => {
+ let suggestions = completion::path_completion(comp.partial);
+ if suggestions.len() == 0 {
+ continue;
+ }
+
+ if suggestions.len() == 1 {
+ // apply suggestion
+ todo!("apply suggestion");
+ }
+
+ cursor::save();
+
+ // one line below
+ print!("\r\n");
+ for s in suggestions {
+ io::stdout().lock().write_all(&s.display).unwrap();
+ println!();
+ }
+
+ cursor::restore();
+ stdout.lock().flush().unwrap();
+ }
+ parse::CompletionKind::None => {
+ for _ in 0..4 {
+ se.line.add(b' ');
+ print!(" ")
+ }
+ }
+ }
}
// Escape sequence
@@ -265,8 +298,10 @@ fn main() {
Ok(_) => break,
Err(_) => {
#[cfg(debug_assertions)]
- unsafe { reload::continue_reload() }
- },
+ unsafe {
+ reload::continue_reload()
+ }
+ }
}
}
diff --git a/src/parse.rs b/src/parse.rs
index 39ede4b..2da97c7 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -1,3 +1,5 @@
+use crate::BString;
+
#[derive(Debug)]
pub enum Ast {
AssignVar(AssignVar),
@@ -36,45 +38,142 @@ pub enum ParseError {
type Result<T> = std::result::Result<T, ParseError>;
-pub fn do_parse(mut x: &[u8]) -> Result<Ast> {
- Ast::parse(&mut x)
+pub fn do_parse(x: &[u8]) -> Result<Ast> {
+ Ast::parse(&mut Cursor::new(x, ParseMode::Command))
}
-trait Parse: Sized {
- fn parse(b: &mut &[u8]) -> Result<Self>;
+pub enum CompletionKind {
+ Command,
+ Argument,
+ None,
}
-#[inline(always)]
-fn parse<T: Parse>(b: &mut &[u8]) -> Result<T> {
- T::parse(b)
+pub struct CompletionContext {
+ pub kind: CompletionKind,
+ pub partial: BString,
}
-fn spaces(b: &mut &[u8]) {
- while let Some(b' ' | b'\t') = b.get(0) {
- *b = &b[1..];
+pub fn completion_context<'a>(x: &'a [u8]) -> CompletionContext {
+ let mut cursor = Cursor::new(x, ParseMode::Completion);
+ let ast = Ast::parse(&mut cursor);
+ match ast {
+ Ok(Ast::Pipes(pipes)) if cursor.spaced == false => {
+ if let Some(cmd) = pipes.cmds.last() {
+ if cmd.args.is_empty() {
+ CompletionContext {
+ kind: CompletionKind::Command,
+ partial: cmd.cmd.clone(),
+ }
+ } else {
+ CompletionContext {
+ kind: CompletionKind::Argument,
+ partial: cmd.args[cmd.args.len() - 1].clone(),
+ }
+ }
+ } else {
+ CompletionContext {
+ kind: CompletionKind::None,
+ partial: Vec::new(),
+ }
+ }
+ }
+ _ => CompletionContext {
+ kind: CompletionKind::None,
+ partial: Vec::new(),
+ },
}
}
-#[inline(always)]
-fn adv(b: &mut &[u8]) {
- *b = &b[1..]
+trait Parse: Sized {
+ fn parse(b: &mut Cursor<'_>) -> Result<Self>;
+}
+
+enum ParseMode {
+ Command,
+ Completion,
+}
+
+struct Cursor<'a> {
+ buf: &'a [u8],
+ mode: ParseMode,
+
+ /// if the last byte that was consumed was whitespace or part of a word
+ spaced: bool,
}
-fn parse_quoted_string(b: &mut &[u8], delim: u8) -> Result<Vec<u8>> {
+impl<'a> Cursor<'a> {
+ fn new(buf: &'a [u8], mode: ParseMode) -> Self {
+ Self {
+ buf,
+ mode,
+ spaced: false,
+ }
+ }
+
+ // non empty
+ fn has(&self) -> bool {
+ !self.buf.is_empty()
+ }
+
+ fn is_empty(&self) -> bool {
+ self.buf.is_empty()
+ }
+
+ fn peek(&self) -> u8 {
+ self.buf[0]
+ }
+
+ fn adv(&mut self) -> u8 {
+ let out = self.buf[0];
+ self.buf = &self.buf[1..];
+ self.spaced = false;
+ out
+ }
+
+ fn spaces(&mut self) {
+ while let Some(b' ' | b'\t') = self.buf.first() {
+ self.adv();
+ self.spaced = true;
+ }
+ }
+
+ fn is_completion(&self) -> bool {
+ match self.mode {
+ ParseMode::Completion => true,
+ _ => false,
+ }
+ }
+
+ fn parse<T: Parse>(&mut self) -> Result<T> {
+ T::parse(self)
+ }
+}
+
+fn parse_quoted_string(b: &mut Cursor<'_>, delim: u8) -> Result<Vec<u8>> {
// TODO: escape sequence stuff
let mut s = Vec::new();
- while b.len() > 0 {
- if b[0] == delim {
- adv(b);
+ while b.has() {
+ if delim == b' ' && b.peek() == b'|' {
+ return if s.len() == 0 {
+ Err(ParseError::UnexpectedPipe)
+ } else {
+ Ok(s)
+ };
+ }
+
+ if b.peek() == delim {
+ b.adv();
+ if delim == b' ' {
+ b.spaced = true;
+ }
return Ok(s);
}
- s.push(b[0]);
- adv(b);
+ s.push(b.adv());
}
- if delim == b' ' {
+ if delim == b' ' || b.is_completion() {
Ok(s)
} else {
Err(ParseError::Incomplete)
@@ -82,16 +181,16 @@ fn parse_quoted_string(b: &mut &[u8], delim: u8) -> Result<Vec<u8>> {
}
impl Parse for Vec<u8> {
- fn parse(b: &mut &[u8]) -> Result<Self> {
- spaces(b);
+ fn parse(b: &mut Cursor<'_>) -> Result<Self> {
+ b.spaces();
if b.is_empty() {
return Err(ParseError::Eof);
}
- let c = b[0];
+ let c = b.peek();
if c == b'|' {
Err(ParseError::UnexpectedPipe)
} else if c == b'\'' || c == b'"' {
- adv(b);
+ b.adv();
parse_quoted_string(b, c)
} else if c.is_ascii_graphic() {
parse_quoted_string(b, b' ')
@@ -102,17 +201,17 @@ impl Parse for Vec<u8> {
}
impl Parse for Ast {
- fn parse(b: &mut &[u8]) -> Result<Self> {
- Ok(Self::Pipes(parse(b)?))
+ fn parse(b: &mut Cursor<'_>) -> Result<Self> {
+ Ok(Self::Pipes(b.parse()?))
}
}
impl Parse for Command {
- fn parse(b: &mut &[u8]) -> Result<Self> {
- let path: Vec<u8> = parse(b)?;
+ fn parse(b: &mut Cursor<'_>) -> Result<Self> {
+ let path: Vec<u8> = b.parse()?;
let mut args = Vec::new();
loop {
- let arg: Result<Vec<u8>> = parse(b);
+ let arg: Result<Vec<u8>> = b.parse();
match arg {
Ok(arg) => args.push(arg),
Err(ParseError::Eof | ParseError::UnexpectedPipe) => break,
@@ -124,19 +223,19 @@ impl Parse for Command {
}
impl Parse for Pipes {
- fn parse(b: &mut &[u8]) -> Result<Self> {
- let mut cmds: Vec<Command> = vec![parse(b)?];
+ fn parse(b: &mut Cursor<'_>) -> Result<Self> {
+ let mut cmds: Vec<Command> = vec![b.parse()?];
loop {
- spaces(b);
+ b.spaces();
if b.is_empty() {
return Ok(Pipes { cmds });
}
- let c = b[0];
+ let c = b.peek();
if c == b'|' {
- adv(b);
- cmds.push(parse(b)?);
+ b.adv();
+ cmds.push(b.parse()?);
} else {
Err(ParseError::Unknown(c))?;
}