initial commit for fern's readline impl
This commit is contained in:
158
Cargo.lock
generated
158
Cargo.lock
generated
@@ -47,7 +47,7 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -58,7 +58,7 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -119,15 +119,6 @@ version = "0.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clipboard-win"
|
|
||||||
version = "5.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
|
|
||||||
dependencies = [
|
|
||||||
"error-code",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -143,7 +134,7 @@ dependencies = [
|
|||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -158,39 +149,6 @@ 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 = "endian-type"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errno"
|
|
||||||
version = "0.3.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "error-code"
|
|
||||||
version = "3.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fd-lock"
|
|
||||||
version = "4.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"rustix",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fern"
|
name = "fern"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -202,7 +160,7 @@ dependencies = [
|
|||||||
"nix",
|
"nix",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"rustyline",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -217,15 +175,6 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "home"
|
|
||||||
version = "0.5.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.42.2"
|
version = "1.42.2"
|
||||||
@@ -257,33 +206,12 @@ version = "0.5.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.4.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.4"
|
version = "2.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nibble_vec"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
|
|
||||||
dependencies = [
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
@@ -350,16 +278,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "radix_trie"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
|
|
||||||
dependencies = [
|
|
||||||
"endian-type",
|
|
||||||
"nibble_vec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -389,65 +307,12 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustix"
|
|
||||||
version = "0.38.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustyline"
|
|
||||||
version = "15.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"clipboard-win",
|
|
||||||
"fd-lock",
|
|
||||||
"home",
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"memchr",
|
|
||||||
"nix",
|
|
||||||
"radix_trie",
|
|
||||||
"rustyline-derive",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
|
||||||
"utf8parse",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustyline-derive"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "similar"
|
name = "similar"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smallvec"
|
|
||||||
version = "1.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -471,12 +336,6 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -489,15 +348,6 @@ 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 = "windows-sys"
|
|
||||||
version = "0.52.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ insta = "1.42.2"
|
|||||||
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
|
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
rustyline = { version = "15.0.0", features = [ "derive" ] }
|
unicode-width = "0.2.0"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "fern"
|
name = "fern"
|
||||||
|
|||||||
@@ -303,12 +303,6 @@ impl From<std::env::VarError> for ShErr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<rustyline::error::ReadlineError> for ShErr {
|
|
||||||
fn from(value: rustyline::error::ReadlineError) -> Self {
|
|
||||||
ShErr::simple(ShErrKind::ParseErr, value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Errno> for ShErr {
|
impl From<Errno> for ShErr {
|
||||||
fn from(value: Errno) -> Self {
|
fn from(value: Errno) -> Self {
|
||||||
ShErr::simple(ShErrKind::Errno, value.to_string())
|
ShErr::simple(ShErrKind::Errno, value.to_string())
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
use std::{env, mem, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, sync::Arc};
|
|
||||||
use crate::builtin::BUILTINS;
|
|
||||||
|
|
||||||
use rustyline::highlight::Highlighter;
|
|
||||||
use crate::{libsh::term::{Style, StyleSet, Styled}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, state::read_logic};
|
|
||||||
|
|
||||||
use super::readline::FernReadline;
|
|
||||||
|
|
||||||
fn is_executable(path: &Path) -> bool {
|
|
||||||
path.metadata()
|
|
||||||
.map(|m| m.permissions().mode() & 0o111 != 0)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
|
||||||
pub struct FernHighlighter {
|
|
||||||
input: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FernHighlighter {
|
|
||||||
pub fn new(input: String) -> Self {
|
|
||||||
Self {
|
|
||||||
input,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn highlight_subsh(&self, token: Tk) -> String {
|
|
||||||
if token.flags.contains(TkFlags::IS_SUBSH) {
|
|
||||||
let raw = token.as_str();
|
|
||||||
Self::hl_subsh_raw(raw)
|
|
||||||
} else if token.flags.contains(TkFlags::IS_CMDSUB) {
|
|
||||||
let raw = token.as_str();
|
|
||||||
Self::hl_cmdsub_raw(raw)
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn hl_subsh_raw(raw: &str) -> String {
|
|
||||||
let mut body = &raw[1..];
|
|
||||||
let mut closed = false;
|
|
||||||
if body.ends_with(')') {
|
|
||||||
body = &body[..body.len() - 1];
|
|
||||||
closed = true;
|
|
||||||
}
|
|
||||||
let sub_hl = FernHighlighter::new(body.to_string());
|
|
||||||
let body_highlighted = sub_hl.hl_input();
|
|
||||||
let open_paren = "(".styled(Style::BrightBlue);
|
|
||||||
let close_paren = ")".styled(Style::BrightBlue);
|
|
||||||
let mut result = format!("{open_paren}{body_highlighted}");
|
|
||||||
if closed {
|
|
||||||
result.push_str(&close_paren);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
pub fn hl_cmdsub_raw(raw: &str) -> String {
|
|
||||||
let mut body = &raw[2..];
|
|
||||||
let mut closed = false;
|
|
||||||
if body.ends_with(')') {
|
|
||||||
body = &body[..body.len() - 1];
|
|
||||||
closed = true;
|
|
||||||
}
|
|
||||||
let sub_hl = FernHighlighter::new(body.to_string());
|
|
||||||
let body_highlighted = sub_hl.hl_input();
|
|
||||||
let dollar_paren = "$(".styled(Style::BrightBlue);
|
|
||||||
let close_paren = ")".styled(Style::BrightBlue);
|
|
||||||
let mut result = format!("{dollar_paren}{body_highlighted}");
|
|
||||||
if closed {
|
|
||||||
result.push_str(&close_paren);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
pub fn hl_command(&self, token: Tk) -> String {
|
|
||||||
let raw = token.as_str();
|
|
||||||
let paths = env::var("PATH")
|
|
||||||
.unwrap_or_default();
|
|
||||||
let mut paths = paths.split(':');
|
|
||||||
|
|
||||||
let is_in_path = {
|
|
||||||
loop {
|
|
||||||
let Some(path) = paths.next() else {
|
|
||||||
break false
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut path = PathBuf::from(path);
|
|
||||||
path.push(PathBuf::from(raw));
|
|
||||||
|
|
||||||
if path.is_file() && is_executable(&path) {
|
|
||||||
break true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// TODO: zsh is capable of highlighting an alias red even if it exists, if the command it refers to is not found
|
|
||||||
// Implement some way to find out if the content of the alias is valid as well
|
|
||||||
let is_alias_or_function = read_logic(|l| {
|
|
||||||
l.get_func(raw).is_some() || l.get_alias(raw).is_some()
|
|
||||||
});
|
|
||||||
|
|
||||||
let is_builtin = BUILTINS.contains(&raw);
|
|
||||||
|
|
||||||
if is_alias_or_function || is_in_path || is_builtin {
|
|
||||||
raw.styled(Style::Green)
|
|
||||||
} else {
|
|
||||||
raw.styled(Style::Bold | Style::Red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn hl_dquote(&self, token: Tk) -> String {
|
|
||||||
let raw = token.as_str();
|
|
||||||
let mut chars = raw.chars().peekable();
|
|
||||||
const YELLOW: &str = "\x1b[33m";
|
|
||||||
const RESET: &str = "\x1b[0m";
|
|
||||||
let mut result = String::new();
|
|
||||||
let mut dquote_count = 0;
|
|
||||||
|
|
||||||
result.push_str(YELLOW);
|
|
||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
|
||||||
match ch {
|
|
||||||
'\\' => {
|
|
||||||
result.push(ch);
|
|
||||||
if let Some(ch) = chars.next() {
|
|
||||||
result.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'"' => {
|
|
||||||
dquote_count += 1;
|
|
||||||
result.push(ch);
|
|
||||||
if dquote_count >= 2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'$' if chars.peek() == Some(&'(') => {
|
|
||||||
let mut raw_cmd_sub = String::new();
|
|
||||||
raw_cmd_sub.push(ch);
|
|
||||||
raw_cmd_sub.push(chars.next().unwrap());
|
|
||||||
let mut cmdsub_count = 1;
|
|
||||||
|
|
||||||
while let Some(cmdsub_ch) = chars.next() {
|
|
||||||
match cmdsub_ch {
|
|
||||||
'\\' => {
|
|
||||||
raw_cmd_sub.push(cmdsub_ch);
|
|
||||||
if let Some(ch) = chars.next() {
|
|
||||||
raw_cmd_sub.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'$' if chars.peek() == Some(&'(') => {
|
|
||||||
cmdsub_count += 1;
|
|
||||||
raw_cmd_sub.push(cmdsub_ch);
|
|
||||||
raw_cmd_sub.push(chars.next().unwrap());
|
|
||||||
}
|
|
||||||
')' => {
|
|
||||||
cmdsub_count -= 1;
|
|
||||||
raw_cmd_sub.push(cmdsub_ch);
|
|
||||||
if cmdsub_count <= 0 {
|
|
||||||
let styled = Self::hl_cmdsub_raw(&mem::take(&mut raw_cmd_sub));
|
|
||||||
result.push_str(&styled);
|
|
||||||
result.push_str(YELLOW);
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => raw_cmd_sub.push(cmdsub_ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !raw_cmd_sub.is_empty() {
|
|
||||||
let styled = Self::hl_cmdsub_raw(&mem::take(&mut raw_cmd_sub));
|
|
||||||
result.push_str(&styled);
|
|
||||||
result.push_str(YELLOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => result.push(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push_str(RESET);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
pub fn hl_input(&self) -> String {
|
|
||||||
let mut output = self.input.clone();
|
|
||||||
|
|
||||||
// TODO: properly implement highlighting for unfinished input
|
|
||||||
let lex_results = LexStream::new(Arc::new(output.clone()), LexFlags::LEX_UNFINISHED);
|
|
||||||
let mut tokens = vec![];
|
|
||||||
|
|
||||||
for result in lex_results {
|
|
||||||
let Ok(token) = result else {
|
|
||||||
return self.input.clone();
|
|
||||||
};
|
|
||||||
tokens.push(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse the tokens, because we want to highlight from right to left
|
|
||||||
// Doing it this way allows us to trust the spans in the tokens throughout the entire process
|
|
||||||
let tokens = tokens.into_iter()
|
|
||||||
.rev()
|
|
||||||
.collect::<Vec<Tk>>();
|
|
||||||
for token in tokens {
|
|
||||||
match token.class {
|
|
||||||
_ if token.flags.intersects(TkFlags::IS_CMDSUB | TkFlags::IS_SUBSH) => {
|
|
||||||
let styled = self.highlight_subsh(token.clone());
|
|
||||||
output.replace_range(token.span.start..token.span.end, &styled);
|
|
||||||
}
|
|
||||||
TkRule::Str => {
|
|
||||||
if token.flags.contains(TkFlags::IS_CMD) {
|
|
||||||
let styled = self.hl_command(token.clone());
|
|
||||||
output.replace_range(token.span.start..token.span.end, &styled);
|
|
||||||
} else if is_dquote(&token) {
|
|
||||||
let styled = self.hl_dquote(token.clone());
|
|
||||||
output.replace_range(token.span.start..token.span.end, &styled);
|
|
||||||
} else {
|
|
||||||
output.replace_range(token.span.start..token.span.end, &token.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TkRule::Pipe |
|
|
||||||
TkRule::ErrPipe |
|
|
||||||
TkRule::And |
|
|
||||||
TkRule::Or |
|
|
||||||
TkRule::Bg |
|
|
||||||
TkRule::Sep |
|
|
||||||
TkRule::Redir => self.style_with_token(&token,&mut output,Style::Cyan.into()),
|
|
||||||
TkRule::CasePattern => self.style_with_token(&token,&mut output,Style::Blue.into()),
|
|
||||||
TkRule::BraceGrpStart |
|
|
||||||
TkRule::BraceGrpEnd => self.style_with_token(&token,&mut output,Style::Cyan.into()),
|
|
||||||
TkRule::Comment => self.style_with_token(&token,&mut output,Style::BrightBlack.into()),
|
|
||||||
_ => { output.replace_range(token.span.start..token.span.end, &token.to_string()); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
fn style_with_token(&self, token: &Tk, highlighted: &mut String, style: StyleSet) {
|
|
||||||
let styled = token.to_string().styled(style);
|
|
||||||
highlighted.replace_range(token.span.start..token.span.end, &styled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Highlighter for FernReadline {
|
|
||||||
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> {
|
|
||||||
let highlighter = FernHighlighter::new(line.to_string());
|
|
||||||
std::borrow::Cow::Owned(highlighter.hl_input())
|
|
||||||
}
|
|
||||||
fn highlight_char(&self, _line: &str, _pos: usize, _kind: rustyline::highlight::CmdKind) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_dquote(token: &Tk) -> bool {
|
|
||||||
let raw = token.as_str();
|
|
||||||
raw.starts_with('"')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,89 +3,25 @@ pub mod highlight;
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use readline::FernReadline;
|
use readline::FernReader;
|
||||||
use rustyline::{error::ReadlineError, history::FileHistory, ColorMode, Config, Editor};
|
|
||||||
|
|
||||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts};
|
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts};
|
||||||
|
|
||||||
/// Initialize the line editor
|
/// Initialize the line editor
|
||||||
fn init_rl() -> ShResult<Editor<FernReadline,FileHistory>> {
|
|
||||||
let rl = FernReadline::new();
|
|
||||||
|
|
||||||
let tab_stop = read_shopts(|s| s.prompt.tab_stop);
|
|
||||||
let edit_mode = read_shopts(|s| s.prompt.edit_mode).into();
|
|
||||||
let bell_style = read_shopts(|s| s.core.bell_style).into();
|
|
||||||
let ignore_dups = read_shopts(|s| s.core.hist_ignore_dupes);
|
|
||||||
let comp_limit = read_shopts(|s| s.prompt.comp_limit);
|
|
||||||
let auto_hist = read_shopts(|s| s.core.auto_hist);
|
|
||||||
let max_hist = read_shopts(|s| s.core.max_hist);
|
|
||||||
let color_mode = match read_shopts(|s| s.prompt.prompt_highlight) {
|
|
||||||
true => ColorMode::Enabled,
|
|
||||||
false => ColorMode::Disabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = Config::builder()
|
|
||||||
.tab_stop(tab_stop)
|
|
||||||
.indent_size(1)
|
|
||||||
.edit_mode(edit_mode)
|
|
||||||
.bell_style(bell_style)
|
|
||||||
.color_mode(color_mode)
|
|
||||||
.history_ignore_dups(ignore_dups).unwrap()
|
|
||||||
.completion_prompt_limit(comp_limit)
|
|
||||||
.auto_add_history(auto_hist)
|
|
||||||
.max_history_size(max_hist).unwrap()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let mut editor = Editor::with_config(config).unwrap();
|
|
||||||
|
|
||||||
editor.set_helper(Some(rl));
|
|
||||||
editor.load_history(&Path::new("/home/pagedmov/.fernhist"))?;
|
|
||||||
Ok(editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_prompt() -> ShResult<String> {
|
fn get_prompt() -> ShResult<String> {
|
||||||
let Ok(prompt) = env::var("PS1") else {
|
let Ok(prompt) = env::var("PS1") else {
|
||||||
// username@hostname
|
// username@hostname
|
||||||
// short/path/to/pwd/
|
// short/path/to/pwd/
|
||||||
// $
|
// $
|
||||||
let default = "\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$ ";
|
let default = "\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||||
return Ok(format!("\n{}",expand_prompt(default)?))
|
return Ok(format!("\n{}",expand_prompt(default)?))
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(format!("\n{}",expand_prompt(&prompt)?))
|
Ok(format!("\n{}",expand_prompt(&prompt)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_hist_path() -> ShResult<PathBuf> {
|
|
||||||
if let Ok(path) = env::var("FERN_HIST") {
|
|
||||||
Ok(PathBuf::from(path))
|
|
||||||
} else {
|
|
||||||
let home = env::var("HOME")?;
|
|
||||||
let path = PathBuf::from(format!("{home}/.fernhist"));
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_line() -> ShResult<String> {
|
pub fn read_line() -> ShResult<String> {
|
||||||
assert!(isatty(STDIN_FILENO).unwrap());
|
|
||||||
let mut editor = init_rl()?;
|
|
||||||
let prompt = get_prompt()?;
|
let prompt = get_prompt()?;
|
||||||
match editor.readline(&prompt) {
|
let mut reader = FernReader::new(prompt);
|
||||||
Ok(line) => {
|
reader.readline()
|
||||||
if !line.is_empty() {
|
|
||||||
let hist_path = get_hist_path()?;
|
|
||||||
editor.add_history_entry(&line)?;
|
|
||||||
editor.save_history(&hist_path)?;
|
|
||||||
}
|
|
||||||
Ok(line)
|
|
||||||
}
|
|
||||||
Err(ReadlineError::Eof) => {
|
|
||||||
kill(Pid::this(), Signal::SIGQUIT)?;
|
|
||||||
Ok(String::new())
|
|
||||||
}
|
|
||||||
Err(ReadlineError::Interrupted) => Ok(String::new()),
|
|
||||||
Err(e) => {
|
|
||||||
Err(e.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,295 @@
|
|||||||
use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper};
|
use std::{arch::asm, os::fd::BorrowedFd};
|
||||||
|
|
||||||
use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}};
|
use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read};
|
||||||
use crate::prelude::*;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::{libsh::error::ShResult, prelude::*};
|
||||||
|
|
||||||
|
#[derive(Clone,Copy,Debug)]
|
||||||
|
pub enum Key {
|
||||||
|
Char(char),
|
||||||
|
Enter,
|
||||||
|
Backspace,
|
||||||
|
Esc,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Ctrl(char),
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Terminal {
|
||||||
|
stdin: RawFd,
|
||||||
|
stdout: RawFd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
assert!(isatty(0).unwrap());
|
||||||
|
Self {
|
||||||
|
stdin: 0,
|
||||||
|
stdout: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn raw_mode() -> termios::Termios {
|
||||||
|
// Get the current terminal attributes
|
||||||
|
let orig_termios = unsafe { termios::tcgetattr(BorrowedFd::borrow_raw(STDIN_FILENO)).expect("Failed to get terminal attributes") };
|
||||||
|
|
||||||
|
// Make a mutable copy
|
||||||
|
let mut raw = orig_termios.clone();
|
||||||
|
|
||||||
|
// Apply raw mode flags
|
||||||
|
termios::cfmakeraw(&mut raw);
|
||||||
|
|
||||||
|
// Set the attributes immediately
|
||||||
|
unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &raw) }
|
||||||
|
.expect("Failed to set terminal to raw mode");
|
||||||
|
|
||||||
|
// Return original attributes so they can be restored later
|
||||||
|
orig_termios
|
||||||
|
}
|
||||||
|
pub fn restore_termios(termios: Termios) {
|
||||||
|
unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &termios) }
|
||||||
|
.expect("Failed to restore terminal settings");
|
||||||
|
}
|
||||||
|
pub fn with_raw_mode<F: FnOnce() -> R,R>(func: F) -> R {
|
||||||
|
let saved = Self::raw_mode();
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
||||||
|
Self::restore_termios(saved);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => std::panic::resume_unwind(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn read_byte(&self, buf: &mut [u8]) -> usize {
|
||||||
|
Self::with_raw_mode(|| {
|
||||||
|
let ret: usize;
|
||||||
|
unsafe {
|
||||||
|
let buf_ptr = buf.as_mut_ptr();
|
||||||
|
let len = buf.len();
|
||||||
|
asm! (
|
||||||
|
"syscall",
|
||||||
|
in("rax") 0,
|
||||||
|
in("rdi") self.stdin,
|
||||||
|
in("rsi") buf_ptr,
|
||||||
|
in("rdx") len,
|
||||||
|
lateout("rax") ret,
|
||||||
|
out("rcx") _,
|
||||||
|
out("r11") _,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn write_bytes(&self, buf: &[u8]) {
|
||||||
|
Self::with_raw_mode(|| {
|
||||||
|
let _ret: usize;
|
||||||
|
unsafe {
|
||||||
|
let buf_ptr = buf.as_ptr();
|
||||||
|
let len = buf.len();
|
||||||
|
asm!(
|
||||||
|
"syscall",
|
||||||
|
in("rax") 1,
|
||||||
|
in("rdi") self.stdout,
|
||||||
|
in("rsi") buf_ptr,
|
||||||
|
in("rdx") len,
|
||||||
|
lateout("rax") _ret,
|
||||||
|
out("rcx") _,
|
||||||
|
out("r11") _,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pub fn write(&self, s: &str) {
|
||||||
|
self.write_bytes(s.as_bytes());
|
||||||
|
}
|
||||||
|
pub fn writeln(&self, s: &str) {
|
||||||
|
self.write(s);
|
||||||
|
self.write_bytes(b"\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Terminal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default,Debug)]
|
#[derive(Default,Debug)]
|
||||||
pub struct FernReadline;
|
pub struct FernReader {
|
||||||
|
pub term: Terminal,
|
||||||
impl FernReadline {
|
pub prompt: String,
|
||||||
pub fn new() -> Self {
|
pub line: LineBuf,
|
||||||
Self
|
pub editor: EditMode
|
||||||
}
|
|
||||||
pub fn search_hist(value: &str, ctx: &rustyline::Context<'_>) -> Option<String> {
|
|
||||||
let len = ctx.history().len();
|
|
||||||
for i in 0..len {
|
|
||||||
let entry = ctx.history().get(i, SearchDirection::Reverse).unwrap().unwrap();
|
|
||||||
if entry.entry.starts_with(value) {
|
|
||||||
return Some(entry.entry.into_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Helper for FernReadline {}
|
impl FernReader {
|
||||||
|
pub fn new(prompt: String) -> Self {
|
||||||
impl Completer for FernReadline {
|
Self {
|
||||||
type Candidate = String;
|
term: Terminal::new(),
|
||||||
}
|
prompt,
|
||||||
|
line: Default::default(),
|
||||||
pub struct FernHint {
|
editor: Default::default()
|
||||||
raw: String,
|
|
||||||
styled: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FernHint {
|
|
||||||
pub fn new(raw: String) -> Self {
|
|
||||||
let styled = (&raw).styled(Style::Dim | Style::BrightBlack);
|
|
||||||
Self { raw, styled }
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Hint for FernHint {
|
|
||||||
fn display(&self) -> &str {
|
|
||||||
&self.styled
|
|
||||||
}
|
}
|
||||||
fn completion(&self) -> Option<&str> {
|
fn pack_line(&self) -> String {
|
||||||
if !self.raw.is_empty() {
|
self.line
|
||||||
Some(&self.raw)
|
.buffer
|
||||||
|
.iter()
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
pub fn readline(&mut self) -> ShResult<String> {
|
||||||
|
self.display_line(false);
|
||||||
|
loop {
|
||||||
|
let key = self.read_key().unwrap();
|
||||||
|
self.process_key(key);
|
||||||
|
self.display_line(true);
|
||||||
|
if let Key::Enter = key {
|
||||||
|
self.term.write_bytes(b"\r");
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(self.pack_line())
|
||||||
|
}
|
||||||
|
pub fn process_key(&mut self, key: Key) {
|
||||||
|
match key {
|
||||||
|
Key::Char(ch) => {
|
||||||
|
self.line.insert_at_cursor(ch);
|
||||||
|
}
|
||||||
|
Key::Enter => {
|
||||||
|
self.line.insert_at_cursor('\n');
|
||||||
|
}
|
||||||
|
Key::Backspace => self.line.backspace_at_cursor(),
|
||||||
|
Key::Esc => todo!(),
|
||||||
|
Key::Up => todo!(),
|
||||||
|
Key::Down => todo!(),
|
||||||
|
Key::Left => self.line.move_cursor_left(),
|
||||||
|
Key::Right => self.line.move_cursor_right(),
|
||||||
|
Key::Ctrl(ctrl) => todo!(),
|
||||||
|
Key::Unknown => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn clear_line(&self) {
|
||||||
|
let prompt_lines = self.prompt.lines().count();
|
||||||
|
let buf_lines = self.line.count_lines().saturating_sub(1); // One of the buffer's lines will overlap with the prompt
|
||||||
|
let total = prompt_lines + buf_lines;
|
||||||
|
self.term.write_bytes(b"\r\n");
|
||||||
|
for _ in 0..total {
|
||||||
|
self.term.write_bytes(b"\r\x1b[2K\x1b[1A");
|
||||||
|
}
|
||||||
|
self.term.write_bytes(b"\r\x1b[2K");
|
||||||
|
}
|
||||||
|
fn display_line(&self, refresh: bool) {
|
||||||
|
if refresh {
|
||||||
|
self.clear_line();
|
||||||
|
}
|
||||||
|
let mut prompt_lines = self.prompt.lines().peekable();
|
||||||
|
let mut last_line_len = 0;
|
||||||
|
while let Some(line) = prompt_lines.next() {
|
||||||
|
if prompt_lines.peek().is_none() {
|
||||||
|
last_line_len = strip_ansi_codes(line).width();
|
||||||
|
self.term.write(line);
|
||||||
} else {
|
} else {
|
||||||
None
|
self.term.writeln(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.term.write(&self.pack_line());
|
||||||
|
|
||||||
|
let cursor_offset = self.line.cursor + last_line_len;
|
||||||
|
self.term.write_bytes(format!("\r\x1b[{}C", cursor_offset).as_bytes());
|
||||||
|
}
|
||||||
|
fn read_key(&mut self) -> Option<Key> {
|
||||||
|
let mut buf = [0; 3];
|
||||||
|
|
||||||
|
let n = self.term.read_byte(&mut buf);
|
||||||
|
if n == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match buf[0] {
|
||||||
|
b'\x1b' => {
|
||||||
|
if n == 3 {
|
||||||
|
match (buf[1], buf[2]) {
|
||||||
|
(b'[', b'A') => Some(Key::Up),
|
||||||
|
(b'[', b'B') => Some(Key::Down),
|
||||||
|
(b'[', b'C') => Some(Key::Right),
|
||||||
|
(b'[', b'D') => Some(Key::Left),
|
||||||
|
_ => Some(Key::Esc),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(Key::Esc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'\r' | b'\n' => Some(Key::Enter),
|
||||||
|
0x7f => Some(Key::Backspace),
|
||||||
|
c if (c as char).is_ascii_control() => {
|
||||||
|
let ctrl = (c ^ 0x40) as char;
|
||||||
|
Some(Key::Ctrl(ctrl))
|
||||||
|
}
|
||||||
|
c => Some(Key::Char(c as char))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hinter for FernReadline {
|
#[derive(Default,Debug)]
|
||||||
type Hint = FernHint;
|
pub enum EditMode {
|
||||||
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
|
Normal,
|
||||||
if line.is_empty() {
|
#[default]
|
||||||
return None
|
Insert,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default,Debug)]
|
||||||
|
pub struct LineBuf {
|
||||||
|
buffer: Vec<char>,
|
||||||
|
cursor: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineBuf {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
}
|
}
|
||||||
let ent = Self::search_hist(line,ctx)?;
|
pub fn count_lines(&self) -> usize {
|
||||||
let entry_raw = ent.get(pos..)?.to_string();
|
self.buffer.iter().filter(|&&c| c == '\n').count()
|
||||||
Some(FernHint::new(entry_raw))
|
}
|
||||||
|
pub fn insert_at_cursor(&mut self, ch: char) {
|
||||||
|
self.buffer.insert(self.cursor, ch);
|
||||||
|
self.move_cursor_right();
|
||||||
|
}
|
||||||
|
pub fn backspace_at_cursor(&mut self) {
|
||||||
|
if self.buffer.is_empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.buffer.remove(self.cursor.saturating_sub(1));
|
||||||
|
self.move_cursor_left();
|
||||||
|
}
|
||||||
|
pub fn move_cursor_left(&mut self) {
|
||||||
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
|
}
|
||||||
|
pub fn move_cursor_right(&mut self) {
|
||||||
|
self.cursor = self.cursor.saturating_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validator for FernReadline {
|
pub fn strip_ansi_codes(s: &str) -> String {
|
||||||
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
|
let mut out = String::with_capacity(s.len());
|
||||||
let mut tokens = vec![];
|
let mut chars = s.chars().peekable();
|
||||||
let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty());
|
|
||||||
for tk in tk_stream {
|
while let Some(c) = chars.next() {
|
||||||
if tk.is_err() {
|
if c == '\x1b' && chars.peek() == Some(&'[') {
|
||||||
return Ok(ValidationResult::Incomplete)
|
// Skip over the escape sequence
|
||||||
|
chars.next(); // consume '['
|
||||||
|
while let Some(&ch) = chars.peek() {
|
||||||
|
if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
|
||||||
|
chars.next(); // consume final letter
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
tokens.push(tk.unwrap());
|
chars.next(); // consume intermediate characters
|
||||||
}
|
}
|
||||||
let nd_stream = ParseStream::new(tokens);
|
} else {
|
||||||
for nd in nd_stream {
|
out.push(c);
|
||||||
if nd.is_err() {
|
|
||||||
return Ok(ValidationResult::Incomplete)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ValidationResult::Valid(None))
|
out
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/prompt/readline_old.rs
Normal file
86
src/prompt/readline_old.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper};
|
||||||
|
|
||||||
|
use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Default,Debug)]
|
||||||
|
pub struct FernReadline;
|
||||||
|
|
||||||
|
impl FernReadline {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
pub fn search_hist(value: &str, ctx: &rustyline::Context<'_>) -> Option<String> {
|
||||||
|
let len = ctx.history().len();
|
||||||
|
for i in 0..len {
|
||||||
|
let entry = ctx.history().get(i, SearchDirection::Reverse).unwrap().unwrap();
|
||||||
|
if entry.entry.starts_with(value) {
|
||||||
|
return Some(entry.entry.into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Helper for FernReadline {}
|
||||||
|
|
||||||
|
impl Completer for FernReadline {
|
||||||
|
type Candidate = String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FernHint {
|
||||||
|
raw: String,
|
||||||
|
styled: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FernHint {
|
||||||
|
pub fn new(raw: String) -> Self {
|
||||||
|
let styled = (&raw).styled(Style::Dim | Style::BrightBlack);
|
||||||
|
Self { raw, styled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hint for FernHint {
|
||||||
|
fn display(&self) -> &str {
|
||||||
|
&self.styled
|
||||||
|
}
|
||||||
|
fn completion(&self) -> Option<&str> {
|
||||||
|
if !self.raw.is_empty() {
|
||||||
|
Some(&self.raw)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hinter for FernReadline {
|
||||||
|
type Hint = FernHint;
|
||||||
|
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
|
||||||
|
if line.is_empty() {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
let ent = Self::search_hist(line,ctx)?;
|
||||||
|
let entry_raw = ent.get(pos..)?.to_string();
|
||||||
|
Some(FernHint::new(entry_raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validator for FernReadline {
|
||||||
|
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
|
||||||
|
let mut tokens = vec![];
|
||||||
|
let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty());
|
||||||
|
for tk in tk_stream {
|
||||||
|
if tk.is_err() {
|
||||||
|
return Ok(ValidationResult::Incomplete)
|
||||||
|
}
|
||||||
|
tokens.push(tk.unwrap());
|
||||||
|
}
|
||||||
|
let nd_stream = ParseStream::new(tokens);
|
||||||
|
for nd in nd_stream {
|
||||||
|
if nd.is_err() {
|
||||||
|
return Ok(ValidationResult::Incomplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ValidationResult::Valid(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/shopt.rs
21
src/shopt.rs
@@ -1,6 +1,5 @@
|
|||||||
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use rustyline::{config::BellStyle, EditMode};
|
|
||||||
|
|
||||||
use crate::{libsh::error::{Note, ShErr, ShErrKind, ShResult}, state::ShFunc};
|
use crate::{libsh::error::{Note, ShErr, ShErrKind, ShResult}, state::ShFunc};
|
||||||
|
|
||||||
@@ -29,17 +28,6 @@ impl FromStr for FernBellStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FernBellStyle> for BellStyle {
|
|
||||||
fn from(val: FernBellStyle) -> Self {
|
|
||||||
match val {
|
|
||||||
FernBellStyle::Audible => BellStyle::Audible,
|
|
||||||
FernBellStyle::Visible => BellStyle::Visible,
|
|
||||||
FernBellStyle::Disable => BellStyle::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Display for FernBellStyle {
|
impl Display for FernBellStyle {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -56,15 +44,6 @@ pub enum FernEditMode {
|
|||||||
Emacs
|
Emacs
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FernEditMode> for EditMode {
|
|
||||||
fn from(val: FernEditMode) -> Self {
|
|
||||||
match val {
|
|
||||||
FernEditMode::Vi => EditMode::Vi,
|
|
||||||
FernEditMode::Emacs => EditMode::Emacs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for FernEditMode {
|
impl FromStr for FernEditMode {
|
||||||
type Err = ShErr;
|
type Err = ShErr;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
|||||||
@@ -1,27 +1 @@
|
|||||||
|
|
||||||
use insta::assert_snapshot;
|
|
||||||
|
|
||||||
use crate::prompt::highlight::FernHighlighter;
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn highlight_simple() {
|
|
||||||
let line = "echo foo bar";
|
|
||||||
let styled = FernHighlighter::new(line.to_string()).hl_input();
|
|
||||||
assert_snapshot!(styled)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn highlight_cmd_sub() {
|
|
||||||
let line = "echo foo $(echo bar)";
|
|
||||||
let styled = FernHighlighter::new(line.to_string()).hl_input();
|
|
||||||
assert_snapshot!(styled)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn highlight_cmd_sub_in_dquotes() {
|
|
||||||
let line = "echo \"foo $(echo bar) biz\"";
|
|
||||||
let styled = FernHighlighter::new(line.to_string()).hl_input();
|
|
||||||
assert_snapshot!(styled)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user