-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Rename height and width to rows and cols * Refactor line editing * Add partial support of delete key * Use parser from vte crate to strip csi * Use parser from vte crate to read input chars * Add suppor of delete key in serial * Move keys handling into separate functions * Add history navigation * Add autocompletion * Use Prompt for shell * Fix shell exiting * Leave more space for kernel on disk
- Loading branch information
Showing
11 changed files
with
709 additions
and
487 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
use crate::sys; | ||
use alloc::string::{String, ToString}; | ||
|
||
pub fn canonicalize(path: &str) -> Result<String, ()> { | ||
match sys::process::env("HOME") { | ||
Some(home) => { | ||
if path.starts_with("~") { | ||
Ok(path.replace("~", &home)) | ||
} else { | ||
Ok(path.to_string()) | ||
} | ||
}, | ||
None => { | ||
Ok(path.to_string()) | ||
} | ||
} | ||
} | ||
|
||
pub fn read_to_string(path: &str) -> Result<String, ()> { | ||
let path = match canonicalize(path) { | ||
Ok(path) => path, | ||
Err(_) => return Err(()), | ||
}; | ||
match sys::fs::File::open(&path) { | ||
Some(mut file) => { | ||
Ok(file.read_to_string()) | ||
}, | ||
None => { | ||
Err(()) | ||
} | ||
} | ||
} | ||
|
||
pub fn write(path: &str, buf: &[u8]) -> Result<(), ()> { | ||
let path = match canonicalize(path) { | ||
Ok(path) => path, | ||
Err(_) => return Err(()), | ||
}; | ||
let mut file = match sys::fs::File::open(&path) { | ||
None => match sys::fs::File::create(&path) { | ||
None => return Err(()), | ||
Some(file) => file, | ||
}, | ||
Some(file) => file, | ||
}; | ||
// TODO: add File::write_all to split buf if needed | ||
match file.write(buf) { | ||
Ok(_) => Ok(()), | ||
Err(_) => Err(()), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,5 +18,7 @@ macro_rules! println { | |
|
||
pub mod console; | ||
pub mod font; | ||
pub mod fs; | ||
pub mod prompt; | ||
pub mod syscall; | ||
pub mod vga; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
use crate::api::fs; | ||
use crate::api::console; | ||
use alloc::boxed::Box; | ||
use alloc::string::{String, ToString}; | ||
use alloc::vec::Vec; | ||
use core::cmp; | ||
use vte::{Params, Parser, Perform}; | ||
|
||
pub struct Prompt { | ||
pub completion: Completion, | ||
pub history: History, | ||
offset: usize, // Offset line by the length of the prompt string | ||
cursor: usize, | ||
line: String, | ||
} | ||
|
||
impl Prompt { | ||
pub fn new() -> Self { | ||
Self { | ||
completion: Completion::new(), | ||
history: History::new(), | ||
offset: 0, | ||
cursor: 0, | ||
line: String::new(), | ||
} | ||
} | ||
|
||
pub fn input(&mut self, prompt: &str) -> Option<String> { | ||
print!("{}", prompt); | ||
self.offset = offset_from_prompt(prompt); | ||
self.cursor = self.offset; | ||
self.line = String::new(); | ||
let mut parser = Parser::new(); | ||
while let Some(c) = console::read_char() { | ||
match c { | ||
'\x03' => { // End of Text (^C) | ||
print!("\n"); | ||
return None; | ||
}, | ||
'\x04' => { // End of Transmission (^D) | ||
print!("\n"); | ||
return None; | ||
}, | ||
'\n' => { // New Line | ||
self.update_completion(); | ||
self.update_history(); | ||
print!("{}", c); | ||
return Some(self.line.clone()); | ||
}, | ||
c => { | ||
if c.is_ascii() { | ||
parser.advance(self, c as u8); | ||
} | ||
} | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
fn update_history(&mut self) { | ||
if let Some(i) = self.history.pos { | ||
self.line = self.history.entries[i].clone(); | ||
self.history.pos = None; | ||
} | ||
} | ||
|
||
fn update_completion(&mut self) { | ||
if let Some(i) = self.completion.pos { | ||
let complete = self.completion.entries[i].clone(); | ||
self.line.push_str(&complete); | ||
self.cursor += complete.len(); | ||
self.completion.pos = None; | ||
self.completion.entries = Vec::new(); | ||
} | ||
} | ||
|
||
fn handle_tab_key(&mut self) { | ||
self.update_history(); | ||
let (bs, pos) = match self.completion.pos { | ||
Some(pos) => { | ||
let bs = self.completion.entries[pos].len(); | ||
if pos + 1 < self.completion.entries.len() { | ||
(bs, pos + 1) | ||
} else { | ||
(bs, 0) | ||
} | ||
}, | ||
None => { | ||
self.completion.entries = (self.completion.completer)(&self.line); | ||
if self.completion.entries.len() > 0 { | ||
(0, 0) | ||
} else { | ||
return | ||
} | ||
}, | ||
}; | ||
let erase = '\x08'.to_string().repeat(bs); | ||
let complete = &self.completion.entries[pos]; | ||
print!("{}{}", erase, complete); | ||
self.completion.pos = Some(pos); | ||
} | ||
|
||
fn handle_up_key(&mut self) { | ||
self.update_completion(); | ||
let n = self.history.entries.len(); | ||
if n == 0 { | ||
return; | ||
} | ||
let (bs, i) = match self.history.pos { | ||
Some(i) => (self.history.entries[i].len(), cmp::max(i, 1) - 1), | ||
None => (self.line.len(), n - 1), | ||
}; | ||
let line = &self.history.entries[i]; | ||
let blank = ' '.to_string().repeat((self.offset + bs) - self.cursor); | ||
let erase = '\x08'.to_string().repeat(bs); | ||
print!("{}{}{}", blank, erase, line); | ||
self.cursor = self.offset + line.len(); | ||
self.history.pos = Some(i); | ||
} | ||
|
||
fn handle_down_key(&mut self) { | ||
self.update_completion(); | ||
let n = self.history.entries.len(); | ||
if n == 0 { | ||
return; | ||
} | ||
let (bs, i) = match self.history.pos { | ||
Some(i) => (self.history.entries[i].len(), i + 1), | ||
None => return, | ||
}; | ||
let (pos, line) = if i < n { | ||
(Some(i), &self.history.entries[i]) | ||
} else { | ||
(None, &self.line) | ||
}; | ||
let erase = '\x08'.to_string().repeat(bs); | ||
print!("{}{}", erase, line); | ||
self.cursor = self.offset + line.len(); | ||
self.history.pos = pos; | ||
} | ||
|
||
fn handle_forward_key(&mut self) { | ||
self.update_completion(); | ||
self.update_history(); | ||
if self.cursor < self.offset + self.line.len() { | ||
print!("\x1b[1C"); | ||
self.cursor += 1; | ||
} | ||
} | ||
|
||
fn handle_backward_key(&mut self) { | ||
self.update_completion(); | ||
self.update_history(); | ||
if self.cursor > self.offset { | ||
print!("\x1b[1D"); | ||
self.cursor -= 1; | ||
} | ||
} | ||
|
||
fn handle_delete_key(&mut self) { | ||
self.update_completion(); | ||
self.update_history(); | ||
if self.cursor < self.offset + self.line.len() { | ||
let i = self.cursor - self.offset; | ||
self.line.remove(i); | ||
let s = &self.line[i..]; | ||
print!("{} \x1b[{}D", s, s.len() + 1); | ||
} | ||
} | ||
|
||
fn handle_backspace_key(&mut self) { | ||
self.update_completion(); | ||
self.update_history(); | ||
if self.cursor > self.offset { | ||
let i = self.cursor - self.offset - 1; | ||
self.line.remove(i); | ||
let s = &self.line[i..]; | ||
print!("{}{} \x1b[{}D", '\x08', s, s.len() + 1); | ||
self.cursor -= 1; | ||
} | ||
} | ||
|
||
fn handle_printable_key(&mut self, c: char) { | ||
self.update_completion(); | ||
self.update_history(); | ||
if console::is_printable(c) { | ||
let i = self.cursor - self.offset; | ||
self.line.insert(i, c); | ||
let s = &self.line[i..]; | ||
print!("{} \x1b[{}D", s, s.len()); | ||
self.cursor += 1; | ||
} | ||
} | ||
} | ||
|
||
impl Perform for Prompt { | ||
fn execute(&mut self, b: u8) { | ||
let c = b as char; | ||
match c { | ||
'\x08' => self.handle_backspace_key(), | ||
'\t' => self.handle_tab_key(), | ||
_ => {}, | ||
} | ||
} | ||
|
||
fn print(&mut self, c: char) { | ||
match c { | ||
'\x7f' => self.handle_delete_key(), | ||
c => self.handle_printable_key(c), | ||
} | ||
} | ||
|
||
fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, c: char) { | ||
match c { | ||
'A' => self.handle_up_key(), | ||
'B' => self.handle_down_key(), | ||
'C' => self.handle_forward_key(), | ||
'D' => self.handle_backward_key(), | ||
'~' => { | ||
for param in params.iter() { | ||
if param[0] == 3 { // Delete | ||
self.handle_delete_key(); | ||
} | ||
} | ||
}, | ||
_ => {}, | ||
} | ||
} | ||
} | ||
|
||
pub struct Completion { | ||
completer: Box<dyn Fn(&str) -> Vec<String>>, | ||
entries: Vec<String>, | ||
pos: Option<usize>, | ||
} | ||
|
||
fn empty_completer(_line: &str) -> Vec<String> { | ||
Vec::new() | ||
} | ||
|
||
impl Completion { | ||
pub fn new() -> Self { | ||
Self { | ||
completer: Box::new(empty_completer), | ||
entries: Vec::new(), | ||
pos: None, | ||
} | ||
} | ||
pub fn set(&mut self, completer: &'static dyn Fn(&str) -> Vec<String>) { | ||
self.completer = Box::new(completer); | ||
} | ||
} | ||
|
||
pub struct History { | ||
entries: Vec<String>, | ||
limit: usize, | ||
pos: Option<usize>, | ||
} | ||
|
||
impl History { | ||
pub fn new() -> Self { | ||
Self { | ||
entries: Vec::new(), | ||
limit: 1000, | ||
pos: None, | ||
} | ||
} | ||
|
||
pub fn load(&mut self, path: &str) { | ||
if let Ok(lines) = fs::read_to_string(path) { | ||
self.entries = lines.split("\n").map(|s| s.to_string()).collect(); | ||
} | ||
} | ||
|
||
pub fn save(&mut self, path: &str) { | ||
fs::write(path, self.entries.join("\n").as_bytes()).ok(); | ||
} | ||
|
||
pub fn add(&mut self, entry: &str) { | ||
// Remove duplicated entries | ||
let mut i = 0; | ||
while i < self.entries.len() { | ||
if self.entries[i] == entry { | ||
self.entries.remove(i); | ||
} else { | ||
i += 1; | ||
} | ||
} | ||
|
||
self.entries.push(entry.to_string()); | ||
|
||
// Remove oldest entries if limit is reached | ||
while self.entries.len() > self.limit { | ||
self.entries.remove(0); | ||
} | ||
} | ||
} | ||
|
||
struct Offset(usize); | ||
|
||
impl Perform for Offset { | ||
fn print(&mut self, c: char) { | ||
self.0 += c.len_utf8(); | ||
} | ||
} | ||
|
||
fn offset_from_prompt(s: &str) -> usize { | ||
let mut parser = Parser::new(); | ||
let mut offset = Offset(0); | ||
|
||
for b in s.bytes() { | ||
parser.advance(&mut offset, b); | ||
} | ||
offset.0 | ||
} |
Oops, something went wrong.