Implemented the 'help' builtin, and support for :h <topic> in ex mode

:h is an alias for the 'help' builtin.

'help' takes a single argument and tries to find a suitable match among the files in '$SHED_HPATH'

if a match is found, this file is opened in your pager

calling the 'help' builtin using :h in ex mode will preserve your current pending line
This commit is contained in:
2026-03-15 18:18:53 -04:00
parent f6a3935bcb
commit 99b9440ee1
18 changed files with 1080 additions and 52 deletions

View File

@@ -35,6 +35,7 @@ rand = "0.10.0"
regex = "1.11.1" regex = "1.11.1"
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde_json = "1.0.149" serde_json = "1.0.149"
tempfile = "3.24.0"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"
vte = "0.15" vte = "0.15"

76
doc/arith.txt Normal file
View File

@@ -0,0 +1,76 @@
*arith* *arithmetic* *arithmetic-expansion*
#ARITHMETIC EXPANSION#
Arithmetic expansion evaluates a mathematical expression and substitutes
the result. The expression is subject to parameter expansion and command
substitution before evaluation.
`$((expression))`
Example:
`echo $((2 + 3))` # prints: 5
`x=$((width * height))`
==============================================================================
1. Operators *arith-operators*
The following operators are supported, listed from highest to lowest
precedence:
`( )` *arith-parens*
Grouping. Override default precedence.
Example:
`echo $(( (2+3) * 4 ))` # prints: 20
`*` `/` `%` *arith-muldivmod*
Multiplication, division, and modulo (remainder).
Example:
`echo $((10 / 3))` # prints: 3
`echo $((10 % 3))` # prints: 1
`+` `-` *arith-addsub*
Addition and subtraction.
Example:
`echo $((10 - 3 + 1))` # prints: 8
==============================================================================
2. Variables in Expressions *arith-variables*
Variables can be referenced by name inside arithmetic expressions.
They are expanded and converted to numbers.
`x=10`
`echo $(($x + 5))` # prints: 15
`echo $((x + 5))` # also works
If a variable is unset or not a valid number, an error is reported.
==============================================================================
3. Nesting *arith-nesting*
Arithmetic expressions can be nested with parentheses to any depth:
`echo $(( (1+2) * (3+4) ))` # prints: 21
Arithmetic expansion can also appear inside other expansions:
`echo "Total: $((price * qty))"`
==============================================================================
4. Whitespace *arith-whitespace*
Whitespace inside `$((...))` is ignored and can be used freely for
readability:
`echo $((2+3))` # prints: 5
`echo $(( 2 + 3 ))` # same result
==============================================================================
See also: |param| |redirect| |glob|

155
doc/glob.txt Normal file
View File

@@ -0,0 +1,155 @@
*glob* *globbing* *pathname-expansion* *filename-expansion*
#PATHNAME EXPANSION#
After word splitting, the shell scans each word for the characters `*`,
`?`, and `[`. If any appear (and are not quoted), the word is treated as a
pattern and replaced with an alphabetically sorted list of matching file
names. If no files match, the pattern is left unchanged.
==============================================================================
1. Wildcards *glob-wildcards*
`*` *glob-star*
Matches any string of zero or more characters, except that it does
not match a leading `.` (see |glob-dotglob|) or a `/`.
Example:
`echo *.txt` # all .txt files
`ls src/*.rs` # all .rs files in src/
`?` *glob-question*
Matches exactly one character, with the same restrictions as `*`.
Example:
`ls file?.txt` # file1.txt, fileA.txt, etc.
`[...]` *glob-bracket*
Matches any one of the enclosed characters. A range can be specified
with a hyphen.
`[abc]` matches `a`, `b`, or `c`
`[a-z]` matches any lowercase letter
`[0-9]` matches any digit
`[A-Za-z]` matches any letter
`[!...]` `[^...]` *glob-bracket-negate*
Matches any character NOT in the set.
Example:
`ls [!.]*.txt` # .txt files not starting with dot
`echo file[^0-9].txt` # files without a digit
==============================================================================
2. Hidden Files *glob-dotglob*
By default, patterns do not match files whose names begin with `.`
(hidden files). A leading dot must be matched explicitly:
`echo .*` # only hidden files
`echo .* *` # hidden and non-hidden files
The `dotglob` shell option changes this behavior:
`shopt core.dotglob true`
When enabled, `*` and `?` will also match files starting with `.`.
==============================================================================
3. Brace Expansion *brace* *brace-expansion*
Brace expansion is performed before globbing and generates multiple
words from a single pattern. It is not a POSIX feature.
`{a,b,c}` *brace-list*
Comma-separated list. Each item becomes a separate word.
Example:
`echo {a,b,c}` # prints: a b c
`echo file.{txt,log}` # prints: file.txt file.log
`mkdir -p src/{bin,lib}`
`{N..M}` *brace-range*
Numeric or character range.
Example:
`echo {1..5}` # prints: 1 2 3 4 5
`echo {a..f}` # prints: a b c d e f
`echo {5..1}` # prints: 5 4 3 2 1
`{N..M..S}` *brace-range-step*
Numeric range with step {S}.
Example:
`echo {0..10..2}` # prints: 0 2 4 6 8 10
`echo {1..20..5}` # prints: 1 6 11 16
`{01..10}` *brace-range-pad*
Zero-padded ranges. If either endpoint has leading zeros, all
generated values are padded to the same width.
Example:
`echo {01..05}` # prints: 01 02 03 04 05
`echo {001..3}` # prints: 001 002 003
Brace expansion can be nested and combined with other expansions:
`echo {a,b{1..3},c}` # prints: a b1 b2 b3 c
==============================================================================
4. Quoting and Escaping *glob-quoting*
Glob characters lose their special meaning when quoted:
`echo "*"` # prints literal *
`echo '*.txt'` # prints literal *.txt
`echo \*` # prints literal *
This is important when passing patterns to commands like `find` or
`grep` where you want the command (not the shell) to interpret the
pattern.
==============================================================================
5. Tilde Expansion *tilde* *tilde-expansion*
Tilde expansion is performed before pathname expansion.
`~` *tilde-home*
Expands to the value of `$HOME`.
`~/path` *tilde-home-path*
Expands `~` to `$HOME`, then appends the path.
Example:
`cd ~/projects`
`ls ~/.config`
`~user` *tilde-user*
Expands to the home directory of {user}.
Example:
`ls ~root` # /root
`cat ~nobody/.profile`
`~uid` *tilde-uid*
Expands to the home directory of the user with numeric uid {uid}.
This is a shed-specific extension.
Example:
`echo ~0` # /root (uid 0)
`echo ~1000` # first normal user's home
==============================================================================
See also: |param| |redirect| |arith|

197
doc/param.txt Normal file
View File

@@ -0,0 +1,197 @@
*param* *parameter-expansion* *param-expansion*
#PARAMETER EXPANSION#
The shell provides several forms of parameter expansion for working with
variables. In each form, {word} is subject to tilde expansion, parameter
expansion, command substitution, and arithmetic expansion.
If {parameter} is unset or null, the behavior depends on the operator used.
"Unset" means the variable has never been assigned. "Null" means the variable
is set but its value is the empty string.
==============================================================================
1. Basic Forms *param-basic*
`$var` Value of {var}
`${var}` Same, with explicit braces (needed for `${var}foo`)
Braces are required when {var} is followed by characters that could be part
of the name, or when using any of the operators below.
==============================================================================
2. Default Values *param-default*
`${var:-word}` *param-default-val*
Use default value. If {var} is unset or null, expand to {word}.
Otherwise, expand to the value of {var}.
Example:
`name=${1:-world}`
`echo "hello $name"` # prints "hello world" if \$1 is unset
`${var-word}` *param-default-nonnull*
Like `:-` but only substitutes {word} if {var} is completely unset,
not if it is null.
==============================================================================
3. Assign Defaults *param-assign*
`${var:=word}` *param-assign-val*
Assign default value. If {var} is unset or null, assign {word} to
{var} and then expand to the new value.
Note: This cannot be used with positional parameters or special
parameters.
Example:
`echo ${cache:=/tmp/cache}` # sets and uses \$cache
`${var=word}` *param-assign-nonnull*
Like `:=` but only assigns if {var} is completely unset.
==============================================================================
4. Error on Unset *param-error*
`${var:?word}` *param-error-val*
Display error. If {var} is unset or null, print {word} to stderr
and exit (in a non-interactive shell). If {word} is omitted, a
default message is printed.
Example:
`input=${1:?usage: myscript \<filename\>}`
`${var?word}` *param-error-nonnull*
Like `:?` but only errors if {var} is completely unset.
==============================================================================
5. Alternate Value *param-alt*
`${var:+word}` *param-alt-val*
Use alternate value. If {var} is unset or null, expand to nothing.
Otherwise, expand to {word}.
Example:
`echo ${verbose:+--verbose}` # flag only if \$verbose is set
`${var+word}` *param-alt-nonnull*
Like `:+` but substitutes {word} only if {var} is set (even if null).
==============================================================================
6. String Length *param-length*
`${#var}` *param-strlen*
Expands to the length of the value of {var} in characters.
Example:
`str="hello"`
`echo ${#str}` # prints 5
==============================================================================
7. Substring Removal *param-substring*
`${var#pattern}` *param-trim-short-left*
Remove shortest matching prefix. Removes the shortest match of
{pattern} from the beginning of the value of {var}.
`${var##pattern}` *param-trim-long-left*
Remove longest matching prefix.
Example:
`path="/home/user/file.txt"`
`echo ${path##*/}` # prints "file.txt"
`${var%pattern}` *param-trim-short-right*
Remove shortest matching suffix. Removes the shortest match of
{pattern} from the end of the value of {var}.
`${var%%pattern}` *param-trim-long-right*
Remove longest matching suffix.
Example:
`file="archive.tar.gz"`
`echo ${file%%.*}` # prints "archive"
`echo ${file%.*}` # prints "archive.tar"
==============================================================================
8. Search and Replace *param-replace*
`${var/pattern/replacement}` *param-replace-first*
Replace first match. Replaces the first occurrence of {pattern}
in the value of {var} with {replacement}.
`${var//pattern/replacement}` *param-replace-all*
Replace all matches.
Example:
`str="hello world"`
`echo ${str/o/0}` # prints "hell0 world"
`echo ${str//o/0}` # prints "hell0 w0rld"
`${var/#pattern/replacement}` *param-replace-prefix*
Replace if matching at the beginning.
`${var/%pattern/replacement}` *param-replace-suffix*
Replace if matching at the end.
==============================================================================
9. Case Modification *param-case*
`${var^}` *param-upper-first*
Uppercase the first character of {var}.
`${var^^}` *param-upper-all*
Uppercase all characters.
`${var,}` *param-lower-first*
Lowercase the first character of {var}.
`${var,,}` *param-lower-all*
Lowercase all characters.
Example:
`name="john doe"`
`echo ${name^}` # prints "John doe"
`echo ${name^^}` # prints "JOHN DOE"
==============================================================================
10. Substrings *param-slice*
`${var:offset}` *param-slice-from*
Substring starting at {offset} (0-indexed).
`${var:offset:length}` *param-slice-range*
Substring of {length} characters starting at {offset}.
Negative offsets count from the end (note the space before the minus
to distinguish from `:-`):
`str="hello world"`
`echo ${str: -5}` # prints "world"
`echo ${str:0:5}` # prints "hello"
==============================================================================
See also: |redirect| |glob| |arith|

181
doc/redirect.txt Normal file
View File

@@ -0,0 +1,181 @@
*redirect* *redirection* *redir*
#REDIRECTION#
Redirections allow you to control where a command reads its input from and
where it sends its output. A redirection applies to a specific file
descriptor; if no descriptor number is given, output redirections default
to stdout (fd 1) and input redirections default to stdin (fd 0).
==============================================================================
1. Output Redirection *redir-output*
`command > file` *redir-out*
Redirect stdout to {file}, creating it if it does not exist or
truncating it if it does.
Example:
`echo hello > out.txt`
`ls 2> errors.txt` # redirect stderr
`command >| file` *redir-out-force*
Like `>` but overrides the {noclobber} option. If {noclobber} is set,
`>` will refuse to overwrite an existing file; `>|` forces the
overwrite.
`command >> file` *redir-append*
Append stdout to {file}, creating it if it does not exist.
Example:
`echo line >> log.txt`
==============================================================================
2. Input Redirection *redir-input*
`command < file` *redir-in*
Redirect {file} to stdin.
Example:
`sort < unsorted.txt`
==============================================================================
3. Read-Write Redirection *redir-readwrite*
`command <> file` *redir-rw*
Open {file} for both reading and writing on the specified file
descriptor (default fd 0). The file is created if it does not exist
but is not truncated.
Useful with the `seek` builtin for random-access file operations.
Example:
`exec 3<> data.bin`
`seek 3 0 set` # seek to beginning
==============================================================================
4. File Descriptor Duplication *redir-dup*
`command N>&M` *redir-dup-out*
Duplicate output file descriptor {M} onto {N}. After this, writing
to fd {N} goes to the same place as fd {M}.
Example:
`command > out.txt 2>&1` # stderr goes where stdout goes
`command N<&M` *redir-dup-in*
Duplicate input file descriptor {M} onto {N}.
`command N>&-` *redir-close-out*
`command N<&-` *redir-close-in*
Close file descriptor {N}.
Example:
`exec 3>&-` # close fd 3
==============================================================================
5. Pipelines *redir-pipe*
`command1 | command2` *pipe*
Connect stdout of {command1} to stdin of {command2}. Both commands
run concurrently.
Example:
`cat file.txt | grep pattern | sort`
`command1 |& command2` *pipe-and*
Connect both stdout and stderr of {command1} to stdin of {command2}.
Equivalent to `command1 2>&1 | command2`.
==============================================================================
6. Here Documents *heredoc*
`command << DELIM` *redir-heredoc*
Read input from the script body until a line containing only {DELIM}
is found. The text between is fed to stdin of {command}.
Parameter expansion, command substitution, and arithmetic expansion
are performed in the body unless the delimiter is quoted.
Example:
`cat << EOF`
`Hello $USER`
`EOF`
`command << 'DELIM'` *redir-heredoc-literal*
Quoting the delimiter (single or double quotes) suppresses all
expansion in the heredoc body. The text is passed literally.
Example:
`cat << 'EOF'`
`This $variable is not expanded`
`EOF`
`command <<- DELIM` *redir-heredoc-indent*
Like `<<` but strips leading tab characters from each line of the
body and from the closing delimiter. This allows heredocs to be
indented for readability without affecting the content.
Example:
`if true; then`
` cat <<- EOF`
` indented content`
` EOF`
`fi`
==============================================================================
7. Here Strings *herestring*
`command <<< word` *redir-herestring*
Feed {word} as a single string to stdin of {command}, with a
trailing newline appended. {word} is subject to the usual expansions.
Example:
`read first rest <<< "hello world"`
`bc <<< "2 + 2"`
==============================================================================
8. File Descriptor Numbers *redir-fd*
Any redirection operator can be prefixed with a file descriptor number:
`2> file` redirect stderr to file
`3< file` open file on fd 3
`4>> file` append to file on fd 4
`5<> file` open file read-write on fd 5
Standard file descriptors:
0 stdin
1 stdout
2 stderr
File descriptors 3 and above are available for general use with `exec`.
==============================================================================
9. Combining Redirections *redir-combine*
Multiple redirections can appear on a single command, processed left
to right:
`command > out.txt 2>&1` # stdout to file, stderr to same file
`command 2>&1 > out.txt` # different! stderr to terminal,
# stdout to file
Order matters: each redirection is applied in sequence.
==============================================================================
See also: |param| |glob| |arith|

291
src/builtin/help.rs Normal file
View File

@@ -0,0 +1,291 @@
use std::{env, io::Write, path::Path};
use ariadne::Span as ASpan;
use nix::libc::STDIN_FILENO;
use crate::{
libsh::{error::{ShErr, ShErrKind, ShResult}, guards::RawModeGuard}, parse::{NdRule, Node, Redir, RedirType, execute::{exec_input, prepare_argv}, lex::{QuoteState, Span}}, procio::{IoFrame, IoMode}, readline::{complete::ScoredCandidate, markers}, state
};
const TAG_SEQ: &str = "\x1b[1;33m"; // bold yellow — searchable tags
const REF_SEQ: &str = "\x1b[4;36m"; // underline cyan — cross-references
const RESET_SEQ: &str = "\x1b[0m";
const HEADER_SEQ: &str = "\x1b[1;35m"; // bold magenta — section headers
const CODE_SEQ: &str = "\x1b[32m"; // green — inline code
const KEYWORD_2_SEQ: &str = "\x1b[1;32m"; // bold green — {keyword}
const KEYWORD_3_SEQ: &str = "\x1b[3;37m"; // italic white — [optional]
pub fn help(node: Node) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let mut argv = prepare_argv(argv)?.into_iter().peekable();
let help = argv.next().unwrap(); // drop 'help'
// Join all of the word-split arguments into a single string
// Preserve the span too
let (topic, span) = if argv.peek().is_none() {
("help.txt".to_string(), help.1)
} else {
argv.fold((String::new(), Span::default()), |mut acc, arg| {
if acc.1 == Span::default() {
acc.1 = arg.1.clone();
} else {
let new_end = arg.1.end();
let start = acc.1.start();
acc.1.set_range(start..new_end);
}
if acc.0.is_empty() {
acc.0 = arg.0;
} else {
acc.0 = acc.0 + &format!(" {}",arg.0);
}
acc
})
};
let hpath = env::var("SHED_HPATH").unwrap_or_default();
for path in hpath.split(':') {
let path = Path::new(&path).join(&topic);
if path.is_file() {
let Ok(contents) = std::fs::read_to_string(&path) else {
continue;
};
let filename = path.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
let unescaped = unescape_help(&contents);
let expanded = expand_help(&unescaped);
open_help(&expanded, None, Some(filename))?;
state::set_status(0);
return Ok(());
}
}
// didn't find an exact filename match, its probably a tag search
for path in hpath.split(':') {
let path = Path::new(path);
if let Ok(entries) = path.read_dir() {
for entry in entries {
let Ok(entry) = entry else { continue };
let path = entry.path();
let filename = path.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
if !path.is_file() {
continue;
}
let Ok(contents) = std::fs::read_to_string(&path) else {
continue;
};
let unescaped = unescape_help(&contents);
let expanded = expand_help(&unescaped);
let tags = read_tags(&expanded);
for (tag, line) in &tags {
}
if let Some((matched_tag, line)) = get_best_match(&topic, &tags) {
open_help(&expanded, Some(line), Some(filename))?;
state::set_status(0);
return Ok(());
} else {
}
}
}
}
state::set_status(1);
Err(ShErr::at(
ShErrKind::NotFound,
span,
"No relevant help page found for this topic",
))
}
pub fn open_help(content: &str, line: Option<usize>, file_name: Option<String>) -> ShResult<()> {
let pager = env::var("PAGER").unwrap_or("less -R".into());
let line_arg = line.map(|ln| format!("+{ln}")).unwrap_or_default();
let prompt_arg = file_name.map(|name| format!("-Ps'{name}'")).unwrap_or_default();
let mut tmp = tempfile::NamedTempFile::new()?;
let tmp_path = tmp.path().to_string_lossy().to_string();
tmp.write_all(content.as_bytes())?;
tmp.flush()?;
RawModeGuard::with_cooked_mode(|| {
exec_input(
format!("{pager} {line_arg} {prompt_arg} {tmp_path}"),
None,
true,
Some("help".into()),
)
})
}
pub fn get_best_match(topic: &str, tags: &[(String, usize)]) -> Option<(String, usize)> {
let mut candidates: Vec<_> = tags.iter()
.map(|(tag,line)| (ScoredCandidate::new(tag.to_string()), *line))
.collect();
for (cand,_) in candidates.iter_mut() {
cand.fuzzy_score(topic);
}
candidates.retain(|(c,_)| c.score.unwrap_or(i32::MIN) > i32::MIN);
candidates.sort_by_key(|(c,_)| c.score.unwrap_or(i32::MIN));
candidates.first().map(|(c,line)| (c.content.clone(), *line))
}
pub fn read_tags(raw: &str) -> Vec<(String, usize)> {
let mut tags = vec![];
for (line_num, line) in raw.lines().enumerate() {
let mut rest = line;
while let Some(pos) = rest.find(TAG_SEQ) {
let after_seq = &rest[pos + TAG_SEQ.len()..];
if let Some(end) = after_seq.find(RESET_SEQ) {
let tag = &after_seq[..end];
tags.push((tag.to_string(), line_num + 1));
rest = &after_seq[end + RESET_SEQ.len()..];
} else {
break;
}
}
}
tags
}
pub fn expand_help(raw: &str) -> String {
let mut result = String::new();
let mut chars = raw.chars();
while let Some(ch) = chars.next() {
match ch {
markers::RESET => result.push_str(RESET_SEQ),
markers::TAG => result.push_str(TAG_SEQ),
markers::REFERENCE => result.push_str(REF_SEQ),
markers::HEADER => result.push_str(HEADER_SEQ),
markers::CODE => result.push_str(CODE_SEQ),
markers::KEYWORD_2 => result.push_str(KEYWORD_2_SEQ),
markers::KEYWORD_3 => result.push_str(KEYWORD_3_SEQ),
_ => result.push(ch),
}
}
result
}
pub fn unescape_help(raw: &str) -> String {
let mut result = String::new();
let mut chars = raw.chars().peekable();
let mut qt_state = QuoteState::default();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'\n' => {
result.push(ch);
qt_state = QuoteState::default();
}
'"' => {
result.push(ch);
qt_state.toggle_double();
}
'\'' => {
result.push(ch);
qt_state.toggle_single();
}
_ if qt_state.in_quote() || chars.peek().is_none_or(|ch| ch.is_whitespace()) => {
result.push(ch);
}
'*' => {
result.push(markers::TAG);
while let Some(next_ch) = chars.next() {
if next_ch == '*' {
result.push(markers::RESET);
break;
} else {
result.push(next_ch);
}
}
}
'|' => {
result.push(markers::REFERENCE);
while let Some(next_ch) = chars.next() {
if next_ch == '|' {
result.push(markers::RESET);
break;
} else {
result.push(next_ch);
}
}
}
'#' => {
result.push(markers::HEADER);
while let Some(next_ch) = chars.next() {
if next_ch == '#' {
result.push(markers::RESET);
break;
} else {
result.push(next_ch);
}
}
}
'`' => {
result.push(markers::CODE);
while let Some(next_ch) = chars.next() {
if next_ch == '`' {
result.push(markers::RESET);
break;
} else {
result.push(next_ch);
}
}
}
'{' => {
result.push(markers::KEYWORD_2);
while let Some(next_ch) = chars.next() {
if next_ch == '}' {
result.push(markers::RESET);
break;
} else {
result.push(next_ch);
}
}
}
'[' => {
result.push(markers::KEYWORD_3);
while let Some(next_ch) = chars.next() {
if next_ch == ']' {
result.push(markers::RESET);
break;
} else {
result.push(next_ch);
}
}
}
_ => result.push(ch),
}
}
result
}

View File

@@ -25,13 +25,14 @@ pub mod source;
pub mod test; // [[ ]] thing pub mod test; // [[ ]] thing
pub mod trap; pub mod trap;
pub mod varcmds; pub mod varcmds;
pub mod help;
pub const BUILTINS: [&str; 50] = [ pub const BUILTINS: [&str; 51] = [
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
"disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin", "disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek", "getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek", "help",
]; ];
pub fn true_builtin() -> ShResult<()> { pub fn true_builtin() -> ShResult<()> {

View File

@@ -479,21 +479,28 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let ch = chars.next().unwrap(); let ch = chars.next().unwrap();
username.push(ch); username.push(ch);
} }
let home = if username.is_empty() { let home = if username.is_empty() {
// standard '~' expansion
env::var("HOME").unwrap_or_default() env::var("HOME").unwrap_or_default()
} }
else if let Ok(result) = User::from_name(&username) else if let Ok(result) = User::from_name(&username)
&& let Some(user) = result { && let Some(user) = result {
// username expansion like '~user'
user.dir.to_string_lossy().to_string() user.dir.to_string_lossy().to_string()
} }
else if let Ok(id) = username.parse::<u32>() else if let Ok(id) = username.parse::<u32>()
&& let Ok(result) = User::from_uid(Uid::from_raw(id)) && let Ok(result) = User::from_uid(Uid::from_raw(id))
&& let Some(user) = result { && let Some(user) = result {
// uid expansion like '~1000'
// shed only feature btw B)
user.dir.to_string_lossy().to_string() user.dir.to_string_lossy().to_string()
} }
else { else {
// no match, use literal
format!("~{username}") format!("~{username}")
}; };
result.push_str(&home); result.push_str(&home);
} }
markers::PROC_SUB_OUT => { markers::PROC_SUB_OUT => {
@@ -1399,6 +1406,7 @@ pub fn unescape_str(raw: &str) -> String {
/// Like unescape_str but for heredoc bodies. Only processes: /// Like unescape_str but for heredoc bodies. Only processes:
/// - $var / ${var} / $(cmd) substitution markers /// - $var / ${var} / $(cmd) substitution markers
/// - Backslash escapes (only before $, `, \, and newline) /// - Backslash escapes (only before $, `, \, and newline)
///
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal. /// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
pub fn unescape_heredoc(raw: &str) -> String { pub fn unescape_heredoc(raw: &str) -> String {
let mut chars = raw.chars().peekable(); let mut chars = raw.chars().peekable();
@@ -1576,6 +1584,10 @@ pub fn unescape_math(raw: &str) -> String {
#[derive(Debug)] #[derive(Debug)]
pub enum ParamExp { pub enum ParamExp {
Len, // #var_name Len, // #var_name
ToUpperFirst, // ^var_name
ToUpperAll, // ^^var_name
ToLowerFirst, // ,var_name
ToLowerAll, // ,,var_name
DefaultUnsetOrNull(String), // :- DefaultUnsetOrNull(String), // :-
DefaultUnset(String), // - DefaultUnset(String), // -
SetDefaultUnsetOrNull(String), // := SetDefaultUnsetOrNull(String), // :=
@@ -1611,6 +1623,11 @@ impl FromStr for ParamExp {
)) ))
}; };
if s == "^^" { return Ok(ToUpperAll) }
if s == "^" { return Ok(ToUpperFirst) }
if s == ",," { return Ok(ToLowerAll) }
if s == "," { return Ok(ToLowerFirst) }
// Handle indirect var expansion: ${!var} // Handle indirect var expansion: ${!var}
if let Some(var) = s.strip_prefix('!') { if let Some(var) = s.strip_prefix('!') {
if var.ends_with('*') || var.ends_with('@') { if var.ends_with('*') || var.ends_with('@') {
@@ -1716,7 +1733,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'!' | '#' | '%' | ':' | '-' | '+' | '=' | '/' | '?' => { '!' | '#' | '%' | ':' | '-' | '+' | '^' | ',' | '=' | '/' | '?' => {
rest.push(ch); rest.push(ch);
rest.push_str(&chars.collect::<String>()); rest.push_str(&chars.collect::<String>());
break; break;
@@ -1728,6 +1745,32 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
if let Ok(expansion) = rest.parse::<ParamExp>() { if let Ok(expansion) = rest.parse::<ParamExp>() {
match expansion { match expansion {
ParamExp::Len => unreachable!(), ParamExp::Len => unreachable!(),
ParamExp::ToUpperAll => {
let value = vars.get_var(&var_name);
Ok(value.to_uppercase())
}
ParamExp::ToUpperFirst => {
let value = vars.get_var(&var_name);
let mut chars = value.chars();
let first = chars.next()
.map(|c| c.to_uppercase()
.to_string())
.unwrap_or_default();
Ok(first + chars.as_str())
}
ParamExp::ToLowerAll => {
let value = vars.get_var(&var_name);
Ok(value.to_lowercase())
}
ParamExp::ToLowerFirst => {
let value = vars.get_var(&var_name);
let mut chars = value.chars();
let first = chars.next()
.map(|c| c.to_lowercase().to_string())
.unwrap_or_default();
Ok(first + chars.as_str())
}
ParamExp::DefaultUnsetOrNull(default) => { ParamExp::DefaultUnsetOrNull(default) => {
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
Some(val) => Ok(val), Some(val) => Ok(val),

View File

@@ -8,29 +8,7 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, help::help, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, seek::seek, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
resource::{ulimit, umask_builtin},
seek::seek,
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset},
}, },
expand::{expand_aliases, expand_case_pattern, glob_to_regex}, expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -1017,6 +995,7 @@ impl Dispatcher {
"ulimit" => ulimit(cmd), "ulimit" => ulimit(cmd),
"umask" => umask_builtin(cmd), "umask" => umask_builtin(cmd),
"seek" => seek(cmd), "seek" => seek(cmd),
"help" => help(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -88,7 +88,6 @@ impl ParsedSrc {
Err(error) => return Err(vec![error]), Err(error) => return Err(vec![error]),
} }
} }
log::trace!("Tokens: {:#?}", tokens);
let mut errors = vec![]; let mut errors = vec![];
let mut nodes = vec![]; let mut nodes = vec![];
@@ -1038,7 +1037,6 @@ impl ParseStream {
Ok(Some(node)) Ok(Some(node))
} }
fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> { fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> {
log::debug!("Trying to parse a brace group");
let mut node_tks: Vec<Tk> = vec![]; let mut node_tks: Vec<Tk> = vec![];
let mut body: Vec<Node> = vec![]; let mut body: Vec<Node> = vec![];
let mut redirs: Vec<Redir> = vec![]; let mut redirs: Vec<Redir> = vec![];
@@ -1051,7 +1049,6 @@ impl ParseStream {
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
loop { loop {
log::debug!("Parsing a brace group body");
if *self.next_tk_class() == TkRule::BraceGrpEnd { if *self.next_tk_class() == TkRule::BraceGrpEnd {
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
break; break;
@@ -1078,7 +1075,6 @@ impl ParseStream {
} }
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
if !self.next_tk_is_some() { if !self.next_tk_is_some() {
log::debug!("Hit end of input while parsing a brace group body, entering panic mode");
self.panic_mode(&mut node_tks); self.panic_mode(&mut node_tks);
return Err(parse_err_full( return Err(parse_err_full(
"Expected a closing brace for this brace group", "Expected a closing brace for this brace group",
@@ -1088,15 +1084,11 @@ impl ParseStream {
} }
} }
log::debug!(
"Finished parsing brace group body, now looking for redirections if it's not a function definition"
);
if !from_func_def { if !from_func_def {
self.parse_redir(&mut redirs, &mut node_tks)?; self.parse_redir(&mut redirs, &mut node_tks)?;
} }
log::debug!("Finished parsing brace group redirections, constructing node");
let node = Node { let node = Node {
class: NdRule::BraceGrp { body }, class: NdRule::BraceGrp { body },

View File

@@ -115,6 +115,11 @@ impl IoMode {
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> { pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
Ok(Self::Buffer { tgt_fd, buf, flags }) Ok(Self::Buffer { tgt_fd, buf, flags })
} }
pub fn loaded_pipe(tgt_fd: RawFd, buf: &[u8]) -> ShResult<Self> {
let (rpipe, wpipe) = nix::unistd::pipe()?;
write(wpipe, buf)?;
Ok(Self::Pipe { tgt_fd, pipe: rpipe.into() })
}
pub fn get_pipes() -> (Self, Self) { pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap(); let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
( (

View File

@@ -739,7 +739,7 @@ impl QueryEditor {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Default, Debug)]
pub struct FuzzySelector { pub struct FuzzySelector {
query: QueryEditor, query: QueryEditor,
filtered: Vec<ScoredCandidate>, filtered: Vec<ScoredCandidate>,

View File

@@ -203,6 +203,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
.collect() .collect()
} }
#[derive(Default,Clone,Debug)]
pub struct History { pub struct History {
path: PathBuf, path: PathBuf,
pub pending: Option<LineBuf>, // command, cursor_pos pub pending: Option<LineBuf>, // command, cursor_pos
@@ -214,6 +215,7 @@ pub struct History {
//search_direction: Direction, //search_direction: Direction,
ignore_dups: bool, ignore_dups: bool,
max_size: Option<u32>, max_size: Option<u32>,
stateless: bool
} }
impl History { impl History {
@@ -229,6 +231,7 @@ impl History {
//search_direction: Direction::Backward, //search_direction: Direction::Backward,
ignore_dups: false, ignore_dups: false,
max_size: None, max_size: None,
stateless: true,
} }
} }
pub fn new() -> ShResult<Self> { pub fn new() -> ShResult<Self> {
@@ -266,6 +269,7 @@ impl History {
//search_direction: Direction::Backward, //search_direction: Direction::Backward,
ignore_dups, ignore_dups,
max_size, max_size,
stateless: false,
}) })
} }
@@ -450,6 +454,9 @@ impl History {
} }
pub fn save(&mut self) -> ShResult<()> { pub fn save(&mut self) -> ShResult<()> {
if self.stateless {
return Ok(());
}
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.create(true) .create(true)
.append(true) .append(true)

View File

@@ -421,7 +421,7 @@ impl IndentCtx {
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Clone, Debug)]
pub struct LineBuf { pub struct LineBuf {
pub buffer: String, pub buffer: String,
pub hint: Option<String>, pub hint: Option<String>,
@@ -440,6 +440,28 @@ pub struct LineBuf {
pub redo_stack: Vec<Edit>, pub redo_stack: Vec<Edit>,
} }
impl Default for LineBuf {
fn default() -> Self {
Self {
buffer: String::new(),
hint: None,
grapheme_indices: Some(vec![]),
cursor: ClampedUsize::new(0, 0, false),
select_mode: None,
select_range: None,
last_selection: None,
insert_mode_start_pos: None,
saved_col: None,
indent_ctx: IndentCtx::new(),
undo_stack: vec![],
redo_stack: vec![],
}
}
}
impl LineBuf { impl LineBuf {
pub fn new() -> Self { pub fn new() -> Self {
let mut new = Self { let mut new = Self {

View File

@@ -132,6 +132,18 @@ pub mod markers {
pub fn is_marker(c: Marker) -> bool { pub fn is_marker(c: Marker) -> bool {
('\u{e000}'..'\u{efff}').contains(&c) ('\u{e000}'..'\u{efff}').contains(&c)
} }
// Help command formatting markers
pub const TAG: Marker = '\u{e180}';
pub const REFERENCE: Marker = '\u{e181}';
pub const HEADER: Marker = '\u{e182}';
pub const CODE: Marker = '\u{e183}';
/// angle brackets
pub const KEYWORD_1: Marker = '\u{e184}';
/// curly brackets
pub const KEYWORD_2: Marker = '\u{e185}';
/// square brackets
pub const KEYWORD_3: Marker = '\u{e186}';
} }
type Marker = char; type Marker = char;
@@ -256,6 +268,7 @@ pub struct ShedVi {
pub old_layout: Option<Layout>, pub old_layout: Option<Layout>,
pub history: History, pub history: History,
pub ex_history: History,
pub needs_redraw: bool, pub needs_redraw: bool,
} }
@@ -277,6 +290,7 @@ impl ShedVi {
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new(), editor: LineBuf::new(),
history: History::new()?, history: History::new()?,
ex_history: History::empty(),
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| { write_vars(|v| {
@@ -308,6 +322,7 @@ impl ShedVi {
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new(), editor: LineBuf::new(),
history: History::empty(), history: History::empty(),
ex_history: History::empty(),
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| { write_vars(|v| {
@@ -798,7 +813,8 @@ impl ShedVi {
let Ok(cmd) = self.mode.handle_key_fallible(key) else { let Ok(cmd) = self.mode.handle_key_fallible(key) else {
// it's an ex mode error // it's an ex mode error
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>; self.swap_mode(&mut (Box::new(ViNormal::new()) as Box<dyn ViMode>));
return Ok(None); return Ok(None);
}; };
@@ -844,9 +860,22 @@ impl ShedVi {
} }
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
log::debug!("is_ex_cmd: {is_ex_cmd}");
if is_shell_cmd {
self.old_layout = None;
}
if is_ex_cmd {
self.ex_history.push(cmd.raw_seq.clone());
self.ex_history.reset();
log::debug!("ex_history: {:?}", self.ex_history.entries());
}
let before = self.editor.buffer.clone(); let before = self.editor.buffer.clone();
self.exec_cmd(cmd, false)?; self.exec_cmd(cmd, false)?;
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) { if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
for key in keys { for key in keys {
self.handle_key(key)?; self.handle_key(key)?;
@@ -1131,7 +1160,7 @@ impl ShedVi {
) )
} }
Verb::ExMode => Box::new(ViEx::new()), Verb::ExMode => Box::new(ViEx::new(self.ex_history.clone())),
Verb::VerbatimMode => { Verb::VerbatimMode => {
self.reader.verbatim_single = true; self.reader.verbatim_single = true;
@@ -1221,7 +1250,7 @@ impl ShedVi {
ModeReport::Normal => Box::new(ViNormal::new()), ModeReport::Normal => Box::new(ViNormal::new()),
ModeReport::Insert => Box::new(ViInsert::new()), ModeReport::Insert => Box::new(ViInsert::new()),
ModeReport::Visual => Box::new(ViVisual::new()), ModeReport::Visual => Box::new(ViVisual::new()),
ModeReport::Ex => Box::new(ViEx::new()), ModeReport::Ex => Box::new(ViEx::new(self.ex_history.clone())),
ModeReport::Replace => Box::new(ViReplace::new()), ModeReport::Replace => Box::new(ViReplace::new()),
ModeReport::Verbatim => Box::new(ViVerbatim::new()), ModeReport::Verbatim => Box::new(ViVerbatim::new()),
ModeReport::Unknown => unreachable!(), ModeReport::Unknown => unreachable!(),
@@ -1266,7 +1295,7 @@ impl ShedVi {
ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>, ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>,
ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>, ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>,
ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>, ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>,
ModeReport::Ex => Box::new(ViEx::new()) as Box<dyn ViMode>, ModeReport::Ex => Box::new(ViEx::new(self.ex_history.clone())) as Box<dyn ViMode>,
ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>, ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>,
ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>, ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>,
ModeReport::Unknown => unreachable!(), ModeReport::Unknown => unreachable!(),

View File

@@ -64,6 +64,7 @@ bitflags! {
const VISUAL_LINE = 1<<1; const VISUAL_LINE = 1<<1;
const VISUAL_BLOCK = 1<<2; const VISUAL_BLOCK = 1<<2;
const EXIT_CUR_MODE = 1<<3; const EXIT_CUR_MODE = 1<<3;
const IS_EX_CMD = 1<<4;
} }
} }

View File

@@ -6,6 +6,7 @@ use itertools::Itertools;
use crate::bitflags; use crate::bitflags;
use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::readline::history::History;
use crate::readline::keys::KeyEvent; use crate::readline::keys::KeyEvent;
use crate::readline::linebuf::LineBuf; use crate::readline::linebuf::LineBuf;
use crate::readline::vicmd::{ use crate::readline::vicmd::{
@@ -33,16 +34,64 @@ bitflags! {
struct ExEditor { struct ExEditor {
buf: LineBuf, buf: LineBuf,
mode: ViInsert, mode: ViInsert,
history: History
} }
impl ExEditor { impl ExEditor {
pub fn new(history: History) -> Self {
let mut new = Self {
history,
..Default::default()
};
new.buf.update_graphemes();
new
}
pub fn clear(&mut self) { pub fn clear(&mut self) {
*self = Self::default() *self = Self::default()
} }
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
cmd.verb().is_none()
&& (cmd
.motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
&& self.buf.start_of_line() == 0)
|| (cmd
.motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
&& self.buf.end_of_line() == self.buf.cursor_max())
}
pub fn scroll_history(&mut self, cmd: ViCmd) {
let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1;
let count = match motion {
Motion::LineUpCharwise => -(*count as isize),
Motion::LineDownCharwise => *count as isize,
_ => unreachable!(),
};
let entry = self.history.scroll(count);
if let Some(entry) = entry {
let buf = std::mem::take(&mut self.buf);
self.buf.set_buffer(entry.command().to_string());
if self.history.pending.is_none() {
self.history.pending = Some(buf);
}
self.buf.set_hint(None);
self.buf.move_cursor_to_end();
} else if let Some(pending) = self.history.pending.take() {
self.buf = pending;
}
}
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> { pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else { let Some(mut cmd) = self.mode.handle_key(key) else {
return Ok(()); return Ok(());
}; };
cmd.alter_line_motion_if_no_verb();
log::debug!("ExEditor got cmd: {:?}", cmd);
if self.should_grab_history(&cmd) {
log::debug!("Grabbing history for cmd: {:?}", cmd);
self.scroll_history(cmd);
return Ok(())
}
self.buf.exec_cmd(cmd) self.buf.exec_cmd(cmd)
} }
} }
@@ -53,8 +102,8 @@ pub struct ViEx {
} }
impl ViEx { impl ViEx {
pub fn new() -> Self { pub fn new(history: History) -> Self {
Self::default() Self { pending_cmd: ExEditor::new(history) }
} }
} }
@@ -62,18 +111,14 @@ impl ViMode for ViEx {
// Ex mode can return errors, so we use this fallible method instead of the normal one // Ex mode can return errors, so we use this fallible method instead of the normal one
fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> { fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> {
use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M};
log::debug!("[ViEx] handle_key_fallible: key={:?}", key);
match key { match key {
E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => { E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
let input = self.pending_cmd.buf.as_str(); let input = self.pending_cmd.buf.as_str();
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
match parse_ex_cmd(input) { match parse_ex_cmd(input) {
Ok(cmd) => { Ok(cmd) => {
log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd);
Ok(cmd) Ok(cmd)
} }
Err(e) => { Err(e) => {
log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e);
let msg = e.unwrap_or(format!("Not an editor command: {}", input)); let msg = e.unwrap_or(format!("Not an editor command: {}", input));
write_meta(|m| m.post_system_message(msg.clone())); write_meta(|m| m.post_system_message(msg.clone()));
Err(ShErr::simple(ShErrKind::ParseErr, msg)) Err(ShErr::simple(ShErrKind::ParseErr, msg))
@@ -81,12 +126,10 @@ impl ViMode for ViEx {
} }
} }
E(C::Char('C'), M::CTRL) => { E(C::Char('C'), M::CTRL) => {
log::debug!("[ViEx] Ctrl-C, clearing");
self.pending_cmd.clear(); self.pending_cmd.clear();
Ok(None) Ok(None)
} }
E(C::Esc, M::NONE) => { E(C::Esc, M::NONE) => {
log::debug!("[ViEx] Esc, returning to normal mode");
Ok(Some(ViCmd { Ok(Some(ViCmd {
register: RegisterName::default(), register: RegisterName::default(),
verb: Some(VerbCmd(1, Verb::NormalMode)), verb: Some(VerbCmd(1, Verb::NormalMode)),
@@ -96,14 +139,12 @@ impl ViMode for ViEx {
})) }))
} }
_ => { _ => {
log::debug!("[ViEx] forwarding key to ExEditor");
self.pending_cmd.handle_key(key).map(|_| None) self.pending_cmd.handle_key(key).map(|_| None)
} }
} }
} }
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> { fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
let result = self.handle_key_fallible(key); let result = self.handle_key_fallible(key);
log::debug!("[ViEx] handle_key result: {:?}", result);
result.ok().flatten() result.ok().flatten()
} }
fn is_repeatable(&self) -> bool { fn is_repeatable(&self) -> bool {
@@ -177,7 +218,7 @@ fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
verb, verb,
motion, motion,
raw_seq: raw.to_string(), raw_seq: raw.to_string(),
flags: CmdFlags::EXIT_CUR_MODE, flags: CmdFlags::EXIT_CUR_MODE | CmdFlags::IS_EX_CMD,
})) }))
} }
@@ -224,6 +265,10 @@ fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Opt
let cmd = unescape_shell_cmd(&cmd); let cmd = unescape_shell_cmd(&cmd);
Ok(Some(Verb::ShellCmd(cmd))) Ok(Some(Verb::ShellCmd(cmd)))
} }
_ if "help".starts_with(&cmd_name) => {
let cmd = "help ".to_string() + chars.collect::<String>().trim();
Ok(Some(Verb::ShellCmd(cmd)))
}
"normal!" => parse_normal(chars), "normal!" => parse_normal(chars),
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),

View File

@@ -1098,6 +1098,8 @@ impl VarTab {
.map(|hname| hname.to_string_lossy().to_string()) .map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default(); .unwrap_or_default();
let help_paths = format!("/usr/share/shed/doc:{home}/.local/share/shed/doc");
unsafe { unsafe {
env::set_var("IFS", " \t\n"); env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone()); env::set_var("HOST", hostname.clone());
@@ -1114,6 +1116,7 @@ impl VarTab {
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env::set_var("SHED_HIST", format!("{}/.shedhist", home)); env::set_var("SHED_HIST", format!("{}/.shedhist", home));
env::set_var("SHED_RC", format!("{}/.shedrc", home)); env::set_var("SHED_RC", format!("{}/.shedrc", home));
env::set_var("SHED_HPATH", help_paths);
} }
} }
pub fn init_sh_argv(&mut self) { pub fn init_sh_argv(&mut self) {