Added 'read_key' builtin that allows widget scripts to handle input
This commit is contained in:
@@ -27,12 +27,12 @@ pub mod intro;
|
|||||||
pub mod getopts;
|
pub mod getopts;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 45] = [
|
pub const BUILTINS: [&str; 46] = [
|
||||||
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
|
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
|
||||||
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
|
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "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"
|
"getopts", "keymap", "read_key"
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn true_builtin() -> ShResult<()> {
|
pub fn true_builtin() -> ShResult<()> {
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ use nix::{
|
|||||||
libc::{STDIN_FILENO, STDOUT_FILENO},
|
libc::{STDIN_FILENO, STDOUT_FILENO},
|
||||||
unistd::{isatty, read, write},
|
unistd::{isatty, read, write},
|
||||||
};
|
};
|
||||||
|
use yansi::Paint;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, sys::TTY_FILENO}, parse::{NdRule, Node, execute::prepare_argv}, procio::borrow_fd, readline::term::{KeyReader, PollReader, RawModeGuard}, state::{self, VarFlags, VarKind, read_vars, write_vars}
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
|
||||||
parse::{NdRule, Node, execute::prepare_argv},
|
|
||||||
procio::borrow_fd,
|
|
||||||
readline::term::RawModeGuard,
|
|
||||||
state::{self, VarFlags, VarKind, read_vars, write_vars},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const READ_OPTS: [OptSpec; 7] = [
|
pub const READ_OPTS: [OptSpec; 7] = [
|
||||||
@@ -45,6 +41,21 @@ pub const READ_OPTS: [OptSpec; 7] = [
|
|||||||
}, // read until delimiter
|
}, // read until delimiter
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub const READ_KEY_OPTS: [OptSpec;3] = [
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('v'), // var name
|
||||||
|
takes_arg: true
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('w'), // char whitelist
|
||||||
|
takes_arg: true
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('b'), // char blacklist
|
||||||
|
takes_arg: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
pub struct ReadFlags: u32 {
|
pub struct ReadFlags: u32 {
|
||||||
const NO_ESCAPES = 0b000001;
|
const NO_ESCAPES = 0b000001;
|
||||||
@@ -245,3 +256,98 @@ pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
|
|||||||
|
|
||||||
Ok(read_opts)
|
Ok(read_opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ReadKeyOpts {
|
||||||
|
var_name: Option<String>,
|
||||||
|
char_whitelist: Option<String>,
|
||||||
|
char_blacklist: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_key(node: Node) -> ShResult<()> {
|
||||||
|
let blame = node.get_span().clone();
|
||||||
|
let NdRule::Command { argv, .. } = node.class else { unreachable!() };
|
||||||
|
|
||||||
|
if !isatty(*TTY_FILENO)? {
|
||||||
|
state::set_status(1);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, opts) = get_opts_from_tokens(argv, &READ_KEY_OPTS).blame(blame.clone())?;
|
||||||
|
let read_key_opts = get_read_key_opts(opts).blame(blame.clone())?;
|
||||||
|
|
||||||
|
let key = {
|
||||||
|
let _raw = crate::readline::term::raw_mode();
|
||||||
|
let mut buf = [0u8; 16];
|
||||||
|
match read(*TTY_FILENO, &mut buf) {
|
||||||
|
Ok(0) => {
|
||||||
|
state::set_status(1);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
let mut reader = PollReader::new();
|
||||||
|
reader.feed_bytes(&buf[..n]);
|
||||||
|
let Some(key) = reader.read_key()? else {
|
||||||
|
state::set_status(1);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
key
|
||||||
|
},
|
||||||
|
Err(Errno::EINTR) => {
|
||||||
|
state::set_status(130);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => return Err(ShErr::simple(ShErrKind::ExecFail, format!("read_key: {e}"))),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let vim_seq = key.as_vim_seq()?;
|
||||||
|
|
||||||
|
if let Some(wl) = read_key_opts.char_whitelist {
|
||||||
|
let allowed = expand_keymap(&wl);
|
||||||
|
if !allowed.contains(&key) {
|
||||||
|
state::set_status(1);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bl) = read_key_opts.char_blacklist {
|
||||||
|
let disallowed = expand_keymap(&bl);
|
||||||
|
if disallowed.contains(&key) {
|
||||||
|
state::set_status(1);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(var) = read_key_opts.var_name {
|
||||||
|
write_vars(|v| v.set_var(&var, VarKind::Str(vim_seq), VarFlags::NONE))?;
|
||||||
|
} else {
|
||||||
|
write(borrow_fd(STDOUT_FILENO), vim_seq.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state::set_status(0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
|
||||||
|
let mut read_key_opts = ReadKeyOpts {
|
||||||
|
var_name: None,
|
||||||
|
char_whitelist: None,
|
||||||
|
char_blacklist: None
|
||||||
|
};
|
||||||
|
|
||||||
|
for opt in opts {
|
||||||
|
match opt {
|
||||||
|
Opt::ShortWithArg('v', var_name) => read_key_opts.var_name = Some(var_name),
|
||||||
|
Opt::ShortWithArg('w', char_whitelist) => read_key_opts.char_whitelist = Some(char_whitelist),
|
||||||
|
Opt::ShortWithArg('b', char_blacklist) => read_key_opts.char_blacklist = Some(char_blacklist),
|
||||||
|
_ => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::ExecFail,
|
||||||
|
format!("read_key: Unexpected flag '{opt}'")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(read_key_opts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
|||||||
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
|
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
use crate::readline::markers;
|
use crate::readline::markers;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars
|
self, ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
@@ -925,7 +925,8 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
|||||||
e.print_error();
|
e.print_error();
|
||||||
unsafe { libc::_exit(1) };
|
unsafe { libc::_exit(1) };
|
||||||
}
|
}
|
||||||
unsafe { libc::_exit(0) };
|
let status = state::get_status();
|
||||||
|
unsafe { libc::_exit(status) };
|
||||||
}
|
}
|
||||||
ForkResult::Parent { child } => {
|
ForkResult::Parent { child } => {
|
||||||
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
|
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
|
||||||
@@ -950,7 +951,10 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
WtStat::Exited(_, _) => Ok(io_buf.as_str()?.trim_end().to_string()),
|
WtStat::Exited(_, code) => {
|
||||||
|
state::set_status(code);
|
||||||
|
Ok(io_buf.as_str()?.trim_end().to_string())
|
||||||
|
},
|
||||||
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
|
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1386,8 +1390,11 @@ impl FromStr for ParamExp {
|
|||||||
pub fn parse_pos_len(s: &str) -> Option<(usize, Option<usize>)> {
|
pub fn parse_pos_len(s: &str) -> Option<(usize, Option<usize>)> {
|
||||||
let raw = s.strip_prefix(':')?;
|
let raw = s.strip_prefix(':')?;
|
||||||
if let Some((start, len)) = raw.split_once(':') {
|
if let Some((start, len)) = raw.split_once(':') {
|
||||||
|
let start = expand_raw(&mut start.chars().peekable()).unwrap_or_else(|_| start.to_string());
|
||||||
|
let len = expand_raw(&mut len.chars().peekable()).unwrap_or_else(|_| len.to_string());
|
||||||
Some((start.parse::<usize>().ok()?, len.parse::<usize>().ok()))
|
Some((start.parse::<usize>().ok()?, len.parse::<usize>().ok()))
|
||||||
} else {
|
} else {
|
||||||
|
let raw = expand_raw(&mut raw.chars().peekable()).unwrap_or_else(|_| raw.to_string());
|
||||||
Some((raw.parse::<usize>().ok()?, None))
|
Some((raw.parse::<usize>().ok()?, None))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1620,8 +1627,9 @@ pub fn glob_to_regex(glob: &str, anchored: bool) -> Regex {
|
|||||||
'\\' => {
|
'\\' => {
|
||||||
// Shell escape: next char is literal
|
// Shell escape: next char is literal
|
||||||
if let Some(esc) = chars.next() {
|
if let Some(esc) = chars.next() {
|
||||||
regex.push('\\');
|
// Some characters have special meaning after \ in regex
|
||||||
regex.push(esc);
|
// (e.g. \< is word boundary), so use hex escape for safety
|
||||||
|
regex.push_str(&format!("\\x{:02x}", esc as u32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'*' => regex.push_str(".*"),
|
'*' => regex.push_str(".*"),
|
||||||
@@ -2145,7 +2153,6 @@ pub fn expand_aliases(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
|
pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
|
||||||
log::debug!("Expanding keymap for '{}'", s);
|
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
let mut chars = s.chars().collect::<VecDeque<char>>();
|
let mut chars = s.chars().collect::<VecDeque<char>>();
|
||||||
while let Some(ch) = chars.pop_front() {
|
while let Some(ch) = chars.pop_front() {
|
||||||
@@ -2165,13 +2172,11 @@ pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
'>' => {
|
'>' => {
|
||||||
log::debug!("Found key alias '{}'", alias);
|
|
||||||
if alias.eq_ignore_ascii_case("leader") {
|
if alias.eq_ignore_ascii_case("leader") {
|
||||||
let mut leader = read_shopts(|o| o.prompt.leader.clone());
|
let mut leader = read_shopts(|o| o.prompt.leader.clone());
|
||||||
if leader == "\\" {
|
if leader == "\\" {
|
||||||
leader.push('\\');
|
leader.push('\\');
|
||||||
}
|
}
|
||||||
log::debug!("Expanding leader key to '{}'", leader);
|
|
||||||
keys.extend(expand_keymap(&leader));
|
keys.extend(expand_keymap(&leader));
|
||||||
} else if let Some(key) = parse_key_alias(&alias) {
|
} else if let Some(key) = parse_key_alias(&alias) {
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
@@ -2188,7 +2193,6 @@ pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Expanded keymap '{}' to {:?}", s, keys);
|
|
||||||
keys
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ pub trait ShResultExt {
|
|||||||
fn blame(self, span: Span) -> Self;
|
fn blame(self, span: Span) -> Self;
|
||||||
fn try_blame(self, span: Span) -> Self;
|
fn try_blame(self, span: Span) -> Self;
|
||||||
fn promote_err(self, span: Span) -> Self;
|
fn promote_err(self, span: Span) -> Self;
|
||||||
|
fn is_flow_control(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ShResultExt for Result<T, ShErr> {
|
impl<T> ShResultExt for Result<T, ShErr> {
|
||||||
@@ -101,6 +102,9 @@ impl<T> ShResultExt for Result<T, ShErr> {
|
|||||||
fn promote_err(self, span: Span) -> Self {
|
fn promote_err(self, span: Span) -> Self {
|
||||||
self.map_err(|e| e.promote(span))
|
self.map_err(|e| e.promote(span))
|
||||||
}
|
}
|
||||||
|
fn is_flow_control(&self) -> bool {
|
||||||
|
self.as_ref().is_err_and(|e| e.is_flow_control())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -179,6 +183,9 @@ impl ShErr {
|
|||||||
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
|
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
|
||||||
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] }
|
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] }
|
||||||
}
|
}
|
||||||
|
pub fn is_flow_control(&self) -> bool {
|
||||||
|
self.kind.is_flow_control()
|
||||||
|
}
|
||||||
pub fn promote(mut self, span: Span) -> Self {
|
pub fn promote(mut self, span: Span) -> Self {
|
||||||
if self.notes.is_empty() {
|
if self.notes.is_empty() {
|
||||||
return self
|
return self
|
||||||
@@ -356,6 +363,18 @@ pub enum ShErrKind {
|
|||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ShErrKind {
|
||||||
|
pub fn is_flow_control(&self) -> bool {
|
||||||
|
matches!(self,
|
||||||
|
Self::CleanExit(_) |
|
||||||
|
Self::FuncReturn(_) |
|
||||||
|
Self::LoopContinue(_) |
|
||||||
|
Self::LoopBreak(_) |
|
||||||
|
Self::ClearReadline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for ShErrKind {
|
impl Display for ShErrKind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let output = match self {
|
let output = match self {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use ariadne::Fmt;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
builtin::{
|
||||||
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, 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::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak
|
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, 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}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak
|
||||||
},
|
},
|
||||||
expand::{expand_aliases, glob_to_regex},
|
expand::{expand_aliases, glob_to_regex},
|
||||||
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
||||||
@@ -423,13 +423,16 @@ impl Dispatcher {
|
|||||||
|
|
||||||
'outer: for block in case_blocks {
|
'outer: for block in case_blocks {
|
||||||
let CaseNode { pattern, body } = block;
|
let CaseNode { pattern, body } = block;
|
||||||
let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim();
|
let block_pattern_raw = pattern.span.as_str().strip_suffix(')').unwrap_or(pattern.span.as_str()).trim();
|
||||||
|
log::debug!("[case] raw block pattern: {:?}", block_pattern_raw);
|
||||||
// Split at '|' to allow for multiple patterns like `foo|bar)`
|
// Split at '|' to allow for multiple patterns like `foo|bar)`
|
||||||
let block_patterns = block_pattern_raw.split('|');
|
let block_patterns = block_pattern_raw.split('|');
|
||||||
|
|
||||||
for pattern in block_patterns {
|
for pattern in block_patterns {
|
||||||
let pattern_regex = glob_to_regex(pattern, false);
|
let pattern_regex = glob_to_regex(pattern, false);
|
||||||
|
log::debug!("[case] testing input {:?} against pattern {:?} (regex: {:?})", pattern_raw, pattern, pattern_regex);
|
||||||
if pattern_regex.is_match(&pattern_raw) {
|
if pattern_regex.is_match(&pattern_raw) {
|
||||||
|
log::debug!("[case] matched pattern {:?}", pattern);
|
||||||
for node in &body {
|
for node in &body {
|
||||||
s.dispatch_node(node.clone())?;
|
s.dispatch_node(node.clone())?;
|
||||||
}
|
}
|
||||||
@@ -824,6 +827,7 @@ impl Dispatcher {
|
|||||||
"type" => intro::type_builtin(cmd),
|
"type" => intro::type_builtin(cmd),
|
||||||
"getopts" => getopts(cmd),
|
"getopts" => getopts(cmd),
|
||||||
"keymap" => keymap::keymap(cmd),
|
"keymap" => keymap::keymap(cmd),
|
||||||
|
"read_key" => read::read_key(cmd),
|
||||||
"true" | ":" => {
|
"true" | ":" => {
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -836,6 +840,9 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
|
if !e.is_flow_control() {
|
||||||
|
state::set_status(1);
|
||||||
|
}
|
||||||
Err(e.with_context(context).with_redirs(redir_guard))
|
Err(e.with_context(context).with_redirs(redir_guard))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -860,7 +867,6 @@ impl Dispatcher {
|
|||||||
let no_fork = cmd.flags.contains(NdFlags::NO_FORK);
|
let no_fork = cmd.flags.contains(NdFlags::NO_FORK);
|
||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
state::set_status(0);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,26 @@ bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clean_input(input: &str) -> String {
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
let mut output = String::new();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'\\' if chars.peek() == Some(&'\n') => {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
'\r' => {
|
||||||
|
if chars.peek() == Some(&'\n') {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
_ => output.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
impl LexStream {
|
impl LexStream {
|
||||||
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
||||||
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
||||||
@@ -825,11 +845,14 @@ impl Iterator for LexStream {
|
|||||||
self.set_next_is_cmd(true);
|
self.set_next_is_cmd(true);
|
||||||
|
|
||||||
while let Some(ch) = get_char(&self.source, self.cursor) {
|
while let Some(ch) = get_char(&self.source, self.cursor) {
|
||||||
if is_hard_sep(ch) {
|
match ch {
|
||||||
// Combine consecutive separators into one, including whitespace
|
'\\' => {
|
||||||
|
self.cursor = (self.cursor + 2).min(self.source.len());
|
||||||
|
}
|
||||||
|
_ if is_hard_sep(ch) => {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
} else {
|
}
|
||||||
break;
|
_ => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
||||||
@@ -1060,6 +1083,7 @@ pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> {
|
|||||||
|
|
||||||
pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
|
pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
|
let mut qt_state = QuoteState::default();
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
pos += ch.len_utf8();
|
pos += ch.len_utf8();
|
||||||
match ch {
|
match ch {
|
||||||
@@ -1069,8 +1093,14 @@ pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
|
|||||||
pos += esc.len_utf8();
|
pos += esc.len_utf8();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
')' => return Some(pos),
|
'\'' => {
|
||||||
'(' => return None,
|
qt_state.toggle_single();
|
||||||
|
}
|
||||||
|
'"' => {
|
||||||
|
qt_state.toggle_double();
|
||||||
|
}
|
||||||
|
')' if qt_state.outside() => return Some(pos),
|
||||||
|
'(' if qt_state.outside() => return None,
|
||||||
_ => { /* continue */ }
|
_ => { /* continue */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ use crate::{
|
|||||||
libsh::{
|
libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult, last_color, next_color},
|
error::{ShErr, ShErrKind, ShResult, last_color, next_color},
|
||||||
utils::{NodeVecUtils, TkVecUtils},
|
utils::{NodeVecUtils, TkVecUtils},
|
||||||
},
|
}, parse::lex::clean_input, prelude::*, procio::IoMode
|
||||||
prelude::*,
|
|
||||||
procio::IoMode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod execute;
|
pub mod execute;
|
||||||
@@ -52,6 +50,11 @@ pub struct ParsedSrc {
|
|||||||
|
|
||||||
impl ParsedSrc {
|
impl ParsedSrc {
|
||||||
pub fn new(src: Arc<String>) -> Self {
|
pub fn new(src: Arc<String>) -> Self {
|
||||||
|
let src = if src.contains("\\\n") || src.contains('\r') {
|
||||||
|
Arc::new(clean_input(&src))
|
||||||
|
} else {
|
||||||
|
src
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
src,
|
src,
|
||||||
name: "<stdin>".into(),
|
name: "<stdin>".into(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
|
|
||||||
// Credit to Rustyline for the design ideas in this module
|
// Credit to Rustyline for the design ideas in this module
|
||||||
// https://github.com/kkawakam/rustyline
|
// https://github.com/kkawakam/rustyline
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
@@ -87,6 +89,109 @@ impl KeyEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn as_vim_seq(&self) -> ShResult<String> {
|
||||||
|
let mut seq = String::new();
|
||||||
|
let KeyEvent(event, mods) = self;
|
||||||
|
let mut needs_angle_bracket = false;
|
||||||
|
|
||||||
|
if mods.contains(ModKeys::CTRL) {
|
||||||
|
seq.push_str("C-");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
if mods.contains(ModKeys::ALT) {
|
||||||
|
seq.push_str("A-");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
if mods.contains(ModKeys::SHIFT) {
|
||||||
|
seq.push_str("S-");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
KeyCode::UnknownEscSeq => return Err(ShErr::simple(
|
||||||
|
ShErrKind::ParseErr,
|
||||||
|
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
|
||||||
|
)),
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
seq.push_str("BS");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::BackTab => {
|
||||||
|
seq.push_str("S-Tab");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::BracketedPasteStart => todo!(),
|
||||||
|
KeyCode::BracketedPasteEnd => todo!(),
|
||||||
|
KeyCode::Delete => {
|
||||||
|
seq.push_str("Del");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
seq.push_str("Down");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::End => {
|
||||||
|
seq.push_str("End");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
seq.push_str("Enter");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
seq.push_str("Esc");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyCode::F(f) => {
|
||||||
|
seq.push_str(&format!("F{}", f));
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Home => {
|
||||||
|
seq.push_str("Home");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Insert => {
|
||||||
|
seq.push_str("Insert");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
seq.push_str("Left");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Null => todo!(),
|
||||||
|
KeyCode::PageDown => {
|
||||||
|
seq.push_str("PgDn");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::PageUp => {
|
||||||
|
seq.push_str("PgUp");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
seq.push_str("Right");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Tab => {
|
||||||
|
seq.push_str("Tab");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
seq.push_str("Up");
|
||||||
|
needs_angle_bracket = true;
|
||||||
|
}
|
||||||
|
KeyCode::Char(ch) => {
|
||||||
|
seq.push(*ch);
|
||||||
|
}
|
||||||
|
KeyCode::Grapheme(gr) => seq.push_str(gr),
|
||||||
|
}
|
||||||
|
|
||||||
|
if needs_angle_bracket {
|
||||||
|
Ok(format!("<{}>", seq))
|
||||||
|
} else {
|
||||||
|
Ok(seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::{
|
|||||||
register::{RegisterContent, write_register},
|
register::{RegisterContent, write_register},
|
||||||
term::RawModeGuard,
|
term::RawModeGuard,
|
||||||
},
|
},
|
||||||
state::{VarFlags, VarKind, read_shopts, read_vars, write_vars},
|
state::{VarFlags, VarKind, read_shopts, read_vars, write_meta, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
||||||
@@ -926,6 +926,7 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
|
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
|
||||||
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
|
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
|
||||||
|
log::debug!("clamped_pos: {}", clamped_pos.get());
|
||||||
let cur_char = self
|
let cur_char = self
|
||||||
.grapheme_at(clamped_pos.get())
|
.grapheme_at(clamped_pos.get())
|
||||||
.map(|c| c.to_string())
|
.map(|c| c.to_string())
|
||||||
@@ -996,7 +997,11 @@ impl LineBuf {
|
|||||||
} else {
|
} else {
|
||||||
self.start_of_word_backward(self.cursor.get(), word)
|
self.start_of_word_backward(self.cursor.get(), word)
|
||||||
};
|
};
|
||||||
let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true);
|
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
|
||||||
|
self.cursor.get()
|
||||||
|
} else {
|
||||||
|
self.end_of_word_forward(self.cursor.get(), word)
|
||||||
|
};
|
||||||
Some((start, end))
|
Some((start, end))
|
||||||
}
|
}
|
||||||
Bound::Around => {
|
Bound::Around => {
|
||||||
@@ -3083,29 +3088,45 @@ impl LineBuf {
|
|||||||
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
||||||
|
|
||||||
Verb::ShellCmd(cmd) => {
|
Verb::ShellCmd(cmd) => {
|
||||||
|
log::debug!("Executing ex-mode command from widget: {cmd}");
|
||||||
let mut vars = HashSet::new();
|
let mut vars = HashSet::new();
|
||||||
vars.insert("BUFFER".into());
|
vars.insert("_BUFFER".into());
|
||||||
vars.insert("CURSOR".into());
|
vars.insert("_CURSOR".into());
|
||||||
|
vars.insert("_ANCHOR".into());
|
||||||
let _guard = var_ctx_guard(vars);
|
let _guard = var_ctx_guard(vars);
|
||||||
|
|
||||||
let mut buf = self.as_str().to_string();
|
let mut buf = self.as_str().to_string();
|
||||||
let mut cursor = self.cursor.get();
|
let mut cursor = self.cursor.get();
|
||||||
|
let mut anchor = self.select_range().map(|r| if r.0 != cursor { r.0 } else { r.1 }).unwrap_or(cursor);
|
||||||
|
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
|
v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
|
||||||
v.set_var("CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)
|
v.set_var("_CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)?;
|
||||||
|
v.set_var("_ANCHOR", VarKind::Str(anchor.to_string()), VarFlags::EXPORT)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?;
|
RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?;
|
||||||
|
|
||||||
read_vars(|v| {
|
let keys = write_vars(|v| {
|
||||||
buf = v.get_var("BUFFER");
|
buf = v.take_var("_BUFFER");
|
||||||
cursor = v.get_var("CURSOR").parse().unwrap_or(cursor);
|
cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor);
|
||||||
|
anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor);
|
||||||
|
v.take_var("_KEYS")
|
||||||
});
|
});
|
||||||
|
|
||||||
self.set_buffer(buf);
|
self.set_buffer(buf);
|
||||||
|
self.update_graphemes();
|
||||||
self.cursor.set_max(self.buffer.graphemes(true).count());
|
self.cursor.set_max(self.buffer.graphemes(true).count());
|
||||||
self.cursor.set(cursor);
|
self.cursor.set(cursor);
|
||||||
|
log::debug!("[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}", cursor, anchor, self.select_range);
|
||||||
|
if anchor != cursor && self.select_range.is_some() {
|
||||||
|
self.select_range = Some(ordered(cursor, anchor));
|
||||||
|
}
|
||||||
|
if !keys.is_empty() {
|
||||||
|
log::debug!("Pending widget keys from shell command: {keys}");
|
||||||
|
write_meta(|m| m.set_pending_widget_keys(&keys))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Verb::Normal(_)
|
Verb::Normal(_)
|
||||||
| Verb::Read(_)
|
| Verb::Read(_)
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
|
|||||||
use crate::expand::expand_prompt;
|
use crate::expand::expand_prompt;
|
||||||
use crate::libsh::sys::TTY_FILENO;
|
use crate::libsh::sys::TTY_FILENO;
|
||||||
use crate::parse::lex::{LexStream, QuoteState};
|
use crate::parse::lex::{LexStream, QuoteState};
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, state};
|
||||||
use crate::readline::complete::FuzzyCompleter;
|
use crate::readline::complete::FuzzyCompleter;
|
||||||
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
||||||
use crate::readline::vimode::ViEx;
|
use crate::readline::vimode::ViEx;
|
||||||
use crate::state::{ShellParam, read_logic, read_shopts};
|
use crate::state::{ShellParam, read_logic, read_shopts, write_meta};
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
||||||
@@ -148,6 +148,9 @@ impl Prompt {
|
|||||||
let Ok(ps1_raw) = env::var("PS1") else {
|
let Ok(ps1_raw) = env::var("PS1") else {
|
||||||
return Self::default();
|
return Self::default();
|
||||||
};
|
};
|
||||||
|
// PS1 expansion may involve running commands (e.g., for \h or \W), which can modify shell state
|
||||||
|
let saved_status = state::get_status();
|
||||||
|
|
||||||
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
|
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
|
||||||
return Self::default();
|
return Self::default();
|
||||||
};
|
};
|
||||||
@@ -158,6 +161,9 @@ impl Prompt {
|
|||||||
.transpose()
|
.transpose()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
|
// Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt
|
||||||
|
state::set_status(saved_status);
|
||||||
Self {
|
Self {
|
||||||
ps1_expanded,
|
ps1_expanded,
|
||||||
ps1_raw,
|
ps1_raw,
|
||||||
@@ -213,10 +219,12 @@ pub struct ShedVi {
|
|||||||
pub completer: Box<dyn Completer>,
|
pub completer: Box<dyn Completer>,
|
||||||
|
|
||||||
pub mode: Box<dyn ViMode>,
|
pub mode: Box<dyn ViMode>,
|
||||||
|
pub saved_mode: Option<Box<dyn ViMode>>,
|
||||||
pub pending_keymap: Vec<KeyEvent>,
|
pub pending_keymap: Vec<KeyEvent>,
|
||||||
pub repeat_action: Option<CmdReplay>,
|
pub repeat_action: Option<CmdReplay>,
|
||||||
pub repeat_motion: Option<MotionCmd>,
|
pub repeat_motion: Option<MotionCmd>,
|
||||||
pub editor: LineBuf,
|
pub editor: LineBuf,
|
||||||
|
pub next_is_escaped: bool,
|
||||||
|
|
||||||
pub old_layout: Option<Layout>,
|
pub old_layout: Option<Layout>,
|
||||||
pub history: History,
|
pub history: History,
|
||||||
@@ -233,6 +241,8 @@ impl ShedVi {
|
|||||||
completer: Box::new(FuzzyCompleter::default()),
|
completer: Box::new(FuzzyCompleter::default()),
|
||||||
highlighter: Highlighter::new(),
|
highlighter: Highlighter::new(),
|
||||||
mode: Box::new(ViInsert::new()),
|
mode: Box::new(ViInsert::new()),
|
||||||
|
next_is_escaped: false,
|
||||||
|
saved_mode: None,
|
||||||
pending_keymap: Vec::new(),
|
pending_keymap: Vec::new(),
|
||||||
old_layout: None,
|
old_layout: None,
|
||||||
repeat_action: None,
|
repeat_action: None,
|
||||||
@@ -365,8 +375,6 @@ impl ShedVi {
|
|||||||
let span_start = self.completer.token_span().0;
|
let span_start = self.completer.token_span().0;
|
||||||
let new_cursor = span_start + candidate.len();
|
let new_cursor = span_start + candidate.len();
|
||||||
let line = self.completer.get_completed_line(&candidate);
|
let line = self.completer.get_completed_line(&candidate);
|
||||||
log::debug!("Completer accepted candidate: {candidate}");
|
|
||||||
log::debug!("New line after completion: {line}");
|
|
||||||
self.editor.set_buffer(line);
|
self.editor.set_buffer(line);
|
||||||
self.editor.cursor.set(new_cursor);
|
self.editor.cursor.set(new_cursor);
|
||||||
// Don't reset yet — clear() needs old_layout to erase the selector.
|
// Don't reset yet — clear() needs old_layout to erase the selector.
|
||||||
@@ -401,13 +409,10 @@ impl ShedVi {
|
|||||||
} else {
|
} else {
|
||||||
let keymap_flags = self.curr_keymap_flags();
|
let keymap_flags = self.curr_keymap_flags();
|
||||||
self.pending_keymap.push(key.clone());
|
self.pending_keymap.push(key.clone());
|
||||||
log::debug!("[keymap] pending={:?} flags={:?}", self.pending_keymap, keymap_flags);
|
|
||||||
|
|
||||||
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
|
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
|
||||||
log::debug!("[keymap] {} matches found", matches.len());
|
|
||||||
if matches.is_empty() {
|
if matches.is_empty() {
|
||||||
// No matches. Drain the buffered keys and execute them.
|
// No matches. Drain the buffered keys and execute them.
|
||||||
log::debug!("[keymap] no matches, flushing {} buffered keys", self.pending_keymap.len());
|
|
||||||
for key in std::mem::take(&mut self.pending_keymap) {
|
for key in std::mem::take(&mut self.pending_keymap) {
|
||||||
if let Some(event) = self.handle_key(key)? {
|
if let Some(event) = self.handle_key(key)? {
|
||||||
return Ok(event);
|
return Ok(event);
|
||||||
@@ -418,11 +423,8 @@ impl ShedVi {
|
|||||||
} else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact {
|
} else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact {
|
||||||
// We have a single exact match. Execute it.
|
// We have a single exact match. Execute it.
|
||||||
let keymap = matches[0].clone();
|
let keymap = matches[0].clone();
|
||||||
log::debug!("[keymap] self.pending_keymap={:?}", self.pending_keymap);
|
|
||||||
log::debug!("[keymap] exact match: {:?} -> {:?}", keymap.keys, keymap.action);
|
|
||||||
self.pending_keymap.clear();
|
self.pending_keymap.clear();
|
||||||
let action = keymap.action_expanded();
|
let action = keymap.action_expanded();
|
||||||
log::debug!("[keymap] expanded action: {:?}", action);
|
|
||||||
for key in action {
|
for key in action {
|
||||||
if let Some(event) = self.handle_key(key)? {
|
if let Some(event) = self.handle_key(key)? {
|
||||||
return Ok(event);
|
return Ok(event);
|
||||||
@@ -432,7 +434,6 @@ impl ShedVi {
|
|||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// There is ambiguity. Allow the timeout in the main loop to handle this.
|
// There is ambiguity. Allow the timeout in the main loop to handle this.
|
||||||
log::debug!("[keymap] ambiguous: {} matches, waiting for more input", matches.len());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,6 +513,13 @@ impl ShedVi {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
|
||||||
|
&& !self.next_is_escaped {
|
||||||
|
self.next_is_escaped = true;
|
||||||
|
} else {
|
||||||
|
self.next_is_escaped = false;
|
||||||
|
}
|
||||||
|
|
||||||
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.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
|
||||||
@@ -519,10 +527,8 @@ impl ShedVi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let Some(mut cmd) = cmd else {
|
let Some(mut cmd) = cmd else {
|
||||||
log::debug!("[readline] mode.handle_key returned None");
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
log::debug!("[readline] got cmd: verb={:?} motion={:?} flags={:?}", cmd.verb, cmd.motion, cmd.flags);
|
|
||||||
cmd.alter_line_motion_if_no_verb();
|
cmd.alter_line_motion_if_no_verb();
|
||||||
|
|
||||||
if self.should_grab_history(&cmd) {
|
if self.should_grab_history(&cmd) {
|
||||||
@@ -532,8 +538,9 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.is_submit_action()
|
if cmd.is_submit_action()
|
||||||
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
&& !self.next_is_escaped
|
||||||
{
|
&& !self.editor.buffer.ends_with('\\')
|
||||||
|
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) {
|
||||||
self.editor.set_hint(None);
|
self.editor.set_hint(None);
|
||||||
self.editor.cursor.set(self.editor.cursor_max());
|
self.editor.cursor.set(self.editor.cursor_max());
|
||||||
self.print_line(true)?;
|
self.print_line(true)?;
|
||||||
@@ -564,6 +571,11 @@ impl ShedVi {
|
|||||||
|
|
||||||
let before = self.editor.buffer.clone();
|
let before = self.editor.buffer.clone();
|
||||||
self.exec_cmd(cmd)?;
|
self.exec_cmd(cmd)?;
|
||||||
|
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
|
||||||
|
for key in keys {
|
||||||
|
self.handle_key(key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
let after = self.editor.as_str();
|
let after = self.editor.as_str();
|
||||||
|
|
||||||
if before != after {
|
if before != after {
|
||||||
@@ -637,6 +649,7 @@ impl ShedVi {
|
|||||||
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
|
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
|
||||||
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
|
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
|
||||||
}
|
}
|
||||||
|
ModeReport::Ex => false,
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -783,7 +796,11 @@ impl ShedVi {
|
|||||||
if cmd.is_mode_transition() {
|
if cmd.is_mode_transition() {
|
||||||
let count = cmd.verb_count();
|
let count = cmd.verb_count();
|
||||||
let mut mode: Box<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
let mut mode: Box<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
||||||
|
if let Some(saved) = self.saved_mode.take() {
|
||||||
|
saved
|
||||||
|
} else {
|
||||||
Box::new(ViNormal::new())
|
Box::new(ViNormal::new())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
match cmd.verb().unwrap().1 {
|
match cmd.verb().unwrap().1 {
|
||||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
||||||
@@ -791,7 +808,9 @@ impl ShedVi {
|
|||||||
Box::new(ViInsert::new().with_count(count as u16))
|
Box::new(ViInsert::new().with_count(count as u16))
|
||||||
}
|
}
|
||||||
|
|
||||||
Verb::ExMode => Box::new(ViEx::new()),
|
Verb::ExMode => {
|
||||||
|
Box::new(ViEx::new())
|
||||||
|
}
|
||||||
|
|
||||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||||
|
|
||||||
@@ -825,6 +844,11 @@ impl ShedVi {
|
|||||||
|
|
||||||
std::mem::swap(&mut mode, &mut self.mode);
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
|
|
||||||
|
if self.mode.report_mode() == ModeReport::Ex {
|
||||||
|
self.saved_mode = Some(mode);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if mode.is_repeatable() {
|
if mode.is_repeatable() {
|
||||||
self.repeat_action = mode.as_replay();
|
self.repeat_action = mode.as_replay();
|
||||||
}
|
}
|
||||||
@@ -917,12 +941,22 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.mode.report_mode() == ModeReport::Visual
|
||||||
|
&& self.editor.select_range().is_none() {
|
||||||
|
self.editor.stop_selecting();
|
||||||
|
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||||
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.is_repeatable() {
|
if cmd.is_repeatable() {
|
||||||
if self.mode.report_mode() == ModeReport::Visual {
|
if self.mode.report_mode() == ModeReport::Visual {
|
||||||
// The motion is assigned in the line buffer execution, so we also have to
|
// The motion is assigned in the line buffer execution, so we also have to
|
||||||
// assign it here in order to be able to repeat it
|
// assign it here in order to be able to repeat it
|
||||||
let range = self.editor.select_range().unwrap();
|
if let Some(range) = self.editor.select_range() {
|
||||||
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
|
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
|
||||||
|
} else {
|
||||||
|
log::warn!("You're in visual mode with no select range??");
|
||||||
|
};
|
||||||
}
|
}
|
||||||
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
||||||
}
|
}
|
||||||
@@ -939,8 +973,20 @@ impl ShedVi {
|
|||||||
std::mem::swap(&mut mode, &mut self.mode);
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() {
|
||||||
|
self.editor.stop_selecting();
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
||||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
let mut mode: Box<dyn ViMode> = if self.mode.report_mode() == ModeReport::Ex {
|
||||||
|
if let Some(saved) = self.saved_mode.take() {
|
||||||
|
saved
|
||||||
|
} else {
|
||||||
|
Box::new(ViNormal::new())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box::new(ViNormal::new())
|
||||||
|
};
|
||||||
std::mem::swap(&mut mode, &mut self.mode);
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||||
}
|
}
|
||||||
@@ -976,6 +1022,8 @@ pub fn annotate_input(input: &str) -> String {
|
|||||||
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
|
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
log::debug!("Annotating input with tokens: {tokens:#?}");
|
||||||
|
|
||||||
for tk in tokens.into_iter().rev() {
|
for tk in tokens.into_iter().rev() {
|
||||||
let insertions = annotate_token(tk);
|
let insertions = annotate_token(tk);
|
||||||
for (pos, marker) in insertions {
|
for (pos, marker) in insertions {
|
||||||
@@ -1019,7 +1067,6 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
|||||||
Some('>') => ">(",
|
Some('>') => ">(",
|
||||||
Some('<') => "<(",
|
Some('<') => "<(",
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'");
|
|
||||||
"<("
|
"<("
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ impl ViVisual {
|
|||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
':' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::ExMode)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
})
|
||||||
|
}
|
||||||
'x' => {
|
'x' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||||
|
|||||||
33
src/state.rs
33
src/state.rs
@@ -11,22 +11,15 @@ use std::{
|
|||||||
use nix::unistd::{User, gethostname, getppid};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget},
|
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{
|
||||||
exec_input,
|
|
||||||
jobs::JobTab,
|
|
||||||
libsh::{
|
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::VecDequeExt,
|
utils::VecDequeExt,
|
||||||
},
|
}, parse::{
|
||||||
parse::{
|
|
||||||
ConjunctNode, NdRule, Node, ParsedSrc,
|
ConjunctNode, NdRule, Node, ParsedSrc,
|
||||||
lex::{LexFlags, LexStream, Span, Tk},
|
lex::{LexFlags, LexStream, Span, Tk},
|
||||||
},
|
}, prelude::*, readline::{
|
||||||
prelude::*,
|
|
||||||
readline::{
|
|
||||||
complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers
|
complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers
|
||||||
},
|
}, shopt::ShOpts
|
||||||
shopt::ShOpts,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Shed {
|
pub struct Shed {
|
||||||
@@ -417,6 +410,11 @@ impl ScopeStack {
|
|||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
pub fn take_var(&mut self, var_name: &str) -> String {
|
||||||
|
let var = self.get_var(var_name);
|
||||||
|
self.unset_var(var_name).ok();
|
||||||
|
var
|
||||||
|
}
|
||||||
pub fn get_var(&self, var_name: &str) -> String {
|
pub fn get_var(&self, var_name: &str) -> String {
|
||||||
if let Ok(param) = var_name.parse::<ShellParam>() {
|
if let Ok(param) = var_name.parse::<ShellParam>() {
|
||||||
return self.get_param(param);
|
return self.get_param(param);
|
||||||
@@ -1125,6 +1123,8 @@ pub struct MetaTab {
|
|||||||
// programmable completion specs
|
// programmable completion specs
|
||||||
comp_specs: HashMap<String, Box<dyn CompSpec>>,
|
comp_specs: HashMap<String, Box<dyn CompSpec>>,
|
||||||
|
|
||||||
|
// pending keys from widget function
|
||||||
|
pending_widget_keys: Vec<KeyEvent>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetaTab {
|
impl MetaTab {
|
||||||
@@ -1133,6 +1133,17 @@ impl MetaTab {
|
|||||||
comp_specs: Self::get_builtin_comp_specs(),
|
comp_specs: Self::get_builtin_comp_specs(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
pub fn set_pending_widget_keys(&mut self, keys: &str) {
|
||||||
|
let exp = expand_keymap(keys);
|
||||||
|
self.pending_widget_keys = exp;
|
||||||
|
}
|
||||||
|
pub fn take_pending_widget_keys(&mut self) -> Option<Vec<KeyEvent>> {
|
||||||
|
if self.pending_widget_keys.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(std::mem::take(&mut self.pending_widget_keys))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn getopts_char_offset(&self) -> usize {
|
pub fn getopts_char_offset(&self) -> usize {
|
||||||
self.getopts_offset
|
self.getopts_offset
|
||||||
|
|||||||
Reference in New Issue
Block a user