Skip to content

Commit

Permalink
Refactor line editing (#212)
Browse files Browse the repository at this point in the history
* 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
vinc authored Jul 24, 2021
1 parent b7eeb25 commit f1084bf
Show file tree
Hide file tree
Showing 11 changed files with 709 additions and 487 deletions.
12 changes: 12 additions & 0 deletions src/api/console.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::sys;
use core::fmt;

pub struct Style {
Expand Down Expand Up @@ -81,3 +82,14 @@ fn color_to_bg(name: &str) -> Option<usize> {
}
}

pub fn read_char() -> Option<char> {
Some(sys::console::get_char())
}

pub fn is_printable(c: char) -> bool {
if cfg!(feature = "video") {
c.is_ascii() && sys::vga::is_printable(c as u8)
} else {
true // TODO
}
}
51 changes: 51 additions & 0 deletions src/api/fs.rs
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(()),
}
}
2 changes: 2 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
316 changes: 316 additions & 0 deletions src/api/prompt.rs
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
}
Loading

0 comments on commit f1084bf

Please sign in to comment.