diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index d6cbe54..aa585ee 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -18,7 +18,9 @@ pub fn alias(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if argv.is_empty() { // Display the environment variables @@ -37,11 +39,19 @@ pub fn alias(node: Node) -> ShResult<()> { } else { for (arg, span) in argv { if arg == "command" || arg == "builtin" { - return Err(ShErr::at(ShErrKind::ExecFail, span, format!("alias: Cannot assign alias to reserved name '{arg}'"))); + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + format!("alias: Cannot assign alias to reserved name '{arg}'"), + )); } let Some((name, body)) = arg.split_once('=') else { - return Err(ShErr::at(ShErrKind::SyntaxErr, span, "alias: Expected an assignment in alias args")); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + span, + "alias: Expected an assignment in alias args", + )); }; write_logic(|l| l.insert_alias(name, body, span.clone())); } @@ -60,7 +70,9 @@ pub fn unalias(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if argv.is_empty() { // Display the environment variables @@ -79,7 +91,11 @@ pub fn unalias(node: Node) -> ShResult<()> { } else { for (arg, span) in argv { if read_logic(|l| l.get_alias(&arg)).is_none() { - return Err(ShErr::at(ShErrKind::SyntaxErr, span, format!("unalias: alias '{}' not found",arg.fg(next_color())))); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + span, + format!("unalias: alias '{}' not found", arg.fg(next_color())), + )); }; write_logic(|l| l.remove_alias(&arg)) } diff --git a/src/builtin/arrops.rs b/src/builtin/arrops.rs index 24f2109..24e0650 100644 --- a/src/builtin/arrops.rs +++ b/src/builtin/arrops.rs @@ -1,44 +1,52 @@ use std::collections::VecDeque; use crate::{ - getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, procio::borrow_fd, state::{self, VarFlags, VarKind, write_vars} + getopt::{Opt, OptSpec, get_opts_from_tokens}, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node, execute::prepare_argv}, + prelude::*, + procio::borrow_fd, + state::{self, VarFlags, VarKind, write_vars}, }; fn arr_op_optspec() -> Vec { - vec![ - OptSpec { - opt: Opt::Short('c'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('r'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('v'), - takes_arg: true - } - ] + vec![ + OptSpec { + opt: Opt::Short('c'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('r'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('v'), + takes_arg: true, + }, + ] } pub struct ArrOpOpts { - count: usize, - reverse: bool, - var: Option, + count: usize, + reverse: bool, + var: Option, } impl Default for ArrOpOpts { - fn default() -> Self { - Self { - count: 1, - reverse: false, - var: None, - } - } + fn default() -> Self { + Self { + count: 1, + reverse: false, + var: None, + } + } } #[derive(Clone, Copy)] -enum End { Front, Back } +enum End { + Front, + Back, +} fn arr_pop_inner(node: Node, end: End) -> ShResult<()> { let NdRule::Command { @@ -49,40 +57,42 @@ fn arr_pop_inner(node: Node, end: End) -> ShResult<()> { unreachable!() }; - let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; - let arr_op_opts = get_arr_op_opts(opts)?; + let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; + let arr_op_opts = get_arr_op_opts(opts)?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let stdout = borrow_fd(STDOUT_FILENO); - let mut status = 0; + let mut status = 0; - for (arg,_) in argv { - for _ in 0..arr_op_opts.count { - let pop = |arr: &mut std::collections::VecDeque| match end { - End::Front => arr.pop_front(), - End::Back => arr.pop_back(), - }; - let Some(popped) = write_vars(|v| v.get_arr_mut(&arg).ok().and_then(pop)) else { - status = 1; - break; - }; - status = 0; + for (arg, _) in argv { + for _ in 0..arr_op_opts.count { + let pop = |arr: &mut std::collections::VecDeque| match end { + End::Front => arr.pop_front(), + End::Back => arr.pop_back(), + }; + let Some(popped) = write_vars(|v| v.get_arr_mut(&arg).ok().and_then(pop)) else { + status = 1; + break; + }; + status = 0; - if let Some(ref var) = arr_op_opts.var { - write_vars(|v| v.set_var(var, VarKind::Str(popped), VarFlags::NONE))?; - } else { - write(stdout, popped.as_bytes())?; - write(stdout, b"\n")?; - } - } - } + if let Some(ref var) = arr_op_opts.var { + write_vars(|v| v.set_var(var, VarKind::Str(popped), VarFlags::NONE))?; + } else { + write(stdout, popped.as_bytes())?; + write(stdout, b"\n")?; + } + } + } state::set_status(status); Ok(()) } fn arr_push_inner(node: Node, end: End) -> ShResult<()> { - let blame = node.get_span().clone(); + let blame = node.get_span().clone(); let NdRule::Command { assignments: _, argv, @@ -91,49 +101,60 @@ fn arr_push_inner(node: Node, end: End) -> ShResult<()> { unreachable!() }; - let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; - let _arr_op_opts = get_arr_op_opts(opts)?; + let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; + let _arr_op_opts = get_arr_op_opts(opts)?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } - let mut argv = argv.into_iter(); - let Some((name, _)) = argv.next() else { - return Err(ShErr::at(ShErrKind::ExecFail, blame, "push: missing array name".to_string())); - }; + let mut argv = argv.into_iter(); + let Some((name, _)) = argv.next() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + "push: missing array name".to_string(), + )); + }; - for (val, span) in argv { - let push_val = val.clone(); - write_vars(|v| { - if let Ok(arr) = v.get_arr_mut(&name) { - match end { - End::Front => arr.push_front(push_val), - End::Back => arr.push_back(push_val), - } - Ok(()) - } else { - v.set_var(&name, VarKind::Arr(VecDeque::from([push_val])), VarFlags::NONE) - } - }).blame(span)?; - } + for (val, span) in argv { + let push_val = val.clone(); + write_vars(|v| { + if let Ok(arr) = v.get_arr_mut(&name) { + match end { + End::Front => arr.push_front(push_val), + End::Back => arr.push_back(push_val), + } + Ok(()) + } else { + v.set_var( + &name, + VarKind::Arr(VecDeque::from([push_val])), + VarFlags::NONE, + ) + } + }) + .blame(span)?; + } state::set_status(0); Ok(()) } pub fn arr_pop(node: Node) -> ShResult<()> { - arr_pop_inner(node, End::Back) + arr_pop_inner(node, End::Back) } pub fn arr_fpop(node: Node) -> ShResult<()> { - arr_pop_inner(node, End::Front) + arr_pop_inner(node, End::Front) } pub fn arr_push(node: Node) -> ShResult<()> { - arr_push_inner(node, End::Back) + arr_push_inner(node, End::Back) } pub fn arr_fpush(node: Node) -> ShResult<()> { - arr_push_inner(node, End::Front) + arr_push_inner(node, End::Front) } pub fn arr_rotate(node: Node) -> ShResult<()> { @@ -145,52 +166,63 @@ pub fn arr_rotate(node: Node) -> ShResult<()> { unreachable!() }; - let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; - let arr_op_opts = get_arr_op_opts(opts)?; + let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; + let arr_op_opts = get_arr_op_opts(opts)?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } - for (arg, _) in argv { - write_vars(|v| -> ShResult<()> { - let arr = v.get_arr_mut(&arg)?; - if arr_op_opts.reverse { - arr.rotate_right(arr_op_opts.count.min(arr.len())); - } else { - arr.rotate_left(arr_op_opts.count.min(arr.len())); - } - Ok(()) - })?; - } + for (arg, _) in argv { + write_vars(|v| -> ShResult<()> { + let arr = v.get_arr_mut(&arg)?; + if arr_op_opts.reverse { + arr.rotate_right(arr_op_opts.count.min(arr.len())); + } else { + arr.rotate_left(arr_op_opts.count.min(arr.len())); + } + Ok(()) + })?; + } state::set_status(0); Ok(()) } pub fn get_arr_op_opts(opts: Vec) -> ShResult { - let mut arr_op_opts = ArrOpOpts::default(); - for opt in opts { - match opt { - Opt::ShortWithArg('c', count) => { - arr_op_opts.count = count.parse::().map_err(|_| { - ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count)) - })?; - } - Opt::Short('c') => { - return Err(ShErr::simple(ShErrKind::ParseErr, "missing count for -c".to_string())); - } - Opt::Short('r') => { - arr_op_opts.reverse = true; - } - Opt::ShortWithArg('v', var) => { - arr_op_opts.var = Some(var); - } - Opt::Short('v') => { - return Err(ShErr::simple(ShErrKind::ParseErr, "missing variable name for -v".to_string())); - } - _ => { - return Err(ShErr::simple(ShErrKind::ParseErr, format!("invalid option: {}", opt))); - } - } - } - Ok(arr_op_opts) + let mut arr_op_opts = ArrOpOpts::default(); + for opt in opts { + match opt { + Opt::ShortWithArg('c', count) => { + arr_op_opts.count = count + .parse::() + .map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count)))?; + } + Opt::Short('c') => { + return Err(ShErr::simple( + ShErrKind::ParseErr, + "missing count for -c".to_string(), + )); + } + Opt::Short('r') => { + arr_op_opts.reverse = true; + } + Opt::ShortWithArg('v', var) => { + arr_op_opts.var = Some(var); + } + Opt::Short('v') => { + return Err(ShErr::simple( + ShErrKind::ParseErr, + "missing variable name for -v".to_string(), + )); + } + _ => { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("invalid option: {}", opt), + )); + } + } + } + Ok(arr_op_opts) } diff --git a/src/builtin/autocmd.rs b/src/builtin/autocmd.rs index 62b5c10..2b7e43d 100644 --- a/src/builtin/autocmd.rs +++ b/src/builtin/autocmd.rs @@ -1,48 +1,56 @@ use regex::Regex; use crate::{ - getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, state::{self, AutoCmd, AutoCmdKind, write_logic} + getopt::{Opt, OptSpec, get_opts_from_tokens}, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node, execute::prepare_argv}, + state::{self, AutoCmd, AutoCmdKind, write_logic}, }; pub struct AutoCmdOpts { - pattern: Option, - clear: bool + pattern: Option, + clear: bool, } -fn autocmd_optspec() -> [OptSpec;2] { - [ - OptSpec { - opt: Opt::Short('p'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('c'), - takes_arg: false - } - ] +fn autocmd_optspec() -> [OptSpec; 2] { + [ + OptSpec { + opt: Opt::Short('p'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('c'), + takes_arg: false, + }, + ] } pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult { - let mut autocmd_opts = AutoCmdOpts { - pattern: None, - clear: false - }; + let mut autocmd_opts = AutoCmdOpts { + pattern: None, + clear: false, + }; - let mut opts = opts.iter(); - while let Some(arg) = opts.next() { - match arg { - Opt::ShortWithArg('p', arg) => { - autocmd_opts.pattern = Some(Regex::new(arg).map_err(|e| ShErr::simple(ShErrKind::ExecFail, format!("invalid regex for -p: {}", e)))?); - } - Opt::Short('c') => { - autocmd_opts.clear = true; - } - _ => { - return Err(ShErr::simple(ShErrKind::ExecFail, format!("unexpected option: {}", arg))); - } - } - } + let mut opts = opts.iter(); + while let Some(arg) = opts.next() { + match arg { + Opt::ShortWithArg('p', arg) => { + autocmd_opts.pattern = Some(Regex::new(arg).map_err(|e| { + ShErr::simple(ShErrKind::ExecFail, format!("invalid regex for -p: {}", e)) + })?); + } + Opt::Short('c') => { + autocmd_opts.clear = true; + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("unexpected option: {}", arg), + )); + } + } + } - Ok(autocmd_opts) + Ok(autocmd_opts) } pub fn autocmd(node: Node) -> ShResult<()> { @@ -55,36 +63,50 @@ pub fn autocmd(node: Node) -> ShResult<()> { unreachable!() }; - let (argv,opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?; - let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?; + let (argv, opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?; + let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } - let mut args = argv.iter(); + if !argv.is_empty() { + argv.remove(0); + } + let mut args = argv.iter(); - let Some(autocmd_kind) = args.next() else { - return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd kind".to_string())); - }; + let Some(autocmd_kind) = args.next() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "expected an autocmd kind".to_string(), + )); + }; - let Ok(autocmd_kind) = autocmd_kind.0.parse::() else { - return Err(ShErr::at(ShErrKind::ExecFail, autocmd_kind.1.clone(), format!("invalid autocmd kind: {}", autocmd_kind.0))); - }; + let Ok(autocmd_kind) = autocmd_kind.0.parse::() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + autocmd_kind.1.clone(), + format!("invalid autocmd kind: {}", autocmd_kind.0), + )); + }; - if autocmd_opts.clear { - write_logic(|l| l.clear_autocmds(autocmd_kind)); - state::set_status(0); - return Ok(()); - } + if autocmd_opts.clear { + write_logic(|l| l.clear_autocmds(autocmd_kind)); + state::set_status(0); + return Ok(()); + } - let Some(autocmd_cmd) = args.next() else { - return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd command".to_string())); - }; + let Some(autocmd_cmd) = args.next() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "expected an autocmd command".to_string(), + )); + }; - let autocmd = AutoCmd { - pattern: autocmd_opts.pattern, - command: autocmd_cmd.0.clone(), - }; + let autocmd = AutoCmd { + pattern: autocmd_opts.pattern, + command: autocmd_cmd.0.clone(), + }; - write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd)); + write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd)); state::set_status(0); Ok(()) diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 23c49e7..d08aa29 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -17,40 +17,58 @@ pub fn cd(node: Node) -> ShResult<()> { else { unreachable!() }; - let cd_span = argv.first().unwrap().span.clone(); + let cd_span = argv.first().unwrap().span.clone(); let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } - let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() { - (PathBuf::from(arg),Some(span)) + let (new_dir, arg_span) = if let Some((arg, span)) = argv.into_iter().next() { + (PathBuf::from(arg), Some(span)) } else { - (PathBuf::from(env::var("HOME").unwrap()),None) + (PathBuf::from(env::var("HOME").unwrap()), None) }; if !new_dir.exists() { - let mut err = ShErr::new( - ShErrKind::ExecFail, - span.clone(), - ).labeled(cd_span.clone(), "Failed to change directory"); - if let Some(span) = arg_span { - err = err.labeled(span, format!("No such file or directory '{}'", new_dir.display().fg(next_color()))); - } - return Err(err); + let mut err = ShErr::new(ShErrKind::ExecFail, span.clone()) + .labeled(cd_span.clone(), "Failed to change directory"); + if let Some(span) = arg_span { + err = err.labeled( + span, + format!( + "No such file or directory '{}'", + new_dir.display().fg(next_color()) + ), + ); + } + return Err(err); } if !new_dir.is_dir() { - return Err(ShErr::new(ShErrKind::ExecFail, span.clone()) - .labeled(cd_span.clone(), format!("cd: Not a directory '{}'", new_dir.display().fg(next_color())))); + return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled( + cd_span.clone(), + format!( + "cd: Not a directory '{}'", + new_dir.display().fg(next_color()) + ), + )); } if let Err(e) = state::change_dir(new_dir) { - return Err(ShErr::new(ShErrKind::ExecFail, span.clone()) - .labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)))); + return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled( + cd_span.clone(), + format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)), + )); } let new_dir = env::current_dir().map_err(|e| { - ShErr::new(ShErrKind::ExecFail, span.clone()) - .labeled(cd_span.clone(), format!("cd: Failed to get current directory: '{}'", e.fg(Color::Red))) + ShErr::new(ShErrKind::ExecFail, span.clone()).labeled( + cd_span.clone(), + format!( + "cd: Failed to get current directory: '{}'", + e.fg(Color::Red) + ), + ) })?; unsafe { env::set_var("PWD", new_dir) }; diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index 1ff00f4..a34bef7 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -167,7 +167,9 @@ pub fn complete_builtin(node: Node) -> ShResult<()> { let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?; let comp_opts = get_comp_opts(opts)?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if comp_opts.flags.contains(CompFlags::PRINT) { if argv.is_empty() { @@ -204,7 +206,11 @@ pub fn complete_builtin(node: Node) -> ShResult<()> { if argv.is_empty() { state::set_status(1); - return Err(ShErr::at(ShErrKind::ExecFail, blame, "complete: no command specified")); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + "complete: no command specified", + )); } let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src); @@ -279,12 +285,16 @@ pub fn get_comp_opts(opts: Vec) -> ShResult { "space" => comp_opts.opt_flags |= CompOptFlags::SPACE, _ => { let span: crate::parse::lex::Span = Default::default(); - return Err(ShErr::at(ShErrKind::InvalidOpt, span, format!("complete: invalid option: {}", opt_flag))); + return Err(ShErr::at( + ShErrKind::InvalidOpt, + span, + format!("complete: invalid option: {}", opt_flag), + )); } }, - Opt::Short('a') => comp_opts.flags |= CompFlags::ALIAS, - Opt::Short('S') => comp_opts.flags |= CompFlags::SIGNALS, + Opt::Short('a') => comp_opts.flags |= CompFlags::ALIAS, + Opt::Short('S') => comp_opts.flags |= CompFlags::SIGNALS, Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE, Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS, Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT, diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs index 187bf2f..cb90af2 100644 --- a/src/builtin/dirstack.rs +++ b/src/builtin/dirstack.rs @@ -47,18 +47,26 @@ fn print_dirs() -> ShResult<()> { fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> { if !target.is_dir() { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("not a directory: '{}'", target.display().fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("not a directory: '{}'", target.display().fg(next_color())), + )); } if let Err(e) = state::change_dir(target) { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to change directory: '{}'", e.fg(Color::Red))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("Failed to change directory: '{}'", e.fg(Color::Red)), + )); } let new_dir = env::current_dir().map_err(|e| { - ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to get current directory: '{}'", e.fg(Color::Red))) + ShErr::at( + ShErrKind::ExecFail, + blame, + format!("Failed to get current directory: '{}'", e.fg(Color::Red)), + ) })?; unsafe { env::set_var("PWD", new_dir) }; Ok(()) @@ -74,24 +82,32 @@ fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult { }; if digits.is_empty() { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!( + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!( "{cmd}: missing index after '{}'", if from_top { "+" } else { "-" } - )) - ); + ), + )); } for ch in digits.chars() { if !ch.is_ascii_digit() { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("{cmd}: invalid argument: '{}'",arg.fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("{cmd}: invalid argument: '{}'", arg.fg(next_color())), + )); } } let n = digits.parse::().map_err(|e| { - ShErr::at(ShErrKind::ExecFail, blame, format!("{cmd}: invalid index: '{}'",e.fg(next_color()))) + ShErr::at( + ShErrKind::ExecFail, + blame, + format!("{cmd}: invalid index: '{}'", e.fg(next_color())), + ) })?; if from_top { @@ -112,7 +128,9 @@ pub fn pushd(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut dir = None; let mut rotate_idx = None; @@ -126,20 +144,29 @@ pub fn pushd(node: Node) -> ShResult<()> { } else if arg == "-n" { no_cd = true; } else if arg.starts_with('-') { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("pushd: invalid option: '{}'", arg.fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("pushd: invalid option: '{}'", arg.fg(next_color())), + )); } else { if dir.is_some() { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, "pushd: too many arguments") - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + "pushd: too many arguments", + )); } let target = PathBuf::from(&arg); if !target.is_dir() { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("pushd: not a directory: '{}'", target.display().fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!( + "pushd: not a directory: '{}'", + target.display().fg(next_color()) + ), + )); } dir = Some(target); } @@ -193,7 +220,9 @@ pub fn popd(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut remove_idx = None; let mut no_cd = false; @@ -206,9 +235,11 @@ pub fn popd(node: Node) -> ShResult<()> { } else if arg == "-n" { no_cd = true; } else if arg.starts_with('-') { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("popd: invalid option: '{}'", arg.fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("popd: invalid option: '{}'", arg.fg(next_color())), + )); } } @@ -221,9 +252,11 @@ pub fn popd(node: Node) -> ShResult<()> { if let Some(dir) = dir { change_directory(&dir, blame.clone())?; } else { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, "popd: directory stack empty") - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + "popd: directory stack empty", + )); } } } @@ -233,9 +266,11 @@ pub fn popd(node: Node) -> ShResult<()> { let dirs = m.dirs_mut(); let idx = n - 1; if idx >= dirs.len() { - return Err( - ShErr::at(ShErrKind::ExecFail, blame.clone(), format!("popd: directory index out of range: +{n}")) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame.clone(), + format!("popd: directory index out of range: +{n}"), + )); } dirs.remove(idx); Ok(()) @@ -245,7 +280,11 @@ pub fn popd(node: Node) -> ShResult<()> { write_meta(|m| -> ShResult<()> { let dirs = m.dirs_mut(); let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| { - ShErr::at(ShErrKind::ExecFail, blame.clone(), format!("popd: directory index out of range: -{n}")) + ShErr::at( + ShErrKind::ExecFail, + blame.clone(), + format!("popd: directory index out of range: -{n}"), + ) })?; dirs.remove(actual); Ok(()) @@ -265,9 +304,11 @@ pub fn popd(node: Node) -> ShResult<()> { change_directory(&dir, blame.clone())?; print_dirs()?; } else { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, "popd: directory stack empty") - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + "popd: directory stack empty", + )); } } @@ -285,7 +326,9 @@ pub fn dirs(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut abbreviate_home = true; let mut one_per_line = false; @@ -306,14 +349,18 @@ pub fn dirs(node: Node) -> ShResult<()> { target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?); } _ if arg.starts_with('-') => { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("dirs: invalid option: '{}'", arg.fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("dirs: invalid option: '{}'", arg.fg(next_color())), + )); } _ => { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!("dirs: unexpected argument: '{}'", arg.fg(next_color()))) - ); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!("dirs: unexpected argument: '{}'", arg.fg(next_color())), + )); } } } @@ -358,15 +405,17 @@ pub fn dirs(node: Node) -> ShResult<()> { if let Some(dir) = target { dirs = vec![dir.clone()]; } else { - return Err( - ShErr::at(ShErrKind::ExecFail, blame, format!( + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + format!( "dirs: directory index out of range: {}", match idx { StackIdx::FromTop(n) => format!("+{n}"), StackIdx::FromBottom(n) => format!("-{n}"), } - )) - ); + ), + )); } } diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 7a8dbb4..71ae274 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -50,7 +50,9 @@ pub fn echo(node: Node) -> ShResult<()> { let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?; let flags = get_echo_flags(opts).blame(blame)?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let output_channel = if flags.contains(EchoFlags::USE_STDERR) { borrow_fd(STDERR_FILENO) diff --git a/src/builtin/eval.rs b/src/builtin/eval.rs index d29fda4..cd3c74a 100644 --- a/src/builtin/eval.rs +++ b/src/builtin/eval.rs @@ -1,6 +1,9 @@ use crate::{ libsh::error::ShResult, - parse::{NdRule, Node, execute::{exec_input, prepare_argv}}, + parse::{ + NdRule, Node, + execute::{exec_input, prepare_argv}, + }, state, }; @@ -14,7 +17,9 @@ pub fn eval(node: Node) -> ShResult<()> { }; let mut expanded_argv = prepare_argv(argv)?; - if !expanded_argv.is_empty() { expanded_argv.remove(0); } + if !expanded_argv.is_empty() { + expanded_argv.remove(0); + } if expanded_argv.is_empty() { state::set_status(0); diff --git a/src/builtin/exec.rs b/src/builtin/exec.rs index 5e3ce1e..140d3dd 100644 --- a/src/builtin/exec.rs +++ b/src/builtin/exec.rs @@ -2,7 +2,10 @@ use nix::{errno::Errno, unistd::execvpe}; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node, execute::{ExecArgs, prepare_argv}}, + parse::{ + NdRule, Node, + execute::{ExecArgs, prepare_argv}, + }, state, }; @@ -16,7 +19,9 @@ pub fn exec_builtin(node: Node) -> ShResult<()> { }; let mut expanded_argv = prepare_argv(argv)?; - if !expanded_argv.is_empty() { expanded_argv.remove(0); } + if !expanded_argv.is_empty() { + expanded_argv.remove(0); + } if expanded_argv.is_empty() { state::set_status(0); @@ -34,9 +39,9 @@ pub fn exec_builtin(node: Node) -> ShResult<()> { let cmd_str = cmd.to_str().unwrap().to_string(); match e { Errno::ENOENT => Err( - ShErr::new(ShErrKind::NotFound, span.clone()) - .labeled(span, format!("exec: command not found: {}", cmd_str)) - ), + ShErr::new(ShErrKind::NotFound, span.clone()) + .labeled(span, format!("exec: command not found: {}", cmd_str)), + ), _ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))), } } diff --git a/src/builtin/flowctl.rs b/src/builtin/flowctl.rs index d5034f9..ca5fdd4 100644 --- a/src/builtin/flowctl.rs +++ b/src/builtin/flowctl.rs @@ -21,7 +21,11 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { let (arg, span) = argv.into_iter().next().unwrap(); let Ok(status) = arg.parse::() else { - return Err(ShErr::at(ShErrKind::SyntaxErr, span, format!("{cmd}: Expected a number"))); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + span, + format!("{cmd}: Expected a number"), + )); }; code = status; diff --git a/src/builtin/getopts.rs b/src/builtin/getopts.rs index 0eba2d0..7a9c12b 100644 --- a/src/builtin/getopts.rs +++ b/src/builtin/getopts.rs @@ -3,229 +3,251 @@ use std::str::FromStr; use ariadne::Fmt; use crate::{ - getopt::{Opt, OptSpec}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node, execute::prepare_argv, lex::Span}, state::{self, VarFlags, VarKind, read_meta, read_vars, write_meta, write_vars} + getopt::{Opt, OptSpec}, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, + parse::{NdRule, Node, execute::prepare_argv, lex::Span}, + state::{self, VarFlags, VarKind, read_meta, read_vars, write_meta, write_vars}, }; enum OptMatch { - NoMatch, - IsMatch, - WantsArg + NoMatch, + IsMatch, + WantsArg, } struct GetOptsSpec { - silent_err: bool, - opt_specs: Vec + silent_err: bool, + opt_specs: Vec, } impl GetOptsSpec { - pub fn matches(&self, ch: char) -> OptMatch { - for spec in &self.opt_specs { - let OptSpec { opt, takes_arg } = spec; - match opt { - Opt::Short(opt_ch) if ch == *opt_ch => { - if *takes_arg { - return OptMatch::WantsArg - } else { - return OptMatch::IsMatch - } - } - _ => { continue } - } - } - OptMatch::NoMatch - } + pub fn matches(&self, ch: char) -> OptMatch { + for spec in &self.opt_specs { + let OptSpec { opt, takes_arg } = spec; + match opt { + Opt::Short(opt_ch) if ch == *opt_ch => { + if *takes_arg { + return OptMatch::WantsArg; + } else { + return OptMatch::IsMatch; + } + } + _ => continue, + } + } + OptMatch::NoMatch + } } impl FromStr for GetOptsSpec { - type Err = ShErr; - fn from_str(s: &str) -> Result { - let mut s = s; - let mut opt_specs = vec![]; - let mut silent_err = false; - if s.starts_with(':') { - silent_err = true; - s = &s[1..]; - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + let mut s = s; + let mut opt_specs = vec![]; + let mut silent_err = false; + if s.starts_with(':') { + silent_err = true; + s = &s[1..]; + } - let mut chars = s.chars().peekable(); - while let Some(ch) = chars.peek() { - match ch { - ch if ch.is_alphanumeric() => { - let opt = Opt::Short(*ch); - chars.next(); - let takes_arg = chars.peek() == Some(&':'); - if takes_arg { - chars.next(); - } - opt_specs.push(OptSpec { opt, takes_arg }) - } - _ => return Err(ShErr::simple( - ShErrKind::ParseErr, - format!("unexpected character '{}'", ch.fg(next_color())) - )), - } - } + let mut chars = s.chars().peekable(); + while let Some(ch) = chars.peek() { + match ch { + ch if ch.is_alphanumeric() => { + let opt = Opt::Short(*ch); + chars.next(); + let takes_arg = chars.peek() == Some(&':'); + if takes_arg { + chars.next(); + } + opt_specs.push(OptSpec { opt, takes_arg }) + } + _ => { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("unexpected character '{}'", ch.fg(next_color())), + )); + } + } + } - Ok(GetOptsSpec { silent_err, opt_specs }) - } + Ok(GetOptsSpec { + silent_err, + opt_specs, + }) + } } fn advance_optind(opt_index: usize, amount: usize) -> ShResult<()> { - write_vars(|v| v.set_var("OPTIND", VarKind::Str((opt_index + amount).to_string()), VarFlags::NONE)) + write_vars(|v| { + v.set_var( + "OPTIND", + VarKind::Str((opt_index + amount).to_string()), + VarFlags::NONE, + ) + }) } -fn getopts_inner(opts_spec: &GetOptsSpec, opt_var: &str, argv: &[String], blame: Span) -> ShResult<()> { - let opt_index = read_vars(|v| v.get_var("OPTIND").parse::().unwrap_or(1)); - // OPTIND is 1-based - let arr_idx = opt_index.saturating_sub(1); +fn getopts_inner( + opts_spec: &GetOptsSpec, + opt_var: &str, + argv: &[String], + blame: Span, +) -> ShResult<()> { + let opt_index = read_vars(|v| v.get_var("OPTIND").parse::().unwrap_or(1)); + // OPTIND is 1-based + let arr_idx = opt_index.saturating_sub(1); - let Some(arg) = argv.get(arr_idx) else { - state::set_status(1); - return Ok(()) - }; + let Some(arg) = argv.get(arr_idx) else { + state::set_status(1); + return Ok(()); + }; - // "--" stops option processing - if arg.as_str() == "--" { - advance_optind(opt_index, 1)?; - write_meta(|m| m.reset_getopts_char_offset()); - state::set_status(1); - return Ok(()) - } + // "--" stops option processing + if arg.as_str() == "--" { + advance_optind(opt_index, 1)?; + write_meta(|m| m.reset_getopts_char_offset()); + state::set_status(1); + return Ok(()); + } - // Not an option — done - let Some(opt_str) = arg.strip_prefix('-') else { - state::set_status(1); - return Ok(()); - }; + // Not an option — done + let Some(opt_str) = arg.strip_prefix('-') else { + state::set_status(1); + return Ok(()); + }; - // Bare "-" is not an option - if opt_str.is_empty() { - state::set_status(1); - return Ok(()); - } + // Bare "-" is not an option + if opt_str.is_empty() { + state::set_status(1); + return Ok(()); + } - let char_idx = read_meta(|m| m.getopts_char_offset()); - let Some(ch) = opt_str.chars().nth(char_idx) else { - // Ran out of chars in this arg (shouldn't normally happen), - // advance to next arg and signal done for this call - write_meta(|m| m.reset_getopts_char_offset()); - advance_optind(opt_index, 1)?; - state::set_status(1); - return Ok(()); - }; + let char_idx = read_meta(|m| m.getopts_char_offset()); + let Some(ch) = opt_str.chars().nth(char_idx) else { + // Ran out of chars in this arg (shouldn't normally happen), + // advance to next arg and signal done for this call + write_meta(|m| m.reset_getopts_char_offset()); + advance_optind(opt_index, 1)?; + state::set_status(1); + return Ok(()); + }; - let last_char_in_arg = char_idx >= opt_str.len() - 1; + let last_char_in_arg = char_idx >= opt_str.len() - 1; - // Advance past this character: either move to next char in this - // arg, or reset offset and bump OPTIND to the next arg. - let advance_one_char = |last: bool| -> ShResult<()> { - if last { - write_meta(|m| m.reset_getopts_char_offset()); - advance_optind(opt_index, 1)?; - } else { - write_meta(|m| m.inc_getopts_char_offset()); - } - Ok(()) - }; + // Advance past this character: either move to next char in this + // arg, or reset offset and bump OPTIND to the next arg. + let advance_one_char = |last: bool| -> ShResult<()> { + if last { + write_meta(|m| m.reset_getopts_char_offset()); + advance_optind(opt_index, 1)?; + } else { + write_meta(|m| m.inc_getopts_char_offset()); + } + Ok(()) + }; - match opts_spec.matches(ch) { - OptMatch::NoMatch => { - advance_one_char(last_char_in_arg)?; - if opts_spec.silent_err { - write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; - write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?; - } else { - write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; - ShErr::at( - ShErrKind::ExecFail, - blame.clone(), - format!("illegal option '-{}'", ch.fg(next_color())) - ).print_error(); - } - state::set_status(0); - } - OptMatch::IsMatch => { - advance_one_char(last_char_in_arg)?; - write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?; - state::set_status(0); - } - OptMatch::WantsArg => { - write_meta(|m| m.reset_getopts_char_offset()); + match opts_spec.matches(ch) { + OptMatch::NoMatch => { + advance_one_char(last_char_in_arg)?; + if opts_spec.silent_err { + write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; + write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?; + } else { + write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; + ShErr::at( + ShErrKind::ExecFail, + blame.clone(), + format!("illegal option '-{}'", ch.fg(next_color())), + ) + .print_error(); + } + state::set_status(0); + } + OptMatch::IsMatch => { + advance_one_char(last_char_in_arg)?; + write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?; + state::set_status(0); + } + OptMatch::WantsArg => { + write_meta(|m| m.reset_getopts_char_offset()); - if !last_char_in_arg { - // Remaining chars in this arg are the argument: -bVALUE - let optarg: String = opt_str.chars().skip(char_idx + 1).collect(); - write_vars(|v| v.set_var("OPTARG", VarKind::Str(optarg), VarFlags::NONE))?; - advance_optind(opt_index, 1)?; - } else if let Some(next_arg) = argv.get(arr_idx + 1) { - // Next arg is the argument - write_vars(|v| v.set_var("OPTARG", VarKind::Str(next_arg.clone()), VarFlags::NONE))?; - // Skip both the option arg and its value - advance_optind(opt_index, 2)?; - } else { - // Missing required argument - if opts_spec.silent_err { - write_vars(|v| v.set_var(opt_var, VarKind::Str(":".into()), VarFlags::NONE))?; - write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?; - } else { - write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; - ShErr::at( - ShErrKind::ExecFail, - blame.clone(), - format!("option '-{}' requires an argument", ch.fg(next_color())) - ).print_error(); - } - advance_optind(opt_index, 1)?; - state::set_status(0); - return Ok(()); - } + if !last_char_in_arg { + // Remaining chars in this arg are the argument: -bVALUE + let optarg: String = opt_str.chars().skip(char_idx + 1).collect(); + write_vars(|v| v.set_var("OPTARG", VarKind::Str(optarg), VarFlags::NONE))?; + advance_optind(opt_index, 1)?; + } else if let Some(next_arg) = argv.get(arr_idx + 1) { + // Next arg is the argument + write_vars(|v| v.set_var("OPTARG", VarKind::Str(next_arg.clone()), VarFlags::NONE))?; + // Skip both the option arg and its value + advance_optind(opt_index, 2)?; + } else { + // Missing required argument + if opts_spec.silent_err { + write_vars(|v| v.set_var(opt_var, VarKind::Str(":".into()), VarFlags::NONE))?; + write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?; + } else { + write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; + ShErr::at( + ShErrKind::ExecFail, + blame.clone(), + format!("option '-{}' requires an argument", ch.fg(next_color())), + ) + .print_error(); + } + advance_optind(opt_index, 1)?; + state::set_status(0); + return Ok(()); + } - write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?; - state::set_status(0); - } - } + write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?; + state::set_status(0); + } + } - Ok(()) + Ok(()) } pub fn getopts(node: Node) -> ShResult<()> { - let span = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv, - } = node.class - else { - unreachable!() - }; + let span = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } - let mut args = argv.into_iter(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { + argv.remove(0); + } + let mut args = argv.into_iter(); - let Some(arg_string) = args.next() else { - return Err(ShErr::at( - ShErrKind::ExecFail, - span, - "getopts: missing option spec" - )) - }; - let Some(opt_var) = args.next() else { - return Err(ShErr::at( - ShErrKind::ExecFail, - span, - "getopts: missing variable name" - )) - }; + let Some(arg_string) = args.next() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "getopts: missing option spec", + )); + }; + let Some(opt_var) = args.next() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "getopts: missing variable name", + )); + }; - let opts_spec = GetOptsSpec::from_str(&arg_string.0) - .promote_err(arg_string.1.clone())?; + let opts_spec = GetOptsSpec::from_str(&arg_string.0).promote_err(arg_string.1.clone())?; - let explicit_args: Vec = args.map(|s| s.0).collect(); + let explicit_args: Vec = args.map(|s| s.0).collect(); - if !explicit_args.is_empty() { - getopts_inner(&opts_spec, &opt_var.0, &explicit_args, span) - } else { - let pos_params: Vec = read_vars(|v| v.sh_argv().iter().skip(1).cloned().collect()); - getopts_inner(&opts_spec, &opt_var.0, &pos_params, span) - } + if !explicit_args.is_empty() { + getopts_inner(&opts_spec, &opt_var.0, &explicit_args, span) + } else { + let pos_params: Vec = read_vars(|v| v.sh_argv().iter().skip(1).cloned().collect()); + getopts_inner(&opts_spec, &opt_var.0, &pos_params, span) + } } diff --git a/src/builtin/intro.rs b/src/builtin/intro.rs index d7cd552..7de4400 100644 --- a/src/builtin/intro.rs +++ b/src/builtin/intro.rs @@ -2,7 +2,12 @@ use std::{env, os::unix::fs::PermissionsExt, path::Path}; use ariadne::{Fmt, Span}; -use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::{NdRule, Node, execute::prepare_argv, lex::KEYWORDS}, state::{self, ShAlias, ShFunc, read_logic}}; +use crate::{ + builtin::BUILTINS, + libsh::error::{ShErr, ShErrKind, ShResult, next_color}, + parse::{NdRule, Node, execute::prepare_argv, lex::KEYWORDS}, + state::{self, ShAlias, ShFunc, read_logic}, +}; pub fn type_builtin(node: Node) -> ShResult<()> { let NdRule::Command { @@ -14,59 +19,75 @@ pub fn type_builtin(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } - /* - * we have to check in the same order that the dispatcher checks this - * 1. function - * 2. builtin - * 3. command - */ + /* + * we have to check in the same order that the dispatcher checks this + * 1. function + * 2. builtin + * 3. command + */ - 'outer: for (arg,span) in argv { - if let Some(func) = read_logic(|v| v.get_func(&arg)) { - let ShFunc { body: _, source } = func; - let (line, col) = source.line_and_col(); - let name = source.source().name(); - println!("{arg} is a function defined at {name}:{}:{}", line + 1, col + 1); - } else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) { - let ShAlias { body, source } = alias; - let (line, col) = source.line_and_col(); - let name = source.source().name(); - println!("{arg} is an alias for '{body}' defined at {name}:{}:{}", line + 1, col + 1); - } else if BUILTINS.contains(&arg.as_str()) { - println!("{arg} is a shell builtin"); - } else if KEYWORDS.contains(&arg.as_str()) { - println!("{arg} is a shell keyword"); - } else { - let path = env::var("PATH").unwrap_or_default(); - let paths = path.split(':') - .map(Path::new) - .collect::>(); + 'outer: for (arg, span) in argv { + if let Some(func) = read_logic(|v| v.get_func(&arg)) { + let ShFunc { body: _, source } = func; + let (line, col) = source.line_and_col(); + let name = source.source().name(); + println!( + "{arg} is a function defined at {name}:{}:{}", + line + 1, + col + 1 + ); + } else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) { + let ShAlias { body, source } = alias; + let (line, col) = source.line_and_col(); + let name = source.source().name(); + println!( + "{arg} is an alias for '{body}' defined at {name}:{}:{}", + line + 1, + col + 1 + ); + } else if BUILTINS.contains(&arg.as_str()) { + println!("{arg} is a shell builtin"); + } else if KEYWORDS.contains(&arg.as_str()) { + println!("{arg} is a shell keyword"); + } else { + let path = env::var("PATH").unwrap_or_default(); + let paths = path.split(':').map(Path::new).collect::>(); - for path in paths { - if let Ok(entries) = path.read_dir() { - for entry in entries.flatten() { - let Ok(meta) = std::fs::metadata(entry.path()) else { - continue; - }; - let is_exec = meta.permissions().mode() & 0o111 != 0; + for path in paths { + if let Ok(entries) = path.read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; + let is_exec = meta.permissions().mode() & 0o111 != 0; - if meta.is_file() - && is_exec - && let Some(name) = entry.file_name().to_str() - && name == arg { - println!("{arg} is {}", entry.path().display()); - continue 'outer; - } - } - } - } + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + && name == arg + { + println!("{arg} is {}", entry.path().display()); + continue 'outer; + } + } + } + } - state::set_status(1); - return Err(ShErr::at(ShErrKind::NotFound, span, format!("'{}' is not a command, function, or alias", arg.fg(next_color())))); - } - } + state::set_status(1); + return Err(ShErr::at( + ShErrKind::NotFound, + span, + format!( + "'{}' is not a command, function, or alias", + arg.fg(next_color()) + ), + )); + } + } state::set_status(0); Ok(()) diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index 04c918b..0941e8d 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -16,8 +16,8 @@ pub enum JobBehavior { pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> { let blame = node.get_span().clone(); - let cmd_tk = node.get_command(); - let cmd_span = cmd_tk.unwrap().span.clone(); + let cmd_tk = node.get_command(); + let cmd_span = cmd_tk.unwrap().span.clone(); let cmd = match behavior { JobBehavior::Foregound => "fg", JobBehavior::Background => "bg", @@ -31,11 +31,17 @@ pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut argv = argv.into_iter(); if read_jobs(|j| j.get_fg().is_some()) { - return Err(ShErr::at(ShErrKind::InternalErr, cmd_span, format!("Somehow called '{}' with an existing foreground job", cmd))); + return Err(ShErr::at( + ShErrKind::InternalErr, + cmd_span, + format!("Somehow called '{}' with an existing foreground job", cmd), + )); } let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { @@ -55,7 +61,11 @@ pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> { if query_result.is_some() { Ok(j.remove_job(id.clone()).unwrap()) } else { - Err(ShErr::at(ShErrKind::ExecFail, blame.clone(), format!("Job id `{}' not found", tabid))) + Err(ShErr::at( + ShErrKind::ExecFail, + blame.clone(), + format!("Job id `{}' not found", tabid), + )) } })?; @@ -82,12 +92,16 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult { if arg.starts_with('%') { let arg = arg.strip_prefix('%').unwrap(); if arg.chars().all(|ch| ch.is_ascii_digit()) { - let num = arg.parse::().unwrap_or_default(); - if num == 0 { - Err(ShErr::at(ShErrKind::SyntaxErr, blame, format!("Invalid job id: {}", arg.fg(next_color())))) - } else { - Ok(num.saturating_sub(1)) - } + let num = arg.parse::().unwrap_or_default(); + if num == 0 { + Err(ShErr::at( + ShErrKind::SyntaxErr, + blame, + format!("Invalid job id: {}", arg.fg(next_color())), + )) + } else { + Ok(num.saturating_sub(1)) + } } else { let result = write_jobs(|j| { let query_result = j.query(JobID::Command(arg.into())); @@ -95,7 +109,11 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult { }); match result { Some(id) => Ok(id), - None => Err(ShErr::at(ShErrKind::InternalErr, blame, "Found a job but no table id in parse_job_id()")), + None => Err(ShErr::at( + ShErrKind::InternalErr, + blame, + "Found a job but no table id in parse_job_id()", + )), } } } else if arg.chars().all(|ch| ch.is_ascii_digit()) { @@ -115,10 +133,18 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult { match result { Some(id) => Ok(id), - None => Err(ShErr::at(ShErrKind::InternalErr, blame, "Found a job but no table id in parse_job_id()")), + None => Err(ShErr::at( + ShErrKind::InternalErr, + blame, + "Found a job but no table id in parse_job_id()", + )), } } else { - Err(ShErr::at(ShErrKind::SyntaxErr, blame, format!("Invalid arg: {}", arg.fg(next_color())))) + Err(ShErr::at( + ShErrKind::SyntaxErr, + blame, + format!("Invalid arg: {}", arg.fg(next_color())), + )) } } @@ -132,13 +158,19 @@ pub fn jobs(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut flags = JobCmdFlags::empty(); for (arg, span) in argv { let mut chars = arg.chars().peekable(); if chars.peek().is_none_or(|ch| *ch != '-') { - return Err(ShErr::at(ShErrKind::SyntaxErr, span, "Invalid flag in jobs call")); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + span, + "Invalid flag in jobs call", + )); } chars.next(); for ch in chars { @@ -149,7 +181,11 @@ pub fn jobs(node: Node) -> ShResult<()> { 'r' => JobCmdFlags::RUNNING, 's' => JobCmdFlags::STOPPED, _ => { - return Err(ShErr::at(ShErrKind::SyntaxErr, span, "Invalid flag in jobs call")); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + span, + "Invalid flag in jobs call", + )); } }; flags |= flag @@ -162,41 +198,44 @@ pub fn jobs(node: Node) -> ShResult<()> { } pub fn wait(node: Node) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv, - } = node.class - else { - unreachable!() - }; + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } - if read_jobs(|j| j.curr_job().is_none()) { - state::set_status(0); - return Err(ShErr::at(ShErrKind::ExecFail, blame, "wait: No jobs found")); - } - let argv = argv.into_iter() - .map(|arg| { - if arg.0.as_str().chars().all(|ch| ch.is_ascii_digit()) { - Ok(JobID::Pid(Pid::from_raw(arg.0.parse::().unwrap()))) - } else { - Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?)) - } - }) - .collect::>>()?; + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { + argv.remove(0); + } + if read_jobs(|j| j.curr_job().is_none()) { + state::set_status(0); + return Err(ShErr::at(ShErrKind::ExecFail, blame, "wait: No jobs found")); + } + let argv = argv + .into_iter() + .map(|arg| { + if arg.0.as_str().chars().all(|ch| ch.is_ascii_digit()) { + Ok(JobID::Pid(Pid::from_raw(arg.0.parse::().unwrap()))) + } else { + Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?)) + } + }) + .collect::>>()?; - if argv.is_empty() { - write_jobs(|j| j.wait_all_bg())?; - } else { - for arg in argv { - wait_bg(arg)?; - } - } + if argv.is_empty() { + write_jobs(|j| j.wait_all_bg())?; + } else { + for arg in argv { + wait_bg(arg)?; + } + } - // don't set status here, the status of the waited-on job should be the status of the wait builtin - Ok(()) + // don't set status here, the status of the waited-on job should be the status of the wait builtin + Ok(()) } pub fn disown(node: Node) -> ShResult<()> { @@ -210,13 +249,19 @@ pub fn disown(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut argv = argv.into_iter(); let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { id } else { - return Err(ShErr::at(ShErrKind::ExecFail, blame, "disown: No jobs to disown")); + return Err(ShErr::at( + ShErrKind::ExecFail, + blame, + "disown: No jobs to disown", + )); }; let mut tabid = curr_job_id; diff --git a/src/builtin/keymap.rs b/src/builtin/keymap.rs index 7642f7a..0e7ac70 100644 --- a/src/builtin/keymap.rs +++ b/src/builtin/keymap.rs @@ -1,112 +1,130 @@ use crate::{ - expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::keys::KeyEvent, state::{self, write_logic} + expand::expand_keymap, + getopt::{Opt, OptSpec, get_opts_from_tokens}, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node, execute::prepare_argv}, + prelude::*, + readline::keys::KeyEvent, + state::{self, write_logic}, }; bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct KeyMapFlags: u32 { - const NORMAL = 0b0000001; - const INSERT = 0b0000010; - const VISUAL = 0b0000100; - const EX = 0b0001000; - const OP_PENDING = 0b0010000; - const REPLACE = 0b0100000; - const VERBATIM = 0b1000000; - } + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct KeyMapFlags: u32 { + const NORMAL = 0b0000001; + const INSERT = 0b0000010; + const VISUAL = 0b0000100; + const EX = 0b0001000; + const OP_PENDING = 0b0010000; + const REPLACE = 0b0100000; + const VERBATIM = 0b1000000; + } } pub struct KeyMapOpts { - remove: Option, - flags: KeyMapFlags, + remove: Option, + flags: KeyMapFlags, } impl KeyMapOpts { - pub fn from_opts(opts: &[Opt]) -> ShResult { - let mut flags = KeyMapFlags::empty(); - let mut remove = None; - for opt in opts { - match opt { - Opt::Short('n') => flags |= KeyMapFlags::NORMAL, - Opt::Short('i') => flags |= KeyMapFlags::INSERT, - Opt::Short('v') => flags |= KeyMapFlags::VISUAL, - Opt::Short('x') => flags |= KeyMapFlags::EX, - Opt::Short('o') => flags |= KeyMapFlags::OP_PENDING, - Opt::Short('r') => flags |= KeyMapFlags::REPLACE, - Opt::LongWithArg(name, arg) if name == "remove" => { - if remove.is_some() { - return Err(ShErr::simple(ShErrKind::ExecFail, "Duplicate --remove option for keymap".to_string())); - } - remove = Some(arg.clone()); - }, - _ => return Err(ShErr::simple(ShErrKind::ExecFail, format!("Invalid option for keymap: {:?}", opt))), - } - } - if flags.is_empty() { - return Err(ShErr::simple(ShErrKind::ExecFail, "At least one mode option must be specified for keymap".to_string()).with_note("Use -n for normal mode, -i for insert mode, -v for visual mode, -x for ex mode, and -o for operator-pending mode".to_string())); - } - Ok(Self { remove, flags }) - } - pub fn keymap_opts() -> [OptSpec;6] { - [ - OptSpec { - opt: Opt::Short('n'), // normal mode - takes_arg: false - }, - OptSpec { - opt: Opt::Short('i'), // insert mode - takes_arg: false - }, - OptSpec { - opt: Opt::Short('v'), // visual mode - takes_arg: false - }, - OptSpec { - opt: Opt::Short('x'), // ex mode - takes_arg: false - }, - OptSpec { - opt: Opt::Short('o'), // operator-pending mode - takes_arg: false - }, - OptSpec { - opt: Opt::Short('r'), // replace mode - takes_arg: false - }, - ] - } + pub fn from_opts(opts: &[Opt]) -> ShResult { + let mut flags = KeyMapFlags::empty(); + let mut remove = None; + for opt in opts { + match opt { + Opt::Short('n') => flags |= KeyMapFlags::NORMAL, + Opt::Short('i') => flags |= KeyMapFlags::INSERT, + Opt::Short('v') => flags |= KeyMapFlags::VISUAL, + Opt::Short('x') => flags |= KeyMapFlags::EX, + Opt::Short('o') => flags |= KeyMapFlags::OP_PENDING, + Opt::Short('r') => flags |= KeyMapFlags::REPLACE, + Opt::LongWithArg(name, arg) if name == "remove" => { + if remove.is_some() { + return Err(ShErr::simple( + ShErrKind::ExecFail, + "Duplicate --remove option for keymap".to_string(), + )); + } + remove = Some(arg.clone()); + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Invalid option for keymap: {:?}", opt), + )); + } + } + } + if flags.is_empty() { + return Err(ShErr::simple(ShErrKind::ExecFail, "At least one mode option must be specified for keymap".to_string()).with_note("Use -n for normal mode, -i for insert mode, -v for visual mode, -x for ex mode, and -o for operator-pending mode".to_string())); + } + Ok(Self { remove, flags }) + } + pub fn keymap_opts() -> [OptSpec; 6] { + [ + OptSpec { + opt: Opt::Short('n'), // normal mode + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('i'), // insert mode + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('v'), // visual mode + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('x'), // ex mode + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('o'), // operator-pending mode + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('r'), // replace mode + takes_arg: false, + }, + ] + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyMapMatch { - NoMatch, - IsPrefix, - IsExact + NoMatch, + IsPrefix, + IsExact, } #[derive(Debug, Clone)] pub struct KeyMap { - pub flags: KeyMapFlags, - pub keys: String, - pub action: String + pub flags: KeyMapFlags, + pub keys: String, + pub action: String, } impl KeyMap { - pub fn keys_expanded(&self) -> Vec { - expand_keymap(&self.keys) - } - pub fn action_expanded(&self) -> Vec { - expand_keymap(&self.action) - } - pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch { - log::debug!("Comparing keymap keys {:?} with input {:?}", self.keys_expanded(), other); - let ours = self.keys_expanded(); - if other == ours { - KeyMapMatch::IsExact - } else if ours.starts_with(other) { - KeyMapMatch::IsPrefix - } else { - KeyMapMatch::NoMatch - } - } + pub fn keys_expanded(&self) -> Vec { + expand_keymap(&self.keys) + } + pub fn action_expanded(&self) -> Vec { + expand_keymap(&self.action) + } + pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch { + log::debug!( + "Comparing keymap keys {:?} with input {:?}", + self.keys_expanded(), + other + ); + let ours = self.keys_expanded(); + if other == ours { + KeyMapMatch::IsExact + } else if ours.starts_with(other) { + KeyMapMatch::IsPrefix + } else { + KeyMapMatch::NoMatch + } + } } pub fn keymap(node: Node) -> ShResult<()> { @@ -119,32 +137,42 @@ pub fn keymap(node: Node) -> ShResult<()> { unreachable!() }; - let (argv, opts) = get_opts_from_tokens(argv, &KeyMapOpts::keymap_opts())?; - let opts = KeyMapOpts::from_opts(&opts).promote_err(span.clone())?; - if let Some(to_rm) = opts.remove { - write_logic(|l| l.remove_keymap(&to_rm)); - state::set_status(0); - return Ok(()); - } + let (argv, opts) = get_opts_from_tokens(argv, &KeyMapOpts::keymap_opts())?; + let opts = KeyMapOpts::from_opts(&opts).promote_err(span.clone())?; + if let Some(to_rm) = opts.remove { + write_logic(|l| l.remove_keymap(&to_rm)); + state::set_status(0); + return Ok(()); + } let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } - let Some((keys,_)) = argv.first() else { - return Err(ShErr::at(ShErrKind::ExecFail, span, "missing keys argument".to_string())); - }; + let Some((keys, _)) = argv.first() else { + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "missing keys argument".to_string(), + )); + }; - let Some((action,_)) = argv.get(1) else { - return Err(ShErr::at(ShErrKind::ExecFail, span, "missing action argument".to_string())); - }; + let Some((action, _)) = argv.get(1) else { + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "missing action argument".to_string(), + )); + }; - let keymap = KeyMap { - flags: opts.flags, - keys: keys.clone(), - action: action.clone(), - }; + let keymap = KeyMap { + flags: opts.flags, + keys: keys.clone(), + action: action.clone(), + }; - write_logic(|l| l.insert_keymap(keymap)); + write_logic(|l| l.insert_keymap(keymap)); state::set_status(0); Ok(()) diff --git a/src/builtin/map.rs b/src/builtin/map.rs index a8cef63..7952364 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -5,240 +5,239 @@ use nix::{libc::STDOUT_FILENO, unistd::write}; use serde_json::{Map, Value}; use crate::{ - expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_tk, split_tk_at}}, procio::borrow_fd, state::{self, read_vars, write_vars} + expand::expand_cmd_sub, + getopt::{Opt, OptSpec, get_opts_from_tokens}, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{ + NdRule, Node, + lex::{split_tk, split_tk_at}, + }, + procio::borrow_fd, + state::{self, read_vars, write_vars}, }; #[derive(Debug, Clone)] pub enum MapNode { - DynamicLeaf(String), // eval'd on access - StaticLeaf(String), // static value - Array(Vec), - Branch(HashMap), + DynamicLeaf(String), // eval'd on access + StaticLeaf(String), // static value + Array(Vec), + Branch(HashMap), } impl Default for MapNode { - fn default() -> Self { - Self::Branch(HashMap::new()) - } + fn default() -> Self { + Self::Branch(HashMap::new()) + } } impl From for serde_json::Value { - fn from(val: MapNode) -> Self { - match val { - MapNode::Branch(map) => { - let val_map = map.into_iter() - .map(|(k,v)| { - (k,v.into()) - }) - .collect::>(); + fn from(val: MapNode) -> Self { + match val { + MapNode::Branch(map) => { + let val_map = map + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(); - Value::Object(val_map) - } - MapNode::Array(nodes) => { - let arr = nodes - .into_iter() - .map(|node| node.into()) - .collect(); - Value::Array(arr) - } - MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => { - Value::String(leaf) - } - } - } + Value::Object(val_map) + } + MapNode::Array(nodes) => { + let arr = nodes.into_iter().map(|node| node.into()).collect(); + Value::Array(arr) + } + MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => Value::String(leaf), + } + } } impl From for MapNode { - fn from(value: Value) -> Self { - match value { - Value::Object(map) => { - let node_map = map.into_iter() - .map(|(k,v)| { - (k, v.into()) - }) - .collect::>(); + fn from(value: Value) -> Self { + match value { + Value::Object(map) => { + let node_map = map + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(); - MapNode::Branch(node_map) - } - Value::Array(arr) => { - let nodes = arr - .into_iter() - .map(|v| v.into()) - .collect(); - MapNode::Array(nodes) - } - Value::String(s) => MapNode::StaticLeaf(s), - v => MapNode::StaticLeaf(v.to_string()) - } - } + MapNode::Branch(node_map) + } + Value::Array(arr) => { + let nodes = arr.into_iter().map(|v| v.into()).collect(); + MapNode::Array(nodes) + } + Value::String(s) => MapNode::StaticLeaf(s), + v => MapNode::StaticLeaf(v.to_string()), + } + } } impl MapNode { - fn get(&self, path: &[String]) -> Option<&MapNode> { - match path { - [] => Some(self), - [key, rest @ ..] => match self { - MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => None, - MapNode::Array(map_nodes) => { - let idx: usize = key.parse().ok()?; - map_nodes.get(idx)?.get(rest) - } - MapNode::Branch(map) => map.get(key)?.get(rest) - } - } - } + fn get(&self, path: &[String]) -> Option<&MapNode> { + match path { + [] => Some(self), + [key, rest @ ..] => match self { + MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => None, + MapNode::Array(map_nodes) => { + let idx: usize = key.parse().ok()?; + map_nodes.get(idx)?.get(rest) + } + MapNode::Branch(map) => map.get(key)?.get(rest), + }, + } + } - fn set(&mut self, path: &[String], value: MapNode) { - match path { - [] => *self = value, - [key, rest @ ..] => { - if matches!(self, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_)) { - // promote leaf to branch if we still have path left to traverse - *self = Self::default(); - } - match self { - MapNode::Branch(map) => { - let child = map - .entry(key.to_string()) - .or_insert_with(Self::default); - child.set(rest, value); - } - MapNode::Array(map_nodes) => { - let idx: usize = key.parse().expect("expected array index"); - if idx >= map_nodes.len() { - map_nodes.resize(idx + 1, Self::default()); - } - map_nodes[idx].set(rest, value); - } - _ => unreachable!() - } - } - } - } + fn set(&mut self, path: &[String], value: MapNode) { + match path { + [] => *self = value, + [key, rest @ ..] => { + if matches!(self, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_)) { + // promote leaf to branch if we still have path left to traverse + *self = Self::default(); + } + match self { + MapNode::Branch(map) => { + let child = map.entry(key.to_string()).or_insert_with(Self::default); + child.set(rest, value); + } + MapNode::Array(map_nodes) => { + let idx: usize = key.parse().expect("expected array index"); + if idx >= map_nodes.len() { + map_nodes.resize(idx + 1, Self::default()); + } + map_nodes[idx].set(rest, value); + } + _ => unreachable!(), + } + } + } + } - fn remove(&mut self, path: &[String]) -> Option { - match path { - [] => None, - [key] => match self { - MapNode::Branch(map) => map.remove(key), - MapNode::Array(nodes) => { - let idx: usize = key.parse().ok()?; - if idx >= nodes.len() { - return None; - } - Some(nodes.remove(idx)) - } - _ => None - } - [key, rest @ ..] => match self { - MapNode::Branch(map) => map.get_mut(key)?.remove(rest), - MapNode::Array(nodes) => { - let idx: usize = key.parse().ok()?; - if idx >= nodes.len() { - return None; - } - nodes[idx].remove(rest) - } - _ => None - } - } - } + fn remove(&mut self, path: &[String]) -> Option { + match path { + [] => None, + [key] => match self { + MapNode::Branch(map) => map.remove(key), + MapNode::Array(nodes) => { + let idx: usize = key.parse().ok()?; + if idx >= nodes.len() { + return None; + } + Some(nodes.remove(idx)) + } + _ => None, + }, + [key, rest @ ..] => match self { + MapNode::Branch(map) => map.get_mut(key)?.remove(rest), + MapNode::Array(nodes) => { + let idx: usize = key.parse().ok()?; + if idx >= nodes.len() { + return None; + } + nodes[idx].remove(rest) + } + _ => None, + }, + } + } - fn keys(&self) -> Vec { - match self { - MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(), - MapNode::Array(nodes) => nodes.iter().filter_map(|n| n.display(false, false).ok()).collect(), - MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => vec![], - } - } + fn keys(&self) -> Vec { + match self { + MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(), + MapNode::Array(nodes) => nodes + .iter() + .filter_map(|n| n.display(false, false).ok()) + .collect(), + MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => vec![], + } + } - fn display(&self, json: bool, pretty: bool) -> ShResult { - if json || matches!(self, MapNode::Branch(_)) { - let val: Value = self.clone().into(); - if pretty { - match serde_json::to_string_pretty(&val) { - Ok(s) => Ok(s), - Err(e) => Err(ShErr::simple( - ShErrKind::InternalErr, - format!("failed to serialize map: {e}") - )) - } - } else { - match serde_json::to_string(&val) { - Ok(s) => Ok(s), - Err(e) => Err(ShErr::simple( - ShErrKind::InternalErr, - format!("failed to serialize map: {e}") - )) - } - } - } else { - match self { - MapNode::StaticLeaf(leaf) => Ok(leaf.clone()), - MapNode::DynamicLeaf(cmd) => expand_cmd_sub(cmd), - MapNode::Array(nodes) => { - let mut s = String::new(); - for node in nodes { - let display = node.display(json, pretty)?; - if matches!(node, MapNode::Branch(_)) { - s.push_str(&format!("'{}'", display)); - } else { - s.push_str(&node.display(json, pretty)?); - } - s.push('\n'); - } - Ok(s.trim_end_matches('\n').to_string()) - } - _ => unreachable!() - } - } - } + fn display(&self, json: bool, pretty: bool) -> ShResult { + if json || matches!(self, MapNode::Branch(_)) { + let val: Value = self.clone().into(); + if pretty { + match serde_json::to_string_pretty(&val) { + Ok(s) => Ok(s), + Err(e) => Err(ShErr::simple( + ShErrKind::InternalErr, + format!("failed to serialize map: {e}"), + )), + } + } else { + match serde_json::to_string(&val) { + Ok(s) => Ok(s), + Err(e) => Err(ShErr::simple( + ShErrKind::InternalErr, + format!("failed to serialize map: {e}"), + )), + } + } + } else { + match self { + MapNode::StaticLeaf(leaf) => Ok(leaf.clone()), + MapNode::DynamicLeaf(cmd) => expand_cmd_sub(cmd), + MapNode::Array(nodes) => { + let mut s = String::new(); + for node in nodes { + let display = node.display(json, pretty)?; + if matches!(node, MapNode::Branch(_)) { + s.push_str(&format!("'{}'", display)); + } else { + s.push_str(&node.display(json, pretty)?); + } + s.push('\n'); + } + Ok(s.trim_end_matches('\n').to_string()) + } + _ => unreachable!(), + } + } + } } fn map_opts_spec() -> [OptSpec; 6] { - [ - OptSpec { - opt: Opt::Short('r'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('j'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('k'), - takes_arg: false - }, - OptSpec { - opt: Opt::Long("pretty".into()), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('F'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('l'), - takes_arg: false - }, - ] + [ + OptSpec { + opt: Opt::Short('r'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('j'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('k'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Long("pretty".into()), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('F'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('l'), + takes_arg: false, + }, + ] } #[derive(Debug, Clone, Copy)] pub struct MapOpts { - flags: MapFlags, + flags: MapFlags, } bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct MapFlags: u32 { - const REMOVE = 0b000001; - const KEYS = 0b000010; - const JSON = 0b000100; - const LOCAL = 0b001000; - const PRETTY = 0b010000; - const FUNC = 0b100000; - } + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct MapFlags: u32 { + const REMOVE = 0b000001; + const KEYS = 0b000010; + const JSON = 0b000100; + const LOCAL = 0b001000; + const PRETTY = 0b010000; + const FUNC = 0b100000; + } } pub fn map(node: Node) -> ShResult<()> { @@ -250,136 +249,140 @@ pub fn map(node: Node) -> ShResult<()> { unreachable!() }; - let (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?; - let map_opts = get_map_opts(opts); - if !argv.is_empty() { - argv.remove(0); // remove "map" command from argv - } + let (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?; + let map_opts = get_map_opts(opts); + if !argv.is_empty() { + argv.remove(0); // remove "map" command from argv + } - for arg in argv { - if let Some((lhs,rhs)) = split_tk_at(&arg, "=") { - let path = split_tk(&lhs, ".") - .into_iter() - .map(|s| s.expand().map(|exp| exp.get_words().join(" "))) - .collect::>>()?; - let Some(name) = path.first() else { - return Err(ShErr::simple( - ShErrKind::InternalErr, - format!("invalid map path: {}", lhs.as_str()) - )); - }; + for arg in argv { + if let Some((lhs, rhs)) = split_tk_at(&arg, "=") { + let path = split_tk(&lhs, ".") + .into_iter() + .map(|s| s.expand().map(|exp| exp.get_words().join(" "))) + .collect::>>()?; + let Some(name) = path.first() else { + return Err(ShErr::simple( + ShErrKind::InternalErr, + format!("invalid map path: {}", lhs.as_str()), + )); + }; - let is_json = map_opts.flags.contains(MapFlags::JSON); - let is_func = map_opts.flags.contains(MapFlags::FUNC); - let make_leaf = |s: String| { - if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) } - }; - let expanded = rhs.expand()?.get_words().join(" "); - let found = write_vars(|v| -> ShResult { - if let Some(map) = v.get_map_mut(name) { - if is_json { - if let Ok(parsed) = serde_json::from_str::(expanded.as_str()) { - map.set(&path[1..], parsed.into()); - } else { - map.set(&path[1..], make_leaf(expanded.clone())); - } - } else { - map.set(&path[1..], make_leaf(expanded.clone())); - } - Ok(true) - } else { - Ok(false) - } - }); + let is_json = map_opts.flags.contains(MapFlags::JSON); + let is_func = map_opts.flags.contains(MapFlags::FUNC); + let make_leaf = |s: String| { + if is_func { + MapNode::DynamicLeaf(s) + } else { + MapNode::StaticLeaf(s) + } + }; + let expanded = rhs.expand()?.get_words().join(" "); + let found = write_vars(|v| -> ShResult { + if let Some(map) = v.get_map_mut(name) { + if is_json { + if let Ok(parsed) = serde_json::from_str::(expanded.as_str()) { + map.set(&path[1..], parsed.into()); + } else { + map.set(&path[1..], make_leaf(expanded.clone())); + } + } else { + map.set(&path[1..], make_leaf(expanded.clone())); + } + Ok(true) + } else { + Ok(false) + } + }); - if !found? { - let mut new = MapNode::default(); - if is_json /*&& let Ok(parsed) = serde_json::from_str::(rhs.as_str()) */{ - let parsed = serde_json::from_str::(expanded.as_str()).unwrap(); - let node: MapNode = parsed.into(); - new.set(&path[1..], node); - } else { - new.set(&path[1..], make_leaf(expanded)); - } - write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL))); - } - } else { - let expanded = arg.expand()?.get_words().join(" "); - let path: Vec = expanded.split('.').map(|s| s.to_string()).collect(); - let Some(name) = path.first() else { - return Err(ShErr::simple( - ShErrKind::InternalErr, - format!("invalid map path: {}", expanded) - )); - }; + if !found? { + let mut new = MapNode::default(); + if is_json + /*&& let Ok(parsed) = serde_json::from_str::(rhs.as_str()) */ + { + let parsed = serde_json::from_str::(expanded.as_str()).unwrap(); + let node: MapNode = parsed.into(); + new.set(&path[1..], node); + } else { + new.set(&path[1..], make_leaf(expanded)); + } + write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL))); + } + } else { + let expanded = arg.expand()?.get_words().join(" "); + let path: Vec = expanded.split('.').map(|s| s.to_string()).collect(); + let Some(name) = path.first() else { + return Err(ShErr::simple( + ShErrKind::InternalErr, + format!("invalid map path: {}", expanded), + )); + }; - if map_opts.flags.contains(MapFlags::REMOVE) { - write_vars(|v| { - if path.len() == 1 { - v.remove_map(name); - } else { - let Some(map) = v.get_map_mut(name) else { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("map not found: {}", name) - )); - }; - map.remove(&path[1..]); - } + if map_opts.flags.contains(MapFlags::REMOVE) { + write_vars(|v| { + if path.len() == 1 { + v.remove_map(name); + } else { + let Some(map) = v.get_map_mut(name) else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("map not found: {}", name), + )); + }; + map.remove(&path[1..]); + } - Ok(()) - })?; - continue; - } + Ok(()) + })?; + continue; + } - let json = map_opts.flags.contains(MapFlags::JSON); - let pretty = map_opts.flags.contains(MapFlags::PRETTY); - let keys = map_opts.flags.contains(MapFlags::KEYS); - let has_map = read_vars(|v| v.get_map(name).is_some()); - if !has_map { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("map not found: {}", name) - )); - } - let Some(node) = read_vars(|v| { - v.get_map(name) - .and_then(|map| map.get(&path[1..]).cloned()) - }) else { - state::set_status(1); - continue; - }; - let output = if keys { - node.keys().join(" ") - } else { - node.display(json, pretty)? - }; + let json = map_opts.flags.contains(MapFlags::JSON); + let pretty = map_opts.flags.contains(MapFlags::PRETTY); + let keys = map_opts.flags.contains(MapFlags::KEYS); + let has_map = read_vars(|v| v.get_map(name).is_some()); + if !has_map { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("map not found: {}", name), + )); + } + let Some(node) = read_vars(|v| v.get_map(name).and_then(|map| map.get(&path[1..]).cloned())) + else { + state::set_status(1); + continue; + }; + let output = if keys { + node.keys().join(" ") + } else { + node.display(json, pretty)? + }; - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, output.as_bytes())?; - write(stdout, b"\n")?; - } - } + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, output.as_bytes())?; + write(stdout, b"\n")?; + } + } state::set_status(0); Ok(()) } pub fn get_map_opts(opts: Vec) -> MapOpts { - let mut map_opts = MapOpts { - flags: MapFlags::empty() - }; + let mut map_opts = MapOpts { + flags: MapFlags::empty(), + }; - for opt in opts { - match opt { - Opt::Short('r') => map_opts.flags |= MapFlags::REMOVE, - Opt::Short('j') => map_opts.flags |= MapFlags::JSON, - Opt::Short('k') => map_opts.flags |= MapFlags::KEYS, - Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL, - Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY, - Opt::Short('F') => map_opts.flags |= MapFlags::FUNC, - _ => unreachable!() - } - } - map_opts + for opt in opts { + match opt { + Opt::Short('r') => map_opts.flags |= MapFlags::REMOVE, + Opt::Short('j') => map_opts.flags |= MapFlags::JSON, + Opt::Short('k') => map_opts.flags |= MapFlags::KEYS, + Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL, + Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY, + Opt::Short('F') => map_opts.flags |= MapFlags::FUNC, + _ => unreachable!(), + } + } + map_opts } diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index c35cc5c..54ee5cf 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -1,9 +1,8 @@ -use crate::{ - libsh::error::ShResult, - state, -}; +use crate::{libsh::error::ShResult, state}; pub mod alias; +pub mod arrops; +pub mod autocmd; pub mod cd; pub mod complete; pub mod dirstack; @@ -11,7 +10,11 @@ pub mod echo; pub mod eval; pub mod exec; pub mod flowctl; +pub mod getopts; +pub mod intro; pub mod jobctl; +pub mod keymap; +pub mod map; pub mod pwd; pub mod read; pub mod shift; @@ -21,19 +24,13 @@ pub mod test; // [[ ]] thing pub mod trap; pub mod varcmds; pub mod zoltraak; -pub mod map; -pub mod arrops; -pub mod intro; -pub mod getopts; -pub mod keymap; -pub mod autocmd; pub const BUILTINS: [&str; 47] = [ "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", - "getopts", "keymap", "read_key", "autocmd" + "getopts", "keymap", "read_key", "autocmd", ]; pub fn true_builtin() -> ShResult<()> { diff --git a/src/builtin/read.rs b/src/builtin/read.rs index 9d7875f..e866150 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -6,7 +6,16 @@ use nix::{ }; use crate::{ - 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} + 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}, }; pub const READ_OPTS: [OptSpec; 7] = [ @@ -40,19 +49,19 @@ pub const READ_OPTS: [OptSpec; 7] = [ }, // 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 - } +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! { @@ -84,7 +93,9 @@ pub fn read_builtin(node: Node) -> ShResult<()> { let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?; let read_opts = get_read_flags(opts).blame(blame.clone())?; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if let Some(prompt) = read_opts.prompt { write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; @@ -257,96 +268,98 @@ pub fn get_read_flags(opts: Vec) -> ShResult { } pub struct ReadKeyOpts { - var_name: Option, - char_whitelist: Option, - char_blacklist: Option + var_name: Option, + char_whitelist: Option, + char_blacklist: Option, } pub fn read_key(node: Node) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { argv, .. } = node.class else { unreachable!() }; + let blame = node.get_span().clone(); + let NdRule::Command { argv, .. } = node.class else { + unreachable!() + }; - if !isatty(*TTY_FILENO)? { - state::set_status(1); - return Ok(()); - } + 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 (_, 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], false); - 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 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], false); + 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()?; + 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(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(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())?; - } + 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(()) + state::set_status(0); + Ok(()) } pub fn get_read_key_opts(opts: Vec) -> ShResult { - let mut read_key_opts = ReadKeyOpts { - var_name: None, - char_whitelist: None, - char_blacklist: None - }; + 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}'") - )); - } - } - } + 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) + Ok(read_key_opts) } diff --git a/src/builtin/shift.rs b/src/builtin/shift.rs index 4e385b1..c2c6f0a 100644 --- a/src/builtin/shift.rs +++ b/src/builtin/shift.rs @@ -14,12 +14,18 @@ pub fn shift(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } let mut argv = argv.into_iter(); if let Some((arg, span)) = argv.next() { let Ok(count) = arg.parse::() else { - return Err(ShErr::at(ShErrKind::ExecFail, span, "Expected a number in shift args")); + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + "Expected a number in shift args", + )); }; for _ in 0..count { write_vars(|v| v.cur_scope_mut().fpop_arg()); diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index ba7f8f1..520c912 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -16,7 +16,9 @@ pub fn shopt(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if argv.is_empty() { let mut output = write_shopts(|s| s.display_opts())?; diff --git a/src/builtin/source.rs b/src/builtin/source.rs index 2485a3e..4d34f3a 100644 --- a/src/builtin/source.rs +++ b/src/builtin/source.rs @@ -15,15 +15,25 @@ pub fn source(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } for (arg, span) in argv { let path = PathBuf::from(arg); if !path.exists() { - return Err(ShErr::at(ShErrKind::ExecFail, span, format!("source: File '{}' not found", path.display()))); + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + format!("source: File '{}' not found", path.display()), + )); } if !path.is_file() { - return Err(ShErr::at(ShErrKind::ExecFail, span, format!("source: Given path '{}' is not a file", path.display()))); + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + format!("source: Given path '{}' is not a file", path.display()), + )); } source_file(path)?; } diff --git a/src/builtin/test.rs b/src/builtin/test.rs index 14e7439..fbe9fd6 100644 --- a/src/builtin/test.rs +++ b/src/builtin/test.rs @@ -61,10 +61,7 @@ impl FromStr for UnaryOp { "-t" => Ok(Self::Terminal), "-n" => Ok(Self::NonNull), "-z" => Ok(Self::Null), - _ => Err(ShErr::simple( - ShErrKind::SyntaxErr, - "Invalid test operator", - )), + _ => Err(ShErr::simple(ShErrKind::SyntaxErr, "Invalid test operator")), } } } @@ -97,10 +94,7 @@ impl FromStr for TestOp { "-ge" => Ok(Self::IntGe), "-le" => Ok(Self::IntLe), _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::()?)), - _ => Err(ShErr::simple( - ShErrKind::SyntaxErr, - "Invalid test operator", - )), + _ => Err(ShErr::simple(ShErrKind::SyntaxErr, "Invalid test operator")), } } } @@ -138,7 +132,11 @@ pub fn double_bracket_test(node: Node) -> ShResult { let operand = operand.expand()?.get_words().join(" "); conjunct_op = conjunct; let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { - return Err(ShErr::at(ShErrKind::SyntaxErr, err_span, "Invalid unary operator")); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + err_span, + "Invalid unary operator", + )); }; match op { UnaryOp::Exists => { @@ -241,7 +239,11 @@ pub fn double_bracket_test(node: Node) -> ShResult { let test_op = operator.as_str().parse::()?; match test_op { TestOp::Unary(_) => { - return Err(ShErr::at(ShErrKind::SyntaxErr, err_span, "Expected a binary operator in this test call; found a unary operator")); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + err_span, + "Expected a binary operator in this test call; found a unary operator", + )); } TestOp::StringEq => { let pattern = crate::expand::glob_to_regex(rhs.trim(), true); @@ -257,7 +259,11 @@ pub fn double_bracket_test(node: Node) -> ShResult { | TestOp::IntGe | TestOp::IntLe | TestOp::IntEq => { - let err = ShErr::at(ShErrKind::SyntaxErr, err_span.clone(), format!("Expected an integer with '{}' operator", operator)); + let err = ShErr::at( + ShErrKind::SyntaxErr, + err_span.clone(), + format!("Expected an integer with '{}' operator", operator), + ); let Ok(lhs) = lhs.trim().parse::() else { return Err(err); }; diff --git a/src/builtin/trap.rs b/src/builtin/trap.rs index 5ec80d6..24a5218 100644 --- a/src/builtin/trap.rs +++ b/src/builtin/trap.rs @@ -122,7 +122,9 @@ pub fn trap(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if argv.is_empty() { let stdout = borrow_fd(STDOUT_FILENO); diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs index d9cd4e8..dc39447 100644 --- a/src/builtin/varcmds.rs +++ b/src/builtin/varcmds.rs @@ -16,7 +16,11 @@ pub fn readonly(node: Node) -> ShResult<()> { }; // Remove "readonly" from argv - let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; + let argv = if !argv.is_empty() { + &argv[1..] + } else { + &argv[..] + }; if argv.is_empty() { // Display the local variables @@ -67,15 +71,25 @@ pub fn unset(node: Node) -> ShResult<()> { }; let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } if argv.is_empty() { - return Err(ShErr::at(ShErrKind::SyntaxErr, blame, "unset: Expected at least one argument")); + return Err(ShErr::at( + ShErrKind::SyntaxErr, + blame, + "unset: Expected at least one argument", + )); } for (arg, span) in argv { if !read_vars(|v| v.var_exists(&arg)) { - return Err(ShErr::at(ShErrKind::ExecFail, span, format!("unset: No such variable '{arg}'"))); + return Err(ShErr::at( + ShErrKind::ExecFail, + span, + format!("unset: No such variable '{arg}'"), + )); } write_vars(|v| v.unset_var(&arg))?; } @@ -94,7 +108,11 @@ pub fn export(node: Node) -> ShResult<()> { }; // Remove "export" from argv - let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; + let argv = if !argv.is_empty() { + &argv[1..] + } else { + &argv[..] + }; if argv.is_empty() { // Display the environment variables @@ -137,7 +155,11 @@ pub fn local(node: Node) -> ShResult<()> { }; // Remove "local" from argv - let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; + let argv = if !argv.is_empty() { + &argv[1..] + } else { + &argv[..] + }; if argv.is_empty() { // Display the local variables diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index 076271c..4a59e90 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -104,7 +104,9 @@ pub fn zoltraak(node: Node) -> ShResult<()> { } let mut argv = prepare_argv(argv)?; - if !argv.is_empty() { argv.remove(0); } + if !argv.is_empty() { + argv.remove(0); + } for (arg, span) in argv { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { @@ -113,9 +115,7 @@ pub fn zoltraak(node: Node) -> ShResult<()> { ShErrKind::ExecFail, "zoltraak: Attempted to destroy root directory '/'", ) - .with_note( - "If you really want to do this, you can use the --no-preserve-root flag" - ), + .with_note("If you really want to do this, you can use the --no-preserve-root flag"), ); } annihilate(&arg, flags).blame(span)? @@ -176,9 +176,7 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> { ShErrKind::ExecFail, format!("zoltraak: '{path}' is a directory"), ) - .with_note( - "Use the '-r' flag to recursively shred directories" - ), + .with_note("Use the '-r' flag to recursively shred directories"), ); } } diff --git a/src/expand.rs b/src/expand.rs index b28ce8c..4896a58 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -10,13 +10,14 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}; use crate::parse::execute::exec_input; use crate::parse::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep}; use crate::parse::{Redir, RedirType}; +use crate::prelude::*; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::readline::keys::{KeyCode, KeyEvent, ModKeys}; use crate::readline::markers; use crate::state::{ - self, 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::*; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; @@ -25,9 +26,7 @@ impl Tk { pub fn expand(self) -> ShResult { let flags = self.flags; let span = self.span.clone(); - let exp = Expander::new(self)? - .expand() - .promote_err(span.clone())?; + let exp = Expander::new(self)?.expand().promote_err(span.clone())?; let class = TkRule::Expanded { exp }; Ok(Self { class, span, flags }) } @@ -251,14 +250,14 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { prefix.push(next); } } - '\'' => { - qt_state.toggle_single(); - prefix.push(ch); - } - '"' => { - qt_state.toggle_double(); - prefix.push(ch); - } + '\'' => { + qt_state.toggle_single(); + prefix.push(ch); + } + '"' => { + qt_state.toggle_double(); + prefix.push(ch); + } '{' if qt_state.outside() => { break; } @@ -279,14 +278,14 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { inner.push(next); } } - '\'' => { - qt_state.toggle_single(); - inner.push(ch); - } - '"' => { - qt_state.toggle_double(); - inner.push(ch); - } + '\'' => { + qt_state.toggle_single(); + inner.push(ch); + } + '"' => { + qt_state.toggle_double(); + inner.push(ch); + } '{' if qt_state.outside() => { depth += 1; inner.push(ch); @@ -330,14 +329,14 @@ fn split_brace_inner(inner: &str) -> Vec { current.push(next); } } - '\'' => { - qt_state.toggle_single(); - current.push(ch); - } - '"' => { - qt_state.toggle_double(); - current.push(ch); - } + '\'' => { + qt_state.toggle_single(); + current.push(ch); + } + '"' => { + qt_state.toggle_double(); + current.push(ch); + } '{' if qt_state.outside() => { depth += 1; current.push(ch); @@ -534,11 +533,9 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { let arg_sep = markers::ARG_SEP.to_string(); read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) } - ArrIndex::ArgCount => { - read_vars(|v| v.get_arr_elems(&var_name)) - .map(|elems| elems.len().to_string()) - .unwrap_or_else(|_| "0".to_string()) - } + ArrIndex::ArgCount => read_vars(|v| v.get_arr_elems(&var_name)) + .map(|elems| elems.len().to_string()) + .unwrap_or_else(|_| "0".to_string()), ArrIndex::AllJoined => { let ifs = read_vars(|v| v.try_get_var("IFS")) .unwrap_or_else(|| " \t\n".to_string()) @@ -653,7 +650,7 @@ enum ArithTk { Op(ArithOp), LParen, RParen, - Var(String) + Var(String), } impl ArithTk { @@ -695,21 +692,21 @@ impl ArithTk { tokens.push(Self::RParen); chars.next(); } - _ if ch.is_alphabetic() || ch == '_' => { - chars.next(); - let mut var_name = ch.to_string(); - while let Some(ch) = chars.peek() { - match ch { - _ if ch.is_alphabetic() || *ch == '_' => { - var_name.push(*ch); - chars.next(); - } - _ => break - } - } + _ if ch.is_alphabetic() || ch == '_' => { + chars.next(); + let mut var_name = ch.to_string(); + while let Some(ch) = chars.peek() { + match ch { + _ if ch.is_alphabetic() || *ch == '_' => { + var_name.push(*ch); + chars.next(); + } + _ => break, + } + } - tokens.push(Self::Var(var_name)); - } + tokens.push(Self::Var(var_name)); + } _ => { return Ok(None); } @@ -753,22 +750,28 @@ impl ArithTk { } } } - ArithTk::Var(var) => { - let Some(val) = read_vars(|v| v.try_get_var(&var)) else { - return Err(ShErr::simple( - ShErrKind::NotFound, - format!("Undefined variable in arithmetic expression: '{}'",var.fg(next_color())), - )); - }; - let Ok(num) = val.parse::() else { - return Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Variable '{}' does not contain a number", var.fg(next_color())), - )); - }; + ArithTk::Var(var) => { + let Some(val) = read_vars(|v| v.try_get_var(&var)) else { + return Err(ShErr::simple( + ShErrKind::NotFound, + format!( + "Undefined variable in arithmetic expression: '{}'", + var.fg(next_color()) + ), + )); + }; + let Ok(num) = val.parse::() else { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!( + "Variable '{}' does not contain a number", + var.fg(next_color()) + ), + )); + }; - output.push(ArithTk::Num(num)); - } + output.push(ArithTk::Num(num)); + } } } @@ -785,14 +788,14 @@ impl ArithTk { match token { ArithTk::Num(n) => stack.push(n), ArithTk::Op(op) => { - let rhs = stack.pop().ok_or(ShErr::simple( - ShErrKind::ParseErr, - "Missing right-hand operand", - ))?; - let lhs = stack.pop().ok_or(ShErr::simple( - ShErrKind::ParseErr, - "Missing left-hand operand", - ))?; + let rhs = stack.pop().ok_or(ShErr::simple( + ShErrKind::ParseErr, + "Missing right-hand operand", + ))?; + let lhs = stack.pop().ok_or(ShErr::simple( + ShErrKind::ParseErr, + "Missing left-hand operand", + ))?; let result = match op { ArithOp::Add => lhs + rhs, ArithOp::Sub => lhs - rhs, @@ -803,19 +806,19 @@ impl ArithTk { stack.push(result); } _ => { - return Err(ShErr::simple( - ShErrKind::ParseErr, - "Unexpected token during evaluation", - )); + return Err(ShErr::simple( + ShErrKind::ParseErr, + "Unexpected token during evaluation", + )); } } } if stack.len() != 1 { - return Err(ShErr::simple( - ShErrKind::ParseErr, - "Invalid arithmetic expression", - )); + return Err(ShErr::simple( + ShErrKind::ParseErr, + "Invalid arithmetic expression", + )); } Ok(stack[0]) @@ -840,10 +843,10 @@ impl FromStr for ArithOp { '*' => Ok(Self::Mul), '/' => Ok(Self::Div), '%' => Ok(Self::Mod), - _ => Err(ShErr::simple( - ShErrKind::ParseErr, - "Invalid arithmetic operator", - )), + _ => Err(ShErr::simple( + ShErrKind::ParseErr, + "Invalid arithmetic operator", + )), } } } @@ -853,8 +856,8 @@ pub fn expand_arithmetic(raw: &str) -> ShResult> { let unescaped = unescape_math(body); let expanded = expand_raw(&mut unescaped.chars().peekable())?; let Some(tokens) = ArithTk::tokenize(&expanded)? else { - return Ok(None); - }; + return Ok(None); + }; let rpn = ArithTk::to_rpn(tokens)?; let result = ArithTk::eval_rpn(rpn)?; Ok(Some(result.to_string())) @@ -894,7 +897,12 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult { let mut io_stack = IoStack::new(); io_stack.push_frame(io_frame); - if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false, Some("process_sub".into())) { + if let Err(e) = exec_input( + raw.to_string(), + Some(io_stack), + false, + Some("process_sub".into()), + ) { e.print_error(); exit(1); } @@ -925,11 +933,16 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { match unsafe { fork()? } { ForkResult::Child => { io_stack.push_frame(cmd_sub_io_frame); - if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false, Some("command_sub".into())) { + if let Err(e) = exec_input( + raw.to_string(), + Some(io_stack), + false, + Some("command_sub".into()), + ) { e.print_error(); unsafe { libc::_exit(1) }; } - let status = state::get_status(); + let status = state::get_status(); unsafe { libc::_exit(status) }; } ForkResult::Parent { child } => { @@ -956,9 +969,9 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { match status { WtStat::Exited(_, code) => { - state::set_status(code); - Ok(io_buf.as_str()?.trim_end().to_string()) - }, + state::set_status(code); + Ok(io_buf.as_str()?.trim_end().to_string()) + } _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")), } } @@ -1062,76 +1075,76 @@ pub fn unescape_str(raw: &str) -> String { } } } - '$' => { - log::debug!("Found ANSI-C quoting"); - chars.next(); - while let Some(q_ch) = chars.next() { - match q_ch { - '\'' => { - break; - } - '\\' => { - if let Some(esc) = chars.next() { - match esc { - 'n' => result.push('\n'), - 't' => result.push('\t'), - 'r' => result.push('\r'), - '\'' => result.push('\''), - '\\' => result.push('\\'), - 'a' => result.push('\x07'), - 'b' => result.push('\x08'), - 'e' | 'E' => result.push('\x1b'), - 'v' => result.push('\x0b'), - 'x' => { - let mut hex = String::new(); - if let Some(h1) = chars.next() { - hex.push(h1); - } else { - result.push_str("\\x"); - continue; - } - if let Some(h2) = chars.next() { - hex.push(h2); - } else { - result.push_str(&format!("\\x{hex}")); - continue; - } - if let Ok(byte) = u8::from_str_radix(&hex, 16) { - result.push(byte as char); - } else { - result.push_str(&format!("\\x{hex}")); - continue; - } - } - 'o' => { - let mut oct = String::new(); - for _ in 0..3 { - if let Some(o) = chars.peek() { - if o.is_digit(8) { - oct.push(*o); - chars.next(); - } else { - break; - } - } else { - break; - } - } - if let Ok(byte) = u8::from_str_radix(&oct, 8) { - result.push(byte as char); - } else { - result.push_str(&format!("\\o{oct}")); - continue; - } - } - _ => result.push(esc), - } - } - } - _ => result.push(q_ch), - } - } - } + '$' => { + log::debug!("Found ANSI-C quoting"); + chars.next(); + while let Some(q_ch) = chars.next() { + match q_ch { + '\'' => { + break; + } + '\\' => { + if let Some(esc) = chars.next() { + match esc { + 'n' => result.push('\n'), + 't' => result.push('\t'), + 'r' => result.push('\r'), + '\'' => result.push('\''), + '\\' => result.push('\\'), + 'a' => result.push('\x07'), + 'b' => result.push('\x08'), + 'e' | 'E' => result.push('\x1b'), + 'v' => result.push('\x0b'), + 'x' => { + let mut hex = String::new(); + if let Some(h1) = chars.next() { + hex.push(h1); + } else { + result.push_str("\\x"); + continue; + } + if let Some(h2) = chars.next() { + hex.push(h2); + } else { + result.push_str(&format!("\\x{hex}")); + continue; + } + if let Ok(byte) = u8::from_str_radix(&hex, 16) { + result.push(byte as char); + } else { + result.push_str(&format!("\\x{hex}")); + continue; + } + } + 'o' => { + let mut oct = String::new(); + for _ in 0..3 { + if let Some(o) = chars.peek() { + if o.is_digit(8) { + oct.push(*o); + chars.next(); + } else { + break; + } + } else { + break; + } + } + if let Ok(byte) = u8::from_str_radix(&oct, 8) { + result.push(byte as char); + } else { + result.push_str(&format!("\\o{oct}")); + continue; + } + } + _ => result.push(esc), + } + } + } + _ => result.push(q_ch), + } + } + } '"' => { result.push(markers::DUB_QUOTE); break; @@ -1144,14 +1157,14 @@ pub fn unescape_str(raw: &str) -> String { result.push(markers::SNG_QUOTE); while let Some(q_ch) = chars.next() { match q_ch { - '\\' => { - if chars.peek() == Some(&'\'') { - result.push('\''); - chars.next(); - } else { - result.push('\\'); - } - } + '\\' => { + if chars.peek() == Some(&'\'') { + result.push('\''); + chars.next(); + } else { + result.push('\\'); + } + } '\'' => { result.push(markers::SNG_QUOTE); break; @@ -1219,7 +1232,7 @@ pub fn unescape_str(raw: &str) -> String { } } '$' if chars.peek() == Some(&'\'') => { - log::debug!("Found ANSI-C quoting"); + log::debug!("Found ANSI-C quoting"); chars.next(); result.push(markers::SNG_QUOTE); while let Some(q_ch) = chars.next() { @@ -1387,13 +1400,13 @@ impl FromStr for ParamExp { use ParamExp::*; let parse_err = || { - Err(ShErr::simple( - ShErrKind::SyntaxErr, - "Invalid parameter expansion", - ) ) + Err(ShErr::simple( + ShErrKind::SyntaxErr, + "Invalid parameter expansion", + )) }; - log::debug!("Parsing parameter expansion: '{:?}'", s); + log::debug!("Parsing parameter expansion: '{:?}'", s); // Handle indirect var expansion: ${!var} if let Some(var) = s.strip_prefix('!') { @@ -1410,7 +1423,7 @@ impl FromStr for ParamExp { return Ok(RemShortestPrefix(rest.to_string())); } if let Some(rest) = s.strip_prefix("%%") { - log::debug!("Matched longest suffix pattern: '{}'", rest); + log::debug!("Matched longest suffix pattern: '{}'", rest); return Ok(RemLongestSuffix(rest.to_string())); } else if let Some(rest) = s.strip_prefix('%') { return Ok(RemShortestSuffix(rest.to_string())); @@ -1476,11 +1489,11 @@ impl FromStr for ParamExp { pub fn parse_pos_len(s: &str) -> Option<(usize, Option)> { let raw = s.strip_prefix(':')?; 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()); + 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::().ok()?, len.parse::().ok())) } else { - let raw = expand_raw(&mut raw.chars().peekable()).unwrap_or_else(|_| raw.to_string()); + let raw = expand_raw(&mut raw.chars().peekable()).unwrap_or_else(|_| raw.to_string()); Some((raw.parse::().ok()?, None)) } } @@ -1554,10 +1567,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { Some(val) => Ok(val), None => { let expanded = expand_raw(&mut err.chars().peekable())?; - Err(ShErr::simple( - ShErrKind::ExecFail, - expanded, - )) + Err(ShErr::simple(ShErrKind::ExecFail, expanded)) } } } @@ -1565,10 +1575,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { Some(val) => Ok(val), None => { let expanded = expand_raw(&mut err.chars().peekable())?; - Err(ShErr::simple( - ShErrKind::ExecFail, - expanded, - )) + Err(ShErr::simple(ShErrKind::ExecFail, expanded)) } }, ParamExp::Substr(pos) => { @@ -1630,7 +1637,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { ParamExp::RemLongestSuffix(suffix) => { let value = vars.get_var(&var_name); let unescaped = unescape_str(&suffix); - let expanded_suffix = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone()); + let expanded_suffix = + expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone()); let pattern = Pattern::new(&expanded_suffix).unwrap(); for i in 0..=value.len() { let sliced = &value[i..]; @@ -2299,82 +2307,82 @@ pub fn expand_aliases( } pub fn expand_keymap(s: &str) -> Vec { - let mut keys = Vec::new(); - let mut chars = s.chars().collect::>(); - while let Some(ch) = chars.pop_front() { - match ch { - '\\' => { - if let Some(next_ch) = chars.pop_front() { - keys.push(KeyEvent(KeyCode::Char(next_ch), ModKeys::NONE)); - } - } - '<' => { - let mut alias = String::new(); - while let Some(a_ch) = chars.pop_front() { - match a_ch { - '\\' => { - if let Some(esc_ch) = chars.pop_front() { - alias.push(esc_ch); - } - } - '>' => { - if alias.eq_ignore_ascii_case("leader") { - let mut leader = read_shopts(|o| o.prompt.leader.clone()); - if leader == "\\" { - leader.push('\\'); - } - keys.extend(expand_keymap(&leader)); - } else if let Some(key) = parse_key_alias(&alias) { - keys.push(key); - } - break; - } - _ => alias.push(a_ch), - } - } - } - _ => { - keys.push(KeyEvent(KeyCode::Char(ch), ModKeys::NONE)); - } - } - } + let mut keys = Vec::new(); + let mut chars = s.chars().collect::>(); + while let Some(ch) = chars.pop_front() { + match ch { + '\\' => { + if let Some(next_ch) = chars.pop_front() { + keys.push(KeyEvent(KeyCode::Char(next_ch), ModKeys::NONE)); + } + } + '<' => { + let mut alias = String::new(); + while let Some(a_ch) = chars.pop_front() { + match a_ch { + '\\' => { + if let Some(esc_ch) = chars.pop_front() { + alias.push(esc_ch); + } + } + '>' => { + if alias.eq_ignore_ascii_case("leader") { + let mut leader = read_shopts(|o| o.prompt.leader.clone()); + if leader == "\\" { + leader.push('\\'); + } + keys.extend(expand_keymap(&leader)); + } else if let Some(key) = parse_key_alias(&alias) { + keys.push(key); + } + break; + } + _ => alias.push(a_ch), + } + } + } + _ => { + keys.push(KeyEvent(KeyCode::Char(ch), ModKeys::NONE)); + } + } + } - keys + keys } pub fn parse_key_alias(alias: &str) -> Option { - let parts: Vec<&str> = alias.split('-').collect(); - let (mods_parts, key_name) = parts.split_at(parts.len() - 1); - let mut mods = ModKeys::NONE; - for m in mods_parts { - match m.to_uppercase().as_str() { - "C" => mods |= ModKeys::CTRL, - "A" | "M" => mods |= ModKeys::ALT, - "S" => mods |= ModKeys::SHIFT, - _ => return None, - } - } + let parts: Vec<&str> = alias.split('-').collect(); + let (mods_parts, key_name) = parts.split_at(parts.len() - 1); + let mut mods = ModKeys::NONE; + for m in mods_parts { + match m.to_uppercase().as_str() { + "C" => mods |= ModKeys::CTRL, + "A" | "M" => mods |= ModKeys::ALT, + "S" => mods |= ModKeys::SHIFT, + _ => return None, + } + } - let key = match *key_name.first()? { - "CR" => KeyCode::Char('\r'), - "ENTER" | "RETURN" => KeyCode::Enter, - "ESC" | "ESCAPE" => KeyCode::Esc, - "TAB" => KeyCode::Tab, - "BS" | "BACKSPACE" => KeyCode::Backspace, - "DEL" | "DELETE" => KeyCode::Delete, - "INS" | "INSERT" => KeyCode::Insert, - "SPACE" => KeyCode::Char(' '), - "UP" => KeyCode::Up, - "DOWN" => KeyCode::Down, - "LEFT" => KeyCode::Left, - "RIGHT" => KeyCode::Right, - "HOME" => KeyCode::Home, - "END" => KeyCode::End, - "PGUP" | "PAGEUP" => KeyCode::PageUp, - "PGDN" | "PAGEDOWN" => KeyCode::PageDown, - k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()), - _ => return None - }; + let key = match *key_name.first()? { + "CR" => KeyCode::Char('\r'), + "ENTER" | "RETURN" => KeyCode::Enter, + "ESC" | "ESCAPE" => KeyCode::Esc, + "TAB" => KeyCode::Tab, + "BS" | "BACKSPACE" => KeyCode::Backspace, + "DEL" | "DELETE" => KeyCode::Delete, + "INS" | "INSERT" => KeyCode::Insert, + "SPACE" => KeyCode::Char(' '), + "UP" => KeyCode::Up, + "DOWN" => KeyCode::Down, + "LEFT" => KeyCode::Left, + "RIGHT" => KeyCode::Right, + "HOME" => KeyCode::Home, + "END" => KeyCode::End, + "PGUP" | "PAGEUP" => KeyCode::PageUp, + "PGDN" | "PAGEDOWN" => KeyCode::PageDown, + k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()), + _ => return None, + }; - Some(KeyEvent(key, mods)) + Some(KeyEvent(key, mods)) } diff --git a/src/jobs.rs b/src/jobs.rs index 930cb61..3a19138 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -59,18 +59,12 @@ impl fmt::Display for DisplayWaitStatus { } pub fn code_from_status(stat: &WtStat) -> Option { - match stat { - WtStat::Exited(_, exit_code) => { - Some(*exit_code) - } - WtStat::Stopped(_, sig) => { - Some(SIG_EXIT_OFFSET + *sig as i32) - } - WtStat::Signaled(_, sig, _) => { - Some(SIG_EXIT_OFFSET + *sig as i32) - } - _ => { None } - } + match stat { + WtStat::Exited(_, exit_code) => Some(*exit_code), + WtStat::Stopped(_, sig) => Some(SIG_EXIT_OFFSET + *sig as i32), + WtStat::Signaled(_, sig, _) => Some(SIG_EXIT_OFFSET + *sig as i32), + _ => None, + } } #[derive(Clone, Debug)] @@ -186,7 +180,12 @@ impl JobTab { } pub fn curr_job(&self) -> Option { // Find the most recent valid job (order can have stale entries) - self.order.iter().rev().find(|&&id| self.jobs.get(id).is_some_and(|slot| slot.is_some())).copied() + self + .order + .iter() + .rev() + .find(|&&id| self.jobs.get(id).is_some_and(|slot| slot.is_some())) + .copied() } pub fn prev_job(&self) -> Option { // Find the second most recent valid job @@ -233,7 +232,7 @@ impl JobTab { self.next_open_pos() }; job.set_tabid(tab_pos); - let last_pid = job.children().last().map(|c| c.pid()); + let last_pid = job.children().last().map(|c| c.pid()); self.order.push(tab_pos); if !silent { write( @@ -247,9 +246,9 @@ impl JobTab { self.jobs[tab_pos] = Some(job); } - if let Some(pid) = last_pid { - write_vars(|v| v.set_param(ShellParam::LastJob, &pid.to_string())) - } + if let Some(pid) = last_pid { + write_vars(|v| v.set_param(ShellParam::LastJob, &pid.to_string())) + } Ok(tab_pos) } @@ -281,25 +280,23 @@ impl JobTab { }), } } - pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { - let Some(job) = self.query_mut(id.clone()) else { - return Ok(()) - }; - match id { - JobID::Pid(pid) => { - let Some(child) = job.children_mut().iter_mut().find(|c| c.pid() == pid) else { - return Ok(()) - }; - child.set_stat(stat); - } - JobID::Pgid(_) | - JobID::TableID(_) | - JobID::Command(_) => { - job.set_stats(stat); - } - } - Ok(()) - } + pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { + let Some(job) = self.query_mut(id.clone()) else { + return Ok(()); + }; + match id { + JobID::Pid(pid) => { + let Some(child) = job.children_mut().iter_mut().find(|c| c.pid() == pid) else { + return Ok(()); + }; + child.set_stat(stat); + } + JobID::Pgid(_) | JobID::TableID(_) | JobID::Command(_) => { + job.set_stats(stat); + } + } + Ok(()) + } pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { match identifier { // Match by process group ID @@ -355,17 +352,17 @@ impl JobTab { } Ok(()) } - pub fn wait_all_bg(&mut self) -> ShResult<()> { - disable_reaping(); - defer! { - enable_reaping(); - } - for job in self.jobs.iter_mut() { - let Some(job) = job else { continue }; - job.wait_pgrp()?; - } - Ok(()) - } + pub fn wait_all_bg(&mut self) -> ShResult<()> { + disable_reaping(); + defer! { + enable_reaping(); + } + for job in self.jobs.iter_mut() { + let Some(job) = job else { continue }; + job.wait_pgrp()?; + } + Ok(()) + } pub fn remove_job(&mut self, id: JobID) -> Option { let tabid = self.query(id).map(|job| job.tabid().unwrap()); if let Some(tabid) = tabid { @@ -611,12 +608,11 @@ impl Job { pub fn children_mut(&mut self) -> &mut Vec { &mut self.children } - pub fn is_done(&self) -> bool { - self - .children - .iter() - .all(|chld| chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true)) - } + pub fn is_done(&self) -> bool { + self.children.iter().all(|chld| { + chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true) + }) + } pub fn killpg(&mut self, sig: Signal) -> ShResult<()> { let stat = match sig { Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), @@ -711,8 +707,8 @@ impl Job { let padding = " ".repeat(padding_count); let mut output = String::new(); - let id_box = format!("[{}]{}", id + 1, symbol); - output.push_str(&format!("{id_box}\t")); + let id_box = format!("[{}]{}", id + 1, symbol); + output.push_str(&format!("{id_box}\t")); for (i, cmd) in self.get_cmds().iter().enumerate() { let pid = if pids || init { let mut pid = self.get_pids().get(i).unwrap().to_string(); @@ -735,12 +731,12 @@ impl Job { }, _ => stat_line.styled(Style::Cyan), }; - if i != 0 { - let padding = " ".repeat(id_box.len() - 1); + if i != 0 { + let padding = " ".repeat(id_box.len() - 1); stat_line = format!("{padding}{}", stat_line); - } + } if i != self.get_cmds().len() - 1 { - stat_line.push_str(" |"); + stat_line.push_str(" |"); } let stat_final = if long { @@ -767,61 +763,64 @@ pub fn term_ctlr() -> Pid { /// Calls attach_tty() on the shell's process group to retake control of the /// terminal pub fn take_term() -> ShResult<()> { - // take the terminal back + // take the terminal back attach_tty(getpgrp())?; - // send SIGWINCH to tell readline to update its window size in case it changed while we were in the background - killpg(getpgrp(), Signal::SIGWINCH)?; + // send SIGWINCH to tell readline to update its window size in case it changed while we were in the background + killpg(getpgrp(), Signal::SIGWINCH)?; Ok(()) } pub fn wait_bg(id: JobID) -> ShResult<()> { - disable_reaping(); - defer! { - enable_reaping(); - }; - match id { - JobID::Pid(pid) => { - let stat = loop { - match waitpid(pid, None) { - Ok(stat) => break stat, - Err(Errno::EINTR) => continue, // Retry on signal interruption - Err(Errno::ECHILD) => return Ok(()), // No such child, treat as already reaped - Err(e) => return Err(e.into()), - } - }; - write_jobs(|j| j.update_by_id(id, stat))?; - set_status(code_from_status(&stat).unwrap_or(0)); - } - _ => { - let Some(mut job) = write_jobs(|j| j.remove_job(id.clone())) else { - return Err(ShErr::simple(ShErrKind::ExecFail, format!("wait: No such job with id {:?}", id))); - }; - let statuses = job.wait_pgrp()?; - let mut was_stopped = false; - let mut code = 0; - for status in statuses { - code = code_from_status(&status).unwrap_or(0); - match status { - WtStat::Stopped(_, _) => { - was_stopped = true; - } - WtStat::Signaled(_, sig, _) => { - if sig == Signal::SIGTSTP { - was_stopped = true; - } - } - _ => { /* Do nothing */ } - } - } + disable_reaping(); + defer! { + enable_reaping(); + }; + match id { + JobID::Pid(pid) => { + let stat = loop { + match waitpid(pid, None) { + Ok(stat) => break stat, + Err(Errno::EINTR) => continue, // Retry on signal interruption + Err(Errno::ECHILD) => return Ok(()), // No such child, treat as already reaped + Err(e) => return Err(e.into()), + } + }; + write_jobs(|j| j.update_by_id(id, stat))?; + set_status(code_from_status(&stat).unwrap_or(0)); + } + _ => { + let Some(mut job) = write_jobs(|j| j.remove_job(id.clone())) else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("wait: No such job with id {:?}", id), + )); + }; + let statuses = job.wait_pgrp()?; + let mut was_stopped = false; + let mut code = 0; + for status in statuses { + code = code_from_status(&status).unwrap_or(0); + match status { + WtStat::Stopped(_, _) => { + was_stopped = true; + } + WtStat::Signaled(_, sig, _) => { + if sig == Signal::SIGTSTP { + was_stopped = true; + } + } + _ => { /* Do nothing */ } + } + } - if was_stopped { - write_jobs(|j| j.insert_job(job, false))?; - } - set_status(code); - } - } - Ok(()) + if was_stopped { + write_jobs(|j| j.insert_job(job, false))?; + } + set_status(code); + } + } + Ok(()) } /// Waits on the current foreground job and updates the shell's last status code @@ -835,12 +834,12 @@ pub fn wait_fg(job: Job, interactive: bool) -> ShResult<()> { attach_tty(job.pgid())?; } disable_reaping(); - defer! { - enable_reaping(); - } + defer! { + enable_reaping(); + } let statuses = write_jobs(|j| j.new_fg(job))?; for status in statuses { - code = code_from_status(&status).unwrap_or(0); + code = code_from_status(&status).unwrap_or(0); match status { WtStat::Stopped(_, _) => { was_stopped = true; diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 024059f..cafdbca 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,9 +1,9 @@ -use std::cell::RefCell; -use std::collections::{HashMap, VecDeque}; -use std::fmt::Display; use ariadne::Color; use ariadne::{Report, ReportKind}; use rand::TryRng; +use std::cell::RefCell; +use std::collections::{HashMap, VecDeque}; +use std::fmt::Display; use crate::procio::RedirGuard; use crate::{ @@ -15,96 +15,96 @@ use crate::{ pub type ShResult = Result; pub struct ColorRng { - last_color: Option, + last_color: Option, } impl ColorRng { - fn get_colors() -> &'static [Color] { - &[ - Color::Red, - Color::Cyan, - Color::Blue, - Color::Green, - Color::Yellow, - Color::Magenta, - Color::Fixed(208), // orange - Color::Fixed(39), // deep sky blue - Color::Fixed(170), // orchid / magenta-pink - Color::Fixed(76), // chartreuse - Color::Fixed(51), // aqua - Color::Fixed(226), // bright yellow - Color::Fixed(99), // slate blue - Color::Fixed(214), // light orange - Color::Fixed(48), // spring green - Color::Fixed(201), // hot pink - Color::Fixed(81), // steel blue - Color::Fixed(220), // gold - Color::Fixed(105), // medium purple - ] - } + fn get_colors() -> &'static [Color] { + &[ + Color::Red, + Color::Cyan, + Color::Blue, + Color::Green, + Color::Yellow, + Color::Magenta, + Color::Fixed(208), // orange + Color::Fixed(39), // deep sky blue + Color::Fixed(170), // orchid / magenta-pink + Color::Fixed(76), // chartreuse + Color::Fixed(51), // aqua + Color::Fixed(226), // bright yellow + Color::Fixed(99), // slate blue + Color::Fixed(214), // light orange + Color::Fixed(48), // spring green + Color::Fixed(201), // hot pink + Color::Fixed(81), // steel blue + Color::Fixed(220), // gold + Color::Fixed(105), // medium purple + ] + } - pub fn last_color(&mut self) -> Color { - if let Some(color) = self.last_color.take() { - color - } else { - let color = self.next().unwrap_or(Color::White); - self.last_color = Some(color); - color - } - } + pub fn last_color(&mut self) -> Color { + if let Some(color) = self.last_color.take() { + color + } else { + let color = self.next().unwrap_or(Color::White); + self.last_color = Some(color); + color + } + } } impl Iterator for ColorRng { - type Item = Color; - fn next(&mut self) -> Option { - let colors = Self::get_colors(); - let idx = rand::rngs::SysRng.try_next_u32().ok()? as usize % colors.len(); - Some(colors[idx]) - } + type Item = Color; + fn next(&mut self) -> Option { + let colors = Self::get_colors(); + let idx = rand::rngs::SysRng.try_next_u32().ok()? as usize % colors.len(); + Some(colors[idx]) + } } thread_local! { - static COLOR_RNG: RefCell = const { RefCell::new(ColorRng { last_color: None }) }; + static COLOR_RNG: RefCell = const { RefCell::new(ColorRng { last_color: None }) }; } pub fn next_color() -> Color { - COLOR_RNG.with(|rng| { - let color = rng.borrow_mut().next().unwrap(); - rng.borrow_mut().last_color = Some(color); - color - }) + COLOR_RNG.with(|rng| { + let color = rng.borrow_mut().next().unwrap(); + rng.borrow_mut().last_color = Some(color); + color + }) } pub fn last_color() -> Color { - COLOR_RNG.with(|rng| rng.borrow_mut().last_color()) + COLOR_RNG.with(|rng| rng.borrow_mut().last_color()) } pub fn clear_color() { - COLOR_RNG.with(|rng| rng.borrow_mut().last_color = None); + COLOR_RNG.with(|rng| rng.borrow_mut().last_color = None); } pub trait ShResultExt { fn blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self; - fn promote_err(self, span: Span) -> Self; - fn is_flow_control(&self) -> bool; + fn promote_err(self, span: Span) -> Self; + fn is_flow_control(&self) -> bool; } impl ShResultExt for Result { /// Blame a span for an error fn blame(self, new_span: Span) -> Self { - self.map_err(|e| e.blame(new_span)) + self.map_err(|e| e.blame(new_span)) } /// Blame a span if no blame has been assigned yet fn try_blame(self, new_span: Span) -> Self { - self.map_err(|e| e.try_blame(new_span)) + self.map_err(|e| e.try_blame(new_span)) + } + fn promote_err(self, span: Span) -> Self { + self.map_err(|e| e.promote(span)) + } + fn is_flow_control(&self) -> bool { + self.as_ref().is_err_and(|e| e.is_flow_control()) } - fn promote_err(self, span: Span) -> Self { - 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)] @@ -163,160 +163,256 @@ impl Display for Note { #[derive(Debug)] pub struct ShErr { - kind: ShErrKind, - src_span: Option, - labels: Vec>, - sources: Vec, - notes: Vec, + kind: ShErrKind, + src_span: Option, + labels: Vec>, + sources: Vec, + notes: Vec, - /// If we propagate through a redirect boundary, we take ownership of - /// the RedirGuard(s) so that redirections stay alive until the error - /// is printed. Multiple guards can accumulate as the error bubbles - /// through nested redirect scopes. - io_guards: Vec + /// If we propagate through a redirect boundary, we take ownership of + /// the RedirGuard(s) so that redirections stay alive until the error + /// is printed. Multiple guards can accumulate as the error bubbles + /// through nested redirect scopes. + io_guards: Vec, } impl ShErr { - pub fn new(kind: ShErrKind, span: Span) -> Self { - Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![], io_guards: vec![] } - } - pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { - 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 { - if self.notes.is_empty() { - return self - } - let first = self.notes[0].clone(); - if self.notes.len() > 1 { - self.notes = self.notes[1..].to_vec(); - } + pub fn new(kind: ShErrKind, span: Span) -> Self { + Self { + kind, + src_span: Some(span), + labels: vec![], + sources: vec![], + notes: vec![], + io_guards: vec![], + } + } + pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { + 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 { + if self.notes.is_empty() { + return self; + } + let first = self.notes[0].clone(); + if self.notes.len() > 1 { + self.notes = self.notes[1..].to_vec(); + } - self.labeled(span, first) - } - pub fn with_redirs(mut self, guard: RedirGuard) -> Self { - self.io_guards.push(guard); - self - } - pub fn at(kind: ShErrKind, span: Span, msg: impl Into) -> Self { - let color = last_color(); // use last_color to ensure the same color is used for the label and the message given - let src = span.span_source().clone(); - let msg: String = msg.into(); - Self::new(kind, span.clone()) - .with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) - } - pub fn labeled(self, span: Span, msg: impl Into) -> Self { - let color = last_color(); - let src = span.span_source().clone(); - let msg: String = msg.into(); - self.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) - } - pub fn blame(self, span: Span) -> Self { - let ShErr { kind, src_span: _, labels, sources, notes, io_guards } = self; - Self { kind, src_span: Some(span), labels, sources, notes, io_guards } - } - pub fn try_blame(self, span: Span) -> Self { - match self { - ShErr { kind, src_span: None, labels, sources, notes, io_guards } => Self { kind, src_span: Some(span), labels, sources, notes, io_guards }, - _ => self - } - } - pub fn kind(&self) -> &ShErrKind { - &self.kind - } - pub fn rename(mut self, name: impl Into) -> Self { - if let Some(span) = self.src_span.as_mut() { - span.rename(name.into()); - } - self - } - pub fn with_label(self, source: SpanSource, label: ariadne::Label) -> Self { - let ShErr { kind, src_span, mut labels, mut sources, notes, io_guards } = self; - sources.push(source); - labels.push(label); - Self { kind, src_span, labels, sources, notes, io_guards } - } - pub fn with_context(self, ctx: VecDeque<(SpanSource, ariadne::Label)>) -> Self { - let ShErr { kind, src_span, mut labels, mut sources, notes, io_guards } = self; - for (src, label) in ctx { - sources.push(src); - labels.push(label); - } - Self { kind, src_span, labels, sources, notes, io_guards } - } - pub fn with_note(self, note: impl Into) -> Self { - let ShErr { kind, src_span, labels, sources, mut notes, io_guards } = self; - notes.push(note.into()); - Self { kind, src_span, labels, sources, notes, io_guards } - } - pub fn build_report(&self) -> Option> { - let span = self.src_span.as_ref()?; - let mut report = Report::build(ReportKind::Error, span.clone()) - .with_config(ariadne::Config::default().with_color(true)); - let msg = if self.notes.is_empty() { - self.kind.to_string() - } else { - format!("{} - {}", self.kind, self.notes.first().unwrap()) - }; - report = report.with_message(msg); + self.labeled(span, first) + } + pub fn with_redirs(mut self, guard: RedirGuard) -> Self { + self.io_guards.push(guard); + self + } + pub fn at(kind: ShErrKind, span: Span, msg: impl Into) -> Self { + let color = last_color(); // use last_color to ensure the same color is used for the label and the message given + let src = span.span_source().clone(); + let msg: String = msg.into(); + Self::new(kind, span.clone()).with_label( + src, + ariadne::Label::new(span) + .with_color(color) + .with_message(msg), + ) + } + pub fn labeled(self, span: Span, msg: impl Into) -> Self { + let color = last_color(); + let src = span.span_source().clone(); + let msg: String = msg.into(); + self.with_label( + src, + ariadne::Label::new(span) + .with_color(color) + .with_message(msg), + ) + } + pub fn blame(self, span: Span) -> Self { + let ShErr { + kind, + src_span: _, + labels, + sources, + notes, + io_guards, + } = self; + Self { + kind, + src_span: Some(span), + labels, + sources, + notes, + io_guards, + } + } + pub fn try_blame(self, span: Span) -> Self { + match self { + ShErr { + kind, + src_span: None, + labels, + sources, + notes, + io_guards, + } => Self { + kind, + src_span: Some(span), + labels, + sources, + notes, + io_guards, + }, + _ => self, + } + } + pub fn kind(&self) -> &ShErrKind { + &self.kind + } + pub fn rename(mut self, name: impl Into) -> Self { + if let Some(span) = self.src_span.as_mut() { + span.rename(name.into()); + } + self + } + pub fn with_label(self, source: SpanSource, label: ariadne::Label) -> Self { + let ShErr { + kind, + src_span, + mut labels, + mut sources, + notes, + io_guards, + } = self; + sources.push(source); + labels.push(label); + Self { + kind, + src_span, + labels, + sources, + notes, + io_guards, + } + } + pub fn with_context(self, ctx: VecDeque<(SpanSource, ariadne::Label)>) -> Self { + let ShErr { + kind, + src_span, + mut labels, + mut sources, + notes, + io_guards, + } = self; + for (src, label) in ctx { + sources.push(src); + labels.push(label); + } + Self { + kind, + src_span, + labels, + sources, + notes, + io_guards, + } + } + pub fn with_note(self, note: impl Into) -> Self { + let ShErr { + kind, + src_span, + labels, + sources, + mut notes, + io_guards, + } = self; + notes.push(note.into()); + Self { + kind, + src_span, + labels, + sources, + notes, + io_guards, + } + } + pub fn build_report(&self) -> Option> { + let span = self.src_span.as_ref()?; + let mut report = Report::build(ReportKind::Error, span.clone()) + .with_config(ariadne::Config::default().with_color(true)); + let msg = if self.notes.is_empty() { + self.kind.to_string() + } else { + format!("{} - {}", self.kind, self.notes.first().unwrap()) + }; + report = report.with_message(msg); - for label in self.labels.clone() { - report = report.with_label(label); - } - for note in &self.notes { - report = report.with_note(note); - } + for label in self.labels.clone() { + report = report.with_label(label); + } + for note in &self.notes { + report = report.with_note(note); + } - Some(report.finish()) - } - fn collect_sources(&self) -> HashMap { - let mut source_map = HashMap::new(); - if let Some(span) = &self.src_span { - let src = span.span_source().clone(); - source_map.entry(src.clone()) - .or_insert_with(|| src.content().to_string()); - } - for src in &self.sources { - source_map.entry(src.clone()) - .or_insert_with(|| src.content().to_string()); - } - source_map - } - pub fn print_error(&self) { - let default = || { - eprintln!("\n{}", self.kind); - for note in &self.notes { - eprintln!("note: {note}"); - } - }; - let Some(report) = self.build_report() else { - return default(); - }; + Some(report.finish()) + } + fn collect_sources(&self) -> HashMap { + let mut source_map = HashMap::new(); + if let Some(span) = &self.src_span { + let src = span.span_source().clone(); + source_map + .entry(src.clone()) + .or_insert_with(|| src.content().to_string()); + } + for src in &self.sources { + source_map + .entry(src.clone()) + .or_insert_with(|| src.content().to_string()); + } + source_map + } + pub fn print_error(&self) { + let default = || { + eprintln!("\n{}", self.kind); + for note in &self.notes { + eprintln!("note: {note}"); + } + }; + let Some(report) = self.build_report() else { + return default(); + }; - let sources = self.collect_sources(); - let cache = ariadne::FnCache::new(move |src: &SpanSource| { - sources.get(src) - .cloned() - .ok_or_else(|| format!("Failed to fetch source '{}'", src.name())) - }); - eprintln!(); - if report.eprint(cache).is_err() { - default(); - } - } + let sources = self.collect_sources(); + let cache = ariadne::FnCache::new(move |src: &SpanSource| { + sources + .get(src) + .cloned() + .ok_or_else(|| format!("Failed to fetch source '{}'", src.name())) + }); + eprintln!(); + if report.eprint(cache).is_err() { + default(); + } + } } impl Display for ShErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.notes.is_empty() { - write!(f, "{}", self.kind) - } else { - write!(f, "{} - {}", self.kind, self.notes.first().unwrap()) - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.notes.is_empty() { + write!(f, "{}", self.kind) + } else { + write!(f, "{} - {}", self.kind, self.notes.first().unwrap()) + } + } } impl From for ShErr { @@ -350,7 +446,7 @@ pub enum ShErrKind { ResourceLimitExceeded, BadPermission, Errno(Errno), - NotFound, + NotFound, ReadlineErr, ExCommand, @@ -364,15 +460,16 @@ pub enum ShErrKind { } impl ShErrKind { - pub fn is_flow_control(&self) -> bool { - matches!(self, - Self::CleanExit(_) | - Self::FuncReturn(_) | - Self::LoopContinue(_) | - Self::LoopBreak(_) | - Self::ClearReadline - ) - } + pub fn is_flow_control(&self) -> bool { + matches!( + self, + Self::CleanExit(_) + | Self::FuncReturn(_) + | Self::LoopContinue(_) + | Self::LoopBreak(_) + | Self::ClearReadline + ) + } } impl Display for ShErrKind { diff --git a/src/libsh/guards.rs b/src/libsh/guards.rs index 2b60c0c..35482c9 100644 --- a/src/libsh/guards.rs +++ b/src/libsh/guards.rs @@ -144,7 +144,8 @@ impl RawModeGuard { F: FnOnce() -> R, { let current = tcgetattr(borrow_fd(*TTY_FILENO)).expect("Failed to get terminal attributes"); - let orig = ORIG_TERMIOS.with(|cell| cell.borrow().clone()) + let orig = ORIG_TERMIOS + .with(|cell| cell.borrow().clone()) .expect("with_cooked_mode called before raw_mode()"); tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig) .expect("Failed to restore cooked mode"); diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index 9b12fe6..345cceb 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -25,8 +25,8 @@ pub trait TkVecUtils { } pub trait AutoCmdVecUtils { - fn exec(&self); - fn exec_with(&self, pattern: &str); + fn exec(&self); + fn exec_with(&self, pattern: &str); } pub trait RedirVecUtils { @@ -37,36 +37,44 @@ pub trait RedirVecUtils { } pub trait NodeVecUtils { - fn get_span(&self) -> Option; + fn get_span(&self) -> Option; } impl AutoCmdVecUtils for Vec { - fn exec(&self) { - let saved_status = crate::state::get_status(); - for cmd in self { - let AutoCmd { pattern: _, command } = cmd; - if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { - e.print_error(); - } - } - crate::state::set_status(saved_status); - } - fn exec_with(&self, other_pattern: &str) { - let saved_status = crate::state::get_status(); - for cmd in self { - let AutoCmd { pattern, command } = cmd; - if let Some(pat) = pattern - && !pat.is_match(other_pattern) { - log::trace!("autocmd pattern '{}' did not match '{}', skipping", pat, other_pattern); - continue; - } + fn exec(&self) { + let saved_status = crate::state::get_status(); + for cmd in self { + let AutoCmd { + pattern: _, + command, + } = cmd; + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { + e.print_error(); + } + } + crate::state::set_status(saved_status); + } + fn exec_with(&self, other_pattern: &str) { + let saved_status = crate::state::get_status(); + for cmd in self { + let AutoCmd { pattern, command } = cmd; + if let Some(pat) = pattern + && !pat.is_match(other_pattern) + { + log::trace!( + "autocmd pattern '{}' did not match '{}', skipping", + pat, + other_pattern + ); + continue; + } - if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { - e.print_error(); - } - } - crate::state::set_status(saved_status); - } + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { + e.print_error(); + } + } + crate::state::set_status(saved_status); + } } impl VecDequeExt for VecDeque { @@ -118,9 +126,12 @@ impl CharDequeUtils for VecDeque { impl TkVecUtils for Vec { fn get_span(&self) -> Option { if let Some(first_tk) = self.first() { - self - .last() - .map(|last_tk| Span::new(first_tk.span.range().start..last_tk.span.range().end, first_tk.source())) + self.last().map(|last_tk| { + Span::new( + first_tk.span.range().start..last_tk.span.range().end, + first_tk.source(), + ) + }) } else { None } @@ -170,13 +181,17 @@ impl RedirVecUtils for Vec { impl NodeVecUtils for Vec { fn get_span(&self) -> Option { if let Some(first_nd) = self.first() - && let Some(last_nd) = self.last() { - let first_start = first_nd.get_span().range().start; - let last_end = last_nd.get_span().range().end; - if first_start <= last_end { - return Some(Span::new(first_start..last_end, first_nd.get_span().source().content())); - } + && let Some(last_nd) = self.last() + { + let first_start = first_nd.get_span().range().start; + let last_end = last_nd.get_span().range().end; + if first_start <= last_end { + return Some(Span::new( + first_start..last_end, + first_nd.get_span().source().content(), + )); + } } - None + None } } diff --git a/src/main.rs b/src/main.rs index c757006..0b8b206 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ clippy::derivable_impls, clippy::tabs_in_doc_comments, clippy::while_let_on_iterator, - clippy::result_large_err + clippy::result_large_err )] pub mod builtin; pub mod expand; @@ -34,7 +34,6 @@ use crate::libsh::sys::TTY_FILENO; use crate::libsh::utils::AutoCmdVecUtils; use crate::parse::execute::exec_input; use crate::prelude::*; -use crate::procio::IoMode; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; @@ -86,20 +85,22 @@ fn setup_panic_handler() { } }); - let data_dir = env::var("XDG_DATA_HOME").unwrap_or_else(|_| { - let home = env::var("HOME").unwrap(); - format!("{home}/.local/share") - }); - let log_dir = Path::new(&data_dir).join("shed").join("log"); - std::fs::create_dir_all(&log_dir).unwrap(); - let log_file_path = log_dir.join("panic.log"); - let mut log_file = parse::get_redir_file(parse::RedirType::Output, log_file_path).unwrap(); + let data_dir = env::var("XDG_DATA_HOME").unwrap_or_else(|_| { + let home = env::var("HOME").unwrap(); + format!("{home}/.local/share") + }); + let log_dir = Path::new(&data_dir).join("shed").join("log"); + std::fs::create_dir_all(&log_dir).unwrap(); + let log_file_path = log_dir.join("panic.log"); + let mut log_file = parse::get_redir_file(parse::RedirType::Output, log_file_path).unwrap(); - let panic_info_raw = info.to_string(); - log_file.write_all(panic_info_raw.as_bytes()).unwrap(); + let panic_info_raw = info.to_string(); + log_file.write_all(panic_info_raw.as_bytes()).unwrap(); - let backtrace = std::backtrace::Backtrace::force_capture(); - log_file.write_all(format!("\nBacktrace:\n{:?}", backtrace).as_bytes()).unwrap(); + let backtrace = std::backtrace::Backtrace::force_capture(); + log_file + .write_all(format!("\nBacktrace:\n{:?}", backtrace).as_bytes()) + .unwrap(); default_panic_hook(info); })); @@ -134,16 +135,17 @@ fn main() -> ExitCode { } else { shed_interactive(args) } { - e.print_error(); + e.print_error(); }; if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) - && let Err(e) = exec_input(trap, None, false, Some("trap".into())) { - e.print_error(); + && let Err(e) = exec_input(trap, None, false, Some("trap".into())) + { + e.print_error(); } - let on_exit_autocmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnExit)); - on_exit_autocmds.exec(); + let on_exit_autocmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnExit)); + on_exit_autocmds.exec(); write_jobs(|j| j.hang_up()); ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) @@ -151,7 +153,7 @@ fn main() -> ExitCode { fn run_script>(path: P, args: Vec) -> ShResult<()> { let path = path.as_ref(); - let path_raw = path.to_string_lossy().to_string(); + let path_raw = path.to_string_lossy().to_string(); if !path.is_file() { eprintln!("shed: Failed to open input file: {}", path.display()); QUIT_CODE.store(1, Ordering::SeqCst); @@ -207,7 +209,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { m.try_rehash_commands(); m.try_rehash_cwd_listing(); }); - error::clear_color(); + error::clear_color(); // Handle any pending signals while signals_pending() { @@ -267,15 +269,26 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { // Timeout — resolve pending keymap ambiguity if !readline.pending_keymap.is_empty() - && fds[0].revents().is_none_or(|r| !r.contains(PollFlags::POLLIN)) + && fds[0] + .revents() + .is_none_or(|r| !r.contains(PollFlags::POLLIN)) { - log::debug!("[keymap timeout] resolving pending={:?}", readline.pending_keymap); + log::debug!( + "[keymap timeout] resolving pending={:?}", + readline.pending_keymap + ); let keymap_flags = readline.curr_keymap_flags(); let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &readline.pending_keymap)); // If there's an exact match, fire it; otherwise flush as normal keys - let exact = matches.iter().find(|km| km.compare(&readline.pending_keymap) == KeyMapMatch::IsExact); + let exact = matches + .iter() + .find(|km| km.compare(&readline.pending_keymap) == KeyMapMatch::IsExact); if let Some(km) = exact { - log::debug!("[keymap timeout] firing exact match: {:?} -> {:?}", km.keys, km.action); + log::debug!( + "[keymap timeout] firing exact match: {:?} -> {:?}", + km.keys, + km.action + ); let action = km.action_expanded(); readline.pending_keymap.clear(); for key in action { @@ -284,7 +297,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { ReadlineEvent::Line(input) => { let start = Instant::now(); write_meta(|m| m.start_timer()); - if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("".into()))) { + if let Err(e) = RawModeGuard::with_cooked_mode(|| { + exec_input(input, None, true, Some("".into())) + }) { match e.kind() { ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); @@ -310,7 +325,10 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { } } } else { - log::debug!("[keymap timeout] no exact match, flushing {} keys as normal input", readline.pending_keymap.len()); + log::debug!( + "[keymap timeout] no exact match, flushing {} keys as normal input", + readline.pending_keymap.len() + ); let buffered = std::mem::take(&mut readline.pending_keymap); for key in buffered { if let Some(event) = readline.handle_key(key)? { @@ -318,7 +336,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { ReadlineEvent::Line(input) => { let start = Instant::now(); write_meta(|m| m.start_timer()); - if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("".into()))) { + if let Err(e) = RawModeGuard::with_cooked_mode(|| { + exec_input(input, None, true, Some("".into())) + }) { match e.kind() { ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); @@ -376,14 +396,16 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { // Process any available input match readline.process_input() { Ok(ReadlineEvent::Line(input)) => { - let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)); - let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)); + let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)); + let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)); - pre_exec.exec_with(&input); + pre_exec.exec_with(&input); let start = Instant::now(); write_meta(|m| m.start_timer()); - if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input.clone(), None, true, Some("".into()))) { + if let Err(e) = RawModeGuard::with_cooked_mode(|| { + exec_input(input.clone(), None, true, Some("".into())) + }) { match e.kind() { ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); @@ -396,10 +418,10 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { log::info!("Command executed in {:.2?}", command_run_time); write_meta(|m| m.stop_timer()); - post_exec.exec_with(&input); + post_exec.exec_with(&input); - readline.fix_column()?; - readline.writer.flush_write("\n\r")?; + readline.fix_column()?; + readline.writer.flush_write("\n\r")?; // Reset for next command with fresh prompt readline.reset(true)?; diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 1647558..5ea7dee 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,15 +1,37 @@ use std::{ - cell::Cell, collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt + cell::Cell, + collections::{HashSet, VecDeque}, + os::unix::fs::PermissionsExt, }; - use ariadne::Fmt; use crate::{ builtin::{ - 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, 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 + 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, + 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::{Expander, expand_aliases, expand_case_pattern, expand_raw, glob_to_regex}, + expand::{expand_aliases, expand_case_pattern, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, libsh::{ error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, @@ -19,7 +41,7 @@ use crate::{ prelude::*, procio::{IoMode, IoStack}, state::{ - self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars + self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars, }, }; @@ -110,7 +132,12 @@ impl ExecArgs { } } -pub fn exec_input(input: String, io_stack: Option, interactive: bool, source_name: Option) -> ShResult<()> { +pub fn exec_input( + input: String, + io_stack: Option, + interactive: bool, + source_name: Option, +) -> ShResult<()> { let log_tab = read_logic(|l| l.clone()); let input = expand_aliases(input, HashSet::new(), &log_tab); let lex_flags = if interactive { @@ -118,8 +145,10 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool, s } else { super::lex::LexFlags::empty() }; - let source_name = source_name.unwrap_or("".into()); - let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags).with_name(source_name.clone()); + let source_name = source_name.unwrap_or("".into()); + let mut parser = ParsedSrc::new(Arc::new(input)) + .with_lex_flags(lex_flags) + .with_name(source_name.clone()); if let Err(errors) = parser.parse_src() { for error in errors { error.print_error(); @@ -149,7 +178,7 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool, s pub struct Dispatcher { nodes: VecDeque, interactive: bool, - source_name: String, + source_name: String, pub io_stack: IoStack, pub job_stack: JobStack, fg_job: bool, @@ -161,7 +190,7 @@ impl Dispatcher { Self { nodes, interactive, - source_name, + source_name, io_stack: IoStack::new(), job_stack: JobStack::new(), fg_job: true, @@ -208,7 +237,12 @@ impl Dispatcher { let stack = IoStack { stack: self.io_stack.clone(), }; - exec_input(format!("cd {dir}"), Some(stack), self.interactive, Some(self.source_name.clone())) + exec_input( + format!("cd {dir}"), + Some(stack), + self.interactive, + Some(self.source_name.clone()), + ) } else { self.exec_cmd(node) } @@ -274,16 +308,16 @@ impl Dispatcher { return Ok(()); } - let func = ShFunc::new(func_parser,blame); + let func = ShFunc::new(func_parser, blame); write_logic(|l| l.insert_func(name, func)); // Store the AST Ok(()) } fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { - let blame = subsh.get_span().clone(); + let blame = subsh.get_span().clone(); let NdRule::Command { assignments, argv } = subsh.class else { unreachable!() }; - let name = self.source_name.clone(); + let name = self.source_name.clone(); self.run_fork("anonymous_subshell", |s| { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { @@ -309,8 +343,11 @@ impl Dispatcher { } fn exec_func(&mut self, func: Node) -> ShResult<()> { let mut blame = func.get_span().clone(); - let func_name = func.get_command().unwrap().to_string(); - let func_ctx = func.get_context(format!("in call to function '{}'",func_name.fg(next_color()))); + let func_name = func.get_command().unwrap().to_string(); + let func_ctx = func.get_context(format!( + "in call to function '{}'", + func_name.fg(next_color()) + )); let NdRule::Command { assignments, mut argv, @@ -340,12 +377,12 @@ impl Dispatcher { self.io_stack.append_to_frame(func.redirs); - blame.rename(func_name.clone()); + blame.rename(func_name.clone()); let argv = prepare_argv(argv).try_blame(blame.clone())?; let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let _guard = scope_guard(Some(argv)); - func_body.body_mut().propagate_context(func_ctx); + func_body.body_mut().propagate_context(func_ctx); func_body.body_mut().flags = func.flags; if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { @@ -354,7 +391,7 @@ impl Dispatcher { state::set_status(*code); Ok(()) } - _ => Err(e) + _ => Err(e), } } else { Ok(()) @@ -423,12 +460,17 @@ impl Dispatcher { 'outer: for block in case_blocks { let CaseNode { pattern, body } = block; - let block_pattern_raw = pattern.span.as_str().strip_suffix(')').unwrap_or(pattern.span.as_str()).trim(); + let block_pattern_raw = pattern + .span + .as_str() + .strip_suffix(')') + .unwrap_or(pattern.span.as_str()) + .trim(); // Split at '|' to allow for multiple patterns like `foo|bar)` let block_patterns = block_pattern_raw.split('|'); for pattern in block_patterns { - let pattern_exp = expand_case_pattern(pattern)?; + let pattern_exp = expand_case_pattern(pattern)?; let pattern_regex = glob_to_regex(&pattern_exp, false); if pattern_regex.is_match(&pattern_raw) { for node in &body { @@ -450,7 +492,9 @@ impl Dispatcher { } }) } else { - case_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) + case_logic(self) + .try_blame(blame) + .map_err(|e| e.with_redirs(guard)) } } fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { @@ -513,7 +557,9 @@ impl Dispatcher { } }) } else { - loop_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) + loop_logic(self) + .try_blame(blame) + .map_err(|e| e.with_redirs(guard)) } } fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { @@ -591,7 +637,9 @@ impl Dispatcher { } }) } else { - for_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) + for_logic(self) + .try_blame(blame) + .map_err(|e| e.with_redirs(guard)) } } fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { @@ -648,7 +696,9 @@ impl Dispatcher { } }) } else { - if_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) + if_logic(self) + .try_blame(blame) + .map_err(|e| e.with_redirs(guard)) } } fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { @@ -692,11 +742,14 @@ impl Dispatcher { // SIGTTOU when they try to modify terminal attributes. // Only for interactive (top-level) pipelines — command substitution // and other non-interactive contexts must not steal the terminal. - if !tty_attached && !is_bg && self.interactive - && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() { - attach_tty(pgid).ok(); - tty_attached = true; - } + if !tty_attached + && !is_bg + && self.interactive + && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() + { + attach_tty(pgid).ok(); + tty_attached = true; + } } let job = self.job_stack.finalize_job().unwrap(); dispatch_job(job, is_bg, self.interactive)?; @@ -732,7 +785,7 @@ impl Dispatcher { } fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> { let cmd_raw = cmd.get_command().unwrap().to_string(); - let context = cmd.context.clone(); + let context = cmd.context.clone(); let NdRule::Command { assignments, argv } = &mut cmd.class else { unreachable!() }; @@ -815,18 +868,18 @@ impl Dispatcher { "unset" => unset(cmd), "complete" => complete_builtin(cmd), "compgen" => compgen_builtin(cmd), - "map" => map::map(cmd), - "pop" => arr_pop(cmd), - "fpop" => arr_fpop(cmd), - "push" => arr_push(cmd), - "fpush" => arr_fpush(cmd), - "rotate" => arr_rotate(cmd), - "wait" => jobctl::wait(cmd), - "type" => intro::type_builtin(cmd), - "getopts" => getopts(cmd), - "keymap" => keymap::keymap(cmd), - "read_key" => read::read_key(cmd), - "autocmd" => autocmd(cmd), + "map" => map::map(cmd), + "pop" => arr_pop(cmd), + "fpop" => arr_fpop(cmd), + "push" => arr_push(cmd), + "fpush" => arr_fpush(cmd), + "rotate" => arr_rotate(cmd), + "wait" => jobctl::wait(cmd), + "type" => intro::type_builtin(cmd), + "getopts" => getopts(cmd), + "keymap" => keymap::keymap(cmd), + "read_key" => read::read_key(cmd), + "autocmd" => autocmd(cmd), "true" | ":" => { state::set_status(0); Ok(()) @@ -838,17 +891,17 @@ impl Dispatcher { _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw), }; - if let Err(e) = result { - if !e.is_flow_control() { - state::set_status(1); - } - Err(e.with_context(context).with_redirs(redir_guard)) - } else { - Ok(()) - } + if let Err(e) = result { + if !e.is_flow_control() { + state::set_status(1); + } + Err(e.with_context(context).with_redirs(redir_guard)) + } else { + Ok(()) + } } fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { - let blame = cmd.get_span().clone(); + let blame = cmd.get_span().clone(); let context = cmd.context.clone(); let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() @@ -887,7 +940,7 @@ impl Dispatcher { // For foreground jobs, take the terminal BEFORE resetting // signals. SIGTTOU is still SIG_IGN (inherited from the shell), - // so tcsetpgrp won't stop us. This prevents a race + // so tcsetpgrp won't stop us. This prevents a race // where the child exec's and tries to read stdin before the // parent has called tcsetpgrp — which would deliver SIGTTIN // (now SIG_DFL after reset_signals) and stop the child. @@ -918,14 +971,14 @@ impl Dispatcher { match e { Errno::ENOENT => { ShErr::new(ShErrKind::NotFound, span.clone()) - .labeled(span, format!("{cmd_str}: command not found")) - .with_context(context) - .print_error(); + .labeled(span, format!("{cmd_str}: command not found")) + .with_context(context) + .print_error(); } _ => { ShErr::at(ShErrKind::Errno(e), span, format!("{e}")) - .with_context(context) - .print_error(); + .with_context(context) + .print_error(); } } exit(e as i32) diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 35e9718..0b54a47 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -25,98 +25,101 @@ pub const KEYWORDS: [&str; 16] = [ pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"]; /// Used to track whether the lexer is currently inside a quote, and if so, which type -#[derive(Default,Debug)] +#[derive(Default, Debug)] pub enum QuoteState { - #[default] - Outside, - Single, - Double + #[default] + Outside, + Single, + Double, } impl QuoteState { - pub fn outside(&self) -> bool { - matches!(self, QuoteState::Outside) - } - pub fn in_single(&self) -> bool { - matches!(self, QuoteState::Single) - } - pub fn in_double(&self) -> bool { - matches!(self, QuoteState::Double) - } - pub fn in_quote(&self) -> bool { - !self.outside() - } - /// Toggles whether we are in a double quote. If self = QuoteState::Single, this does nothing, since double quotes inside single quotes are just literal characters - pub fn toggle_double(&mut self) { - match self { - QuoteState::Outside => *self = QuoteState::Double, - QuoteState::Double => *self = QuoteState::Outside, - _ => {} - } - } - /// Toggles whether we are in a single quote. If self == QuoteState::Double, this does nothing, since single quotes are not interpreted inside double quotes - pub fn toggle_single(&mut self) { - match self { - QuoteState::Outside => *self = QuoteState::Single, - QuoteState::Single => *self = QuoteState::Outside, - _ => {} - } - } + pub fn outside(&self) -> bool { + matches!(self, QuoteState::Outside) + } + pub fn in_single(&self) -> bool { + matches!(self, QuoteState::Single) + } + pub fn in_double(&self) -> bool { + matches!(self, QuoteState::Double) + } + pub fn in_quote(&self) -> bool { + !self.outside() + } + /// Toggles whether we are in a double quote. If self = QuoteState::Single, this does nothing, since double quotes inside single quotes are just literal characters + pub fn toggle_double(&mut self) { + match self { + QuoteState::Outside => *self = QuoteState::Double, + QuoteState::Double => *self = QuoteState::Outside, + _ => {} + } + } + /// Toggles whether we are in a single quote. If self == QuoteState::Double, this does nothing, since single quotes are not interpreted inside double quotes + pub fn toggle_single(&mut self) { + match self { + QuoteState::Outside => *self = QuoteState::Single, + QuoteState::Single => *self = QuoteState::Outside, + _ => {} + } + } } #[derive(Clone, PartialEq, Default, Debug, Eq, Hash)] pub struct SpanSource { - name: String, - content: Arc + name: String, + content: Arc, } impl SpanSource { - pub fn name(&self) -> &str { - &self.name - } - pub fn content(&self) -> Arc { - self.content.clone() - } - pub fn rename(&mut self, name: String) { - self.name = name; - } + pub fn name(&self) -> &str { + &self.name + } + pub fn content(&self) -> Arc { + self.content.clone() + } + pub fn rename(&mut self, name: String) { + self.name = name; + } } impl Display for SpanSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } } /// Span::new(10..20) #[derive(Clone, PartialEq, Default, Debug)] pub struct Span { range: Range, - source: SpanSource + source: SpanSource, } impl Span { /// New `Span`. Wraps a range and a string slice that it refers to. pub fn new(range: Range, source: Arc) -> Self { - let source = SpanSource { name: "".into(), content: source }; + let source = SpanSource { + name: "".into(), + content: source, + }; Span { range, source } } pub fn from_span_source(range: Range, source: SpanSource) -> Self { Span { range, source } } - pub fn rename(&mut self, name: String) { - self.source.name = name; - } - pub fn with_name(mut self, name: String) -> Self { - self.source.name = name; - self - } - pub fn line_and_col(&self) -> (usize,usize) { - let content = self.source.content(); - let source = ariadne::Source::from(content.as_str()); - let (_, line, col) = source.get_byte_line(self.range.start).unwrap(); - (line, col) - } + pub fn rename(&mut self, name: String) { + self.source.name = name; + } + pub fn with_name(mut self, name: String) -> Self { + self.source.name = name; + self + } + pub fn line_and_col(&self) -> (usize, usize) { + let content = self.source.content(); + let source = ariadne::Source::from(content.as_str()); + let (_, line, col) = source.get_byte_line(self.range.start).unwrap(); + (line, col) + } /// Slice the source string at the wrapped range pub fn as_str(&self) -> &str { &self.source.content[self.range().start..self.range().end] @@ -138,19 +141,19 @@ impl Span { } impl ariadne::Span for Span { - type SourceId = SpanSource; + type SourceId = SpanSource; - fn source(&self) -> &Self::SourceId { - &self.source - } + fn source(&self) -> &Self::SourceId { + &self.source + } - fn start(&self) -> usize { - self.range.start - } + fn start(&self) -> usize { + self.range.start + } - fn end(&self) -> usize { - self.range.end - } + fn end(&self) -> usize { + self.range.end + } } /// Allows simple access to the underlying range wrapped by the span @@ -243,7 +246,7 @@ bitflags! { pub struct LexStream { source: Arc, pub cursor: usize, - pub name: String, + pub name: String, quote_state: QuoteState, brc_grp_depth: usize, brc_grp_start: Option, @@ -273,23 +276,23 @@ 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 + 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 { @@ -298,7 +301,7 @@ impl LexStream { Self { flags, source, - name: "".into(), + name: "".into(), cursor: 0, quote_state: QuoteState::default(), brc_grp_depth: 0, @@ -327,10 +330,10 @@ impl LexStream { }; self.source.get(start..end) } - pub fn with_name(mut self, name: String) -> Self { - self.name = name; - self - } + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } pub fn slice_from_cursor(&self) -> Option<&str> { self.slice(self.cursor..) } @@ -475,11 +478,11 @@ impl LexStream { pos += ch.len_utf8(); } } - '\'' => { - pos += 1; - self.quote_state.toggle_single(); - } - _ if self.quote_state.in_single() => pos += ch.len_utf8(), + '\'' => { + pos += 1; + self.quote_state.toggle_single(); + } + _ if self.quote_state.in_single() => pos += ch.len_utf8(), '$' if chars.peek() == Some(&'(') => { pos += 2; chars.next(); @@ -543,11 +546,11 @@ impl LexStream { } } } - '"' => { - pos += 1; - self.quote_state.toggle_double(); - } - _ if self.quote_state.in_double() => pos += ch.len_utf8(), + '"' => { + pos += 1; + self.quote_state.toggle_double(); + } + _ if self.quote_state.in_double() => pos += ch.len_utf8(), '<' if chars.peek() == Some(&'(') => { pos += 2; chars.next(); @@ -770,7 +773,7 @@ impl LexStream { } pub fn get_token(&self, range: Range, class: TkRule) -> Tk { let mut span = Span::new(range, self.source.clone()); - span.rename(self.name.clone()); + span.rename(self.name.clone()); Tk::new(class, span) } } @@ -845,15 +848,15 @@ impl Iterator for LexStream { self.set_next_is_cmd(true); while let Some(ch) = get_char(&self.source, self.cursor) { - match ch { - '\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => { - self.cursor = (self.cursor + 2).min(self.source.len()); - } - _ if is_hard_sep(ch) => { - self.cursor += 1; - } - _ => break, - } + match ch { + '\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => { + self.cursor = (self.cursor + 2).min(self.source.len()); + } + _ if is_hard_sep(ch) => { + self.cursor += 1; + } + _ => break, + } } self.get_token(ch_idx..self.cursor, TkRule::Sep) } @@ -974,84 +977,101 @@ pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool { /// Splits a string by a pattern, but only if the pattern is not escaped by a backslash /// and not in quotes. pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec { - let mut cursor = 0; - let mut splits = vec![]; - while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { - cursor += split.0.len() + pat.len(); - splits.push(split.0); - } - if let Some(remaining) = slice.get(cursor..) { - splits.push(remaining.to_string()); - } - splits + let mut cursor = 0; + let mut splits = vec![]; + while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { + cursor += split.0.len() + pat.len(); + splits.push(split.0); + } + if let Some(remaining) = slice.get(cursor..) { + splits.push(remaining.to_string()); + } + splits } /// Splits a string at the first occurrence of a pattern, but only if the pattern is not escaped by a backslash /// and not in quotes. Returns None if the pattern is not found or only found escaped. -pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> { - let mut chars = slice.char_indices().peekable(); - let mut qt_state = QuoteState::default(); +pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String, String)> { + let mut chars = slice.char_indices().peekable(); + let mut qt_state = QuoteState::default(); - while let Some((i, ch)) = chars.next() { - match ch { - '\\' => { chars.next(); continue; } - '\'' => qt_state.toggle_single(), - '"' => qt_state.toggle_double(), - _ if qt_state.in_quote() => continue, - _ => {} - } + while let Some((i, ch)) = chars.next() { + match ch { + '\\' => { + chars.next(); + continue; + } + '\'' => qt_state.toggle_single(), + '"' => qt_state.toggle_double(), + _ if qt_state.in_quote() => continue, + _ => {} + } - if slice[i..].starts_with(pat) { - let before = slice[..i].to_string(); - let after = slice[i + pat.len()..].to_string(); - return Some((before, after)); - } - } + if slice[i..].starts_with(pat) { + let before = slice[..i].to_string(); + let after = slice[i + pat.len()..].to_string(); + return Some((before, after)); + } + } - - None + None } pub fn split_tk(tk: &Tk, pat: &str) -> Vec { - let slice = tk.as_str(); - let mut cursor = 0; - let mut splits = vec![]; - while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { - let before_span = Span::new(tk.span.range().start + cursor..tk.span.range().start + cursor + split.0.len(), tk.source().clone()); - splits.push(Tk::new(tk.class.clone(), before_span)); - cursor += split.0.len() + pat.len(); - } - if slice.get(cursor..).is_some_and(|s| !s.is_empty()) { - let remaining_span = Span::new(tk.span.range().start + cursor..tk.span.range().end, tk.source().clone()); - splits.push(Tk::new(tk.class.clone(), remaining_span)); - } - splits + let slice = tk.as_str(); + let mut cursor = 0; + let mut splits = vec![]; + while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { + let before_span = Span::new( + tk.span.range().start + cursor..tk.span.range().start + cursor + split.0.len(), + tk.source().clone(), + ); + splits.push(Tk::new(tk.class.clone(), before_span)); + cursor += split.0.len() + pat.len(); + } + if slice.get(cursor..).is_some_and(|s| !s.is_empty()) { + let remaining_span = Span::new( + tk.span.range().start + cursor..tk.span.range().end, + tk.source().clone(), + ); + splits.push(Tk::new(tk.class.clone(), remaining_span)); + } + splits } pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> { - let slice = tk.as_str(); - let mut chars = slice.char_indices().peekable(); - let mut qt_state = QuoteState::default(); + let slice = tk.as_str(); + let mut chars = slice.char_indices().peekable(); + let mut qt_state = QuoteState::default(); - while let Some((i, ch)) = chars.next() { - match ch { - '\\' => { chars.next(); continue; } - '\'' => qt_state.toggle_single(), - '"' => qt_state.toggle_double(), - _ if qt_state.in_quote() => continue, - _ => {} - } + while let Some((i, ch)) = chars.next() { + match ch { + '\\' => { + chars.next(); + continue; + } + '\'' => qt_state.toggle_single(), + '"' => qt_state.toggle_double(), + _ if qt_state.in_quote() => continue, + _ => {} + } - if slice[i..].starts_with(pat) { - let before_span = Span::new(tk.span.range().start..tk.span.range().start + i, tk.source().clone()); - let after_span = Span::new(tk.span.range().start + i + pat.len()..tk.span.range().end, tk.source().clone()); - let before_tk = Tk::new(tk.class.clone(), before_span); - let after_tk = Tk::new(tk.class.clone(), after_span); - return Some((before_tk, after_tk)); - } - } + if slice[i..].starts_with(pat) { + let before_span = Span::new( + tk.span.range().start..tk.span.range().start + i, + tk.source().clone(), + ); + let after_span = Span::new( + tk.span.range().start + i + pat.len()..tk.span.range().end, + tk.source().clone(), + ); + let before_tk = Tk::new(tk.class.clone(), before_span); + let after_tk = Tk::new(tk.class.clone(), after_span); + return Some((before_tk, after_tk)); + } + } - None + None } pub fn pos_is_escaped(slice: &str, pos: usize) -> bool { @@ -1083,7 +1103,7 @@ pub fn lookahead(pat: &str, mut chars: Chars) -> Option { pub fn case_pat_lookahead(mut chars: Peekable) -> Option { let mut pos = 0; - let mut qt_state = QuoteState::default(); + let mut qt_state = QuoteState::default(); while let Some(ch) = chars.next() { pos += ch.len_utf8(); match ch { @@ -1108,12 +1128,12 @@ pub fn case_pat_lookahead(mut chars: Peekable) -> Option { } } } - '\'' => { - qt_state.toggle_single(); - } - '"' => { - qt_state.toggle_double(); - } + '\'' => { + qt_state.toggle_single(); + } + '"' => { + qt_state.toggle_double(); + } ')' if qt_state.outside() => return Some(pos), '(' if qt_state.outside() => return None, _ => { /* continue */ } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 8231304..05d9b29 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -9,7 +9,10 @@ use crate::{ libsh::{ error::{ShErr, ShErrKind, ShResult, last_color, next_color}, utils::{NodeVecUtils, TkVecUtils}, - }, parse::lex::clean_input, prelude::*, procio::IoMode + }, + parse::lex::clean_input, + prelude::*, + procio::IoMode, }; pub mod execute; @@ -42,7 +45,7 @@ macro_rules! try_match { #[derive(Clone, Debug)] pub struct ParsedSrc { pub src: Arc, - pub name: String, + pub name: String, pub ast: Ast, pub lex_flags: LexFlags, pub context: LabelCtx, @@ -57,16 +60,16 @@ impl ParsedSrc { }; Self { src, - name: "".into(), + name: "".into(), ast: Ast::new(vec![]), lex_flags: LexFlags::empty(), context: VecDeque::new(), } } - pub fn with_name(mut self, name: String) -> Self { - self.name = name; - self - } + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } pub fn with_lex_flags(mut self, flags: LexFlags) -> Self { self.lex_flags = flags; self @@ -77,7 +80,8 @@ impl ParsedSrc { } pub fn parse_src(&mut self) -> Result<(), Vec> { let mut tokens = vec![]; - for lex_result in LexStream::new(self.src.clone(), self.lex_flags).with_name(self.name.clone()) { + for lex_result in LexStream::new(self.src.clone(), self.lex_flags).with_name(self.name.clone()) + { match lex_result { Ok(token) => tokens.push(token), Err(error) => return Err(vec![error]), @@ -128,7 +132,7 @@ pub struct Node { pub flags: NdFlags, pub redirs: Vec, pub tokens: Vec, - pub context: LabelCtx, + pub context: LabelCtx, } impl Node { @@ -143,108 +147,108 @@ impl Node { None } } - pub fn get_context(&self, msg: String) -> (SpanSource, Label) { - let color = last_color(); - let span = self.get_span().clone(); - ( - span.clone().source().clone(), - Label::new(span).with_color(color).with_message(msg) - ) - } - fn walk_tree(&mut self, f: &F) { - f(self); + pub fn get_context(&self, msg: String) -> (SpanSource, Label) { + let color = last_color(); + let span = self.get_span().clone(); + ( + span.clone().source().clone(), + Label::new(span).with_color(color).with_message(msg), + ) + } + fn walk_tree(&mut self, f: &F) { + f(self); - match self.class { - NdRule::IfNode { - ref mut cond_nodes, - ref mut else_block, - } => { - for node in cond_nodes { - let CondNode { cond, body } = node; - cond.walk_tree(f); - for body_node in body { - body_node.walk_tree(f); - } - } + match self.class { + NdRule::IfNode { + ref mut cond_nodes, + ref mut else_block, + } => { + for node in cond_nodes { + let CondNode { cond, body } = node; + cond.walk_tree(f); + for body_node in body { + body_node.walk_tree(f); + } + } - for else_node in else_block { - else_node.walk_tree(f); - } - } - NdRule::LoopNode { - kind: _, - ref mut cond_node, - } => { - let CondNode { cond, body } = cond_node; - cond.walk_tree(f); - for body_node in body { - body_node.walk_tree(f); - } - } - NdRule::ForNode { - vars: _, - arr: _, - ref mut body, - } => { - for body_node in body { - body_node.walk_tree(f); - } - } - NdRule::CaseNode { - pattern: _, - ref mut case_blocks, - } => { - for block in case_blocks { - let CaseNode { pattern: _, body } = block; - for body_node in body { - body_node.walk_tree(f); - } - } - } - NdRule::Command { - ref mut assignments, - argv: _, - } => { - for assign_node in assignments { - assign_node.walk_tree(f); - } - } - NdRule::Pipeline { - ref mut cmds, - pipe_err: _, - } => { - for cmd_node in cmds { - cmd_node.walk_tree(f); - } - } - NdRule::Conjunction { ref mut elements } => { - for node in elements.iter_mut() { - let ConjunctNode { cmd, operator: _ } = node; - cmd.walk_tree(f); - } - } - NdRule::Assignment { - kind: _, - var: _, - val: _, - } => (), // No nodes to check - NdRule::BraceGrp { ref mut body } => { - for body_node in body { - body_node.walk_tree(f); - } - } - NdRule::FuncDef { - name: _, - ref mut body, - } => { - body.walk_tree(f); - } - NdRule::Test { cases: _ } => (), - } - } - pub fn propagate_context(&mut self, ctx: (SpanSource, Label)) { - self.walk_tree(&|nd| nd.context.push_back(ctx.clone())); - } + for else_node in else_block { + else_node.walk_tree(f); + } + } + NdRule::LoopNode { + kind: _, + ref mut cond_node, + } => { + let CondNode { cond, body } = cond_node; + cond.walk_tree(f); + for body_node in body { + body_node.walk_tree(f); + } + } + NdRule::ForNode { + vars: _, + arr: _, + ref mut body, + } => { + for body_node in body { + body_node.walk_tree(f); + } + } + NdRule::CaseNode { + pattern: _, + ref mut case_blocks, + } => { + for block in case_blocks { + let CaseNode { pattern: _, body } = block; + for body_node in body { + body_node.walk_tree(f); + } + } + } + NdRule::Command { + ref mut assignments, + argv: _, + } => { + for assign_node in assignments { + assign_node.walk_tree(f); + } + } + NdRule::Pipeline { + ref mut cmds, + pipe_err: _, + } => { + for cmd_node in cmds { + cmd_node.walk_tree(f); + } + } + NdRule::Conjunction { ref mut elements } => { + for node in elements.iter_mut() { + let ConjunctNode { cmd, operator: _ } = node; + cmd.walk_tree(f); + } + } + NdRule::Assignment { + kind: _, + var: _, + val: _, + } => (), // No nodes to check + NdRule::BraceGrp { ref mut body } => { + for body_node in body { + body_node.walk_tree(f); + } + } + NdRule::FuncDef { + name: _, + ref mut body, + } => { + body.walk_tree(f); + } + NdRule::Test { cases: _ } => (), + } + } + pub fn propagate_context(&mut self, ctx: (SpanSource, Label)) { + self.walk_tree(&|nd| nd.context.push_back(ctx.clone())); + } pub fn get_span(&self) -> Span { let Some(first_tk) = self.tokens.first() else { unreachable!() @@ -662,20 +666,23 @@ pub enum NdRule { pub struct ParseStream { pub tokens: Vec, - pub context: LabelCtx + pub context: LabelCtx, } impl Debug for ParseStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ParseStream") - .field("tokens", &self.tokens) - .finish() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ParseStream") + .field("tokens", &self.tokens) + .finish() + } } impl ParseStream { pub fn new(tokens: Vec) -> Self { - Self { tokens, context: VecDeque::new() } + Self { + tokens, + context: VecDeque::new(), + } } pub fn with_context(tokens: Vec, context: LabelCtx) -> Self { Self { tokens, context } @@ -831,39 +838,42 @@ impl ParseStream { let name_tk = self.next_tk().unwrap(); node_tks.push(name_tk.clone()); let name = name_tk.clone(); - let name_raw = name.to_string(); - let mut src = name_tk.span.span_source().clone(); - src.rename(name_raw.clone()); - let color = next_color(); - // Push a placeholder context so child nodes inherit it - self.context.push_back(( - src.clone(), - Label::new(name_tk.span.clone().with_name(name_raw.clone())) - .with_message(format!("in function '{}' defined here", name_raw.clone().fg(color))) - .with_color(color), - )); + let name_raw = name.to_string(); + let mut src = name_tk.span.span_source().clone(); + src.rename(name_raw.clone()); + let color = next_color(); + // Push a placeholder context so child nodes inherit it + self.context.push_back(( + src.clone(), + Label::new(name_tk.span.clone().with_name(name_raw.clone())) + .with_message(format!( + "in function '{}' defined here", + name_raw.clone().fg(color) + )) + .with_color(color), + )); let Some(brc_grp) = self.parse_brc_grp(true /* from_func_def */)? else { - self.context.pop_back(); + self.context.pop_back(); return Err(parse_err_full( "Expected a brace group after function name", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); }; body = Box::new(brc_grp); - // Replace placeholder with full-span label - self.context.pop_back(); + // Replace placeholder with full-span label + self.context.pop_back(); let node = Node { class: NdRule::FuncDef { name, body }, flags: NdFlags::empty(), redirs: vec![], tokens: node_tks, - context: self.context.clone() + context: self.context.clone(), }; - self.context.pop_back(); + self.context.pop_back(); Ok(Some(node)) } fn panic_mode(&mut self, node_tks: &mut Vec) { @@ -893,7 +903,7 @@ impl ParseStream { return Err(parse_err_full( "Malformed test call", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } else { break; @@ -920,7 +930,7 @@ impl ParseStream { return Err(parse_err_full( "Invalid placement for logical operator in test", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } let op = match tk.class { @@ -936,7 +946,7 @@ impl ParseStream { return Err(parse_err_full( "Invalid placement for logical operator in test", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } } @@ -982,7 +992,7 @@ impl ParseStream { return Err(parse_err_full( "Expected a closing brace for this brace group", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } } @@ -1049,11 +1059,9 @@ impl ParseStream { let pat_err = parse_err_full( "Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), ) - .with_note( - "Patterns can be raw text, or anything that gets substituted with raw text" - ); + .with_note("Patterns can be raw text, or anything that gets substituted with raw text"); let Some(pat_tk) = self.next_tk() else { self.panic_mode(&mut node_tks); @@ -1073,7 +1081,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'in' after case variable name", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1086,7 +1094,7 @@ impl ParseStream { return Err(parse_err_full( "Expected a case pattern here", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } let case_pat_tk = self.next_tk().unwrap(); @@ -1123,7 +1131,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'esac' after case block", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } } @@ -1140,12 +1148,12 @@ impl ParseStream { }; Ok(Some(node)) } - fn make_err(&self, span: lex::Span, label: Label) -> ShErr { - let src = span.span_source().clone(); - ShErr::new(ShErrKind::ParseErr, span) - .with_label(src, label) - .with_context(self.context.clone()) - } + fn make_err(&self, span: lex::Span, label: Label) -> ShErr { + let src = span.span_source().clone(); + ShErr::new(ShErrKind::ParseErr, span) + .with_label(src, label) + .with_context(self.context.clone()) + } fn parse_if(&mut self) -> ShResult> { // Needs at last one 'if-then', // Any number of 'elif-then', @@ -1164,14 +1172,19 @@ impl ParseStream { let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" }; let Some(cond) = self.parse_cmd_list()? else { self.panic_mode(&mut node_tks); - let span = node_tks.get_span().unwrap(); - let color = next_color(); - return Err(self.make_err(span.clone(), - Label::new(span) - .with_message(format!("Expected an expression after '{}'", prefix_keywrd.fg(color))) - .with_color(color) - )); - + let span = node_tks.get_span().unwrap(); + let color = next_color(); + return Err( + self.make_err( + span.clone(), + Label::new(span) + .with_message(format!( + "Expected an expression after '{}'", + prefix_keywrd.fg(color) + )) + .with_color(color), + ), + ); }; node_tks.extend(cond.tokens.clone()); @@ -1180,7 +1193,7 @@ impl ParseStream { return Err(parse_err_full( &format!("Expected 'then' after '{prefix_keywrd}' condition"), &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1196,7 +1209,7 @@ impl ParseStream { return Err(parse_err_full( "Expected an expression after 'then'", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); }; let cond_node = CondNode { @@ -1205,7 +1218,7 @@ impl ParseStream { }; cond_nodes.push(cond_node); - self.catch_separator(&mut node_tks); + self.catch_separator(&mut node_tks); if !self.check_keyword("elif") || !self.next_tk_is_some() { break; } else { @@ -1214,7 +1227,7 @@ impl ParseStream { } } - self.catch_separator(&mut node_tks); + self.catch_separator(&mut node_tks); if self.check_keyword("else") { node_tks.push(self.next_tk().unwrap()); self.catch_separator(&mut node_tks); @@ -1226,7 +1239,7 @@ impl ParseStream { return Err(parse_err_full( "Expected an expression after 'else'", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } } @@ -1237,7 +1250,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'fi' after if statement", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1293,7 +1306,7 @@ impl ParseStream { return Err(parse_err_full( "This for loop is missing a variable", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } if arr.is_empty() { @@ -1301,7 +1314,7 @@ impl ParseStream { return Err(parse_err_full( "This for loop is missing an array", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } if !self.check_keyword("do") || !self.next_tk_is_some() { @@ -1309,7 +1322,7 @@ impl ParseStream { return Err(parse_err_full( "Missing a 'do' for this for loop", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1325,7 +1338,7 @@ impl ParseStream { return Err(parse_err_full( "Missing a 'done' after this for loop", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1366,7 +1379,7 @@ impl ParseStream { return Err(parse_err_full( &format!("Expected an expression after '{loop_kind}'"), // It also implements Display &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); }; node_tks.extend(cond.tokens.clone()); @@ -1376,7 +1389,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'do' after loop condition", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1392,7 +1405,7 @@ impl ParseStream { return Err(parse_err_full( "Expected an expression after 'do'", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); }; @@ -1402,7 +1415,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'done' after loop body", &node_tks.get_span().unwrap(), - self.context.clone() + self.context.clone(), )); } node_tks.push(self.next_tk().unwrap()); @@ -1479,7 +1492,7 @@ impl ParseStream { return Err(parse_err_full( "Found case pattern in command", &prefix_tk.span, - self.context.clone() + self.context.clone(), )); } let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD); @@ -1515,14 +1528,14 @@ impl ParseStream { // If we have assignments but no command word, // return the assignment-only command without parsing more tokens self.commit(node_tks.len()); - let mut context = self.context.clone(); - let assignments_span = assignments.get_span().unwrap(); - context.push_back(( - assignments_span.source().clone(), - Label::new(assignments_span) - .with_message("in variable assignment defined here".to_string()) - .with_color(next_color()) - )); + let mut context = self.context.clone(); + let assignments_span = assignments.get_span().unwrap(); + context.push_back(( + assignments_span.source().clone(), + Label::new(assignments_span) + .with_message("in variable assignment defined here".to_string()) + .with_color(next_color()), + )); return Ok(Some(Node { class: NdRule::Command { assignments, argv }, tokens: node_tks, @@ -1559,7 +1572,7 @@ impl ParseStream { let path_tk = tk_iter.next(); if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) { - self.panic_mode(&mut node_tks); + self.panic_mode(&mut node_tks); return Err(ShErr::at( ShErrKind::ParseErr, tk.span.clone(), @@ -1737,7 +1750,7 @@ fn node_is_punctuated(tokens: &[Tk]) -> bool { } pub fn get_redir_file>(class: RedirType, path: P) -> ShResult { - let path = path.as_ref(); + let path = path.as_ref(); let result = match class { RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)), RedirType::Output => OpenOptions::new() @@ -1745,23 +1758,22 @@ pub fn get_redir_file>(class: RedirType, path: P) -> ShResult OpenOptions::new() - .create(true) - .append(true) - .open(path), + RedirType::Append => OpenOptions::new().create(true).append(true).open(path), _ => unimplemented!(), }; Ok(result?) } fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr { - let color = last_color(); - ShErr::new(ShErrKind::ParseErr, blame.clone()) - .with_label( - blame.span_source().clone(), - Label::new(blame.clone()).with_message(reason).with_color(color) - ) - .with_context(context) + let color = last_color(); + ShErr::new(ShErrKind::ParseErr, blame.clone()) + .with_label( + blame.span_source().clone(), + Label::new(blame.clone()) + .with_message(reason) + .with_color(color), + ) + .with_context(context) } fn is_func_name(tk: Option<&Tk>) -> bool { diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 5a7d23e..36000c0 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -1,44 +1,50 @@ use std::{ - collections::HashSet, fmt::{Write,Debug}, path::PathBuf, sync::Arc, + collections::HashSet, + fmt::{Debug, Write}, + path::PathBuf, + sync::Arc, }; use nix::sys::signal::Signal; use crate::{ builtin::complete::{CompFlags, CompOptFlags, CompOpts}, - libsh::{ - error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils - }, + libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils}, parse::{ execute::exec_input, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, }, readline::{ - Marker, annotate_input_recursive, keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, linebuf::{ClampedUsize, LineBuf}, markers::{self, is_marker}, term::{LineWriter, TermWriter, calc_str_width, get_win_size}, vimode::{ViInsert, ViMode} + Marker, annotate_input_recursive, + keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, + linebuf::{ClampedUsize, LineBuf}, + markers::{self, is_marker}, + term::{LineWriter, TermWriter, calc_str_width, get_win_size}, + vimode::{ViInsert, ViMode}, }, state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars}, }; pub fn complete_signals(start: &str) -> Vec { - Signal::iterator() - .map(|s| { - s.to_string() - .strip_prefix("SIG") - .unwrap_or(s.as_ref()) - .to_string() - }) - .filter(|s| s.starts_with(start)) - .collect() + Signal::iterator() + .map(|s| { + s.to_string() + .strip_prefix("SIG") + .unwrap_or(s.as_ref()) + .to_string() + }) + .filter(|s| s.starts_with(start)) + .collect() } pub fn complete_aliases(start: &str) -> Vec { - read_logic(|l| { - l.aliases() - .iter() - .filter(|a| a.0.starts_with(start)) - .map(|a| a.0.clone()) - .collect() - }) + read_logic(|l| { + l.aliases() + .iter() + .filter(|a| a.0.starts_with(start)) + .map(|a| a.0.clone()) + .collect() + }) } pub fn complete_jobs(start: &str) -> Vec { @@ -224,9 +230,14 @@ fn complete_filename(start: &str) -> Vec { pub enum CompSpecResult { NoSpec, // No compspec registered - NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns + NoMatch { + flags: CompOptFlags, + }, /* Compspec found but no candidates matched, returns * behavior flags */ - Match { result: CompResult, flags: CompOptFlags }, // Compspec found and candidates returned + Match { + result: CompResult, + flags: CompOptFlags, + }, // Compspec found and candidates returned } #[derive(Default, Debug, Clone)] @@ -249,8 +260,8 @@ pub struct BashCompSpec { pub signals: bool, /// -j: complete job pids or names pub jobs: bool, - /// -a: complete aliases - pub aliases: bool, + /// -a: complete aliases + pub aliases: bool, pub flags: CompOptFlags, /// The original command @@ -301,10 +312,10 @@ impl BashCompSpec { self.jobs = enable; self } - pub fn aliases(mut self, enable: bool) -> Self { - self.aliases = enable; - self - } + pub fn aliases(mut self, enable: bool) -> Self { + self.aliases = enable; + self + } pub fn from_comp_opts(opts: CompOpts) -> Self { let CompOpts { func, @@ -322,7 +333,7 @@ impl BashCompSpec { users: flags.contains(CompFlags::USERS), vars: flags.contains(CompFlags::VARS), jobs: flags.contains(CompFlags::JOBS), - aliases: flags.contains(CompFlags::ALIAS), + aliases: flags.contains(CompFlags::ALIAS), flags: opt_flags, signals: false, // TODO: implement signal completion source: String::new(), @@ -419,26 +430,27 @@ impl CompSpec for BashCompSpec { if self.jobs { candidates.extend(complete_jobs(&expanded)); } - if self.aliases { - candidates.extend(complete_aliases(&expanded)); - } - if self.signals { - candidates.extend(complete_signals(&expanded)); - } + if self.aliases { + candidates.extend(complete_aliases(&expanded)); + } + if self.signals { + candidates.extend(complete_signals(&expanded)); + } if let Some(words) = &self.wordlist { candidates.extend(words.iter().filter(|w| w.starts_with(&expanded)).cloned()); } if self.function.is_some() { candidates.extend(self.exec_comp_func(ctx)?); } - candidates = candidates.into_iter() - .map(|c| { - let stripped = c.strip_prefix(&expanded).unwrap_or_default(); - format!("{prefix}{stripped}") - }) - .collect(); + candidates = candidates + .into_iter() + .map(|c| { + let stripped = c.strip_prefix(&expanded).unwrap_or_default(); + format!("{prefix}{stripped}") + }) + .collect(); - candidates.sort_by_key(|c| c.len()); // sort by length to prioritize shorter completions, ties are then sorted alphabetically + candidates.sort_by_key(|c| c.len()); // sort by length to prioritize shorter completions, ties are then sorted alphabetically Ok(candidates) } @@ -510,494 +522,528 @@ impl CompResult { } pub enum CompResponse { - Passthrough, // key falls through - Accept(String), // user accepted completion - Dismiss, // user canceled completion - Consumed // key was handled, but completion remains active + Passthrough, // key falls through + Accept(String), // user accepted completion + Dismiss, // user canceled completion + Consumed, // key was handled, but completion remains active } pub trait Completer { - fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult>; - fn reset(&mut self); - fn reset_stay_active(&mut self); - fn is_active(&self) -> bool; - fn selected_candidate(&self) -> Option; - fn token_span(&self) -> (usize, usize); - fn original_input(&self) -> &str; - fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>; - fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) } - fn set_prompt_line_context(&mut self, _line_width: u16, _cursor_col: u16) {} - fn handle_key(&mut self, key: K) -> ShResult; - fn get_completed_line(&self, candidate: &str) -> String; + fn complete( + &mut self, + line: String, + cursor_pos: usize, + direction: i32, + ) -> ShResult>; + fn reset(&mut self); + fn reset_stay_active(&mut self); + fn is_active(&self) -> bool; + fn selected_candidate(&self) -> Option; + fn token_span(&self) -> (usize, usize); + fn original_input(&self) -> &str; + fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>; + fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { + Ok(()) + } + fn set_prompt_line_context(&mut self, _line_width: u16, _cursor_col: u16) {} + fn handle_key(&mut self, key: K) -> ShResult; + fn get_completed_line(&self, candidate: &str) -> String; } #[derive(Default, Debug, Clone)] pub struct ScoredCandidate { - content: String, - score: Option, + content: String, + score: Option, } impl ScoredCandidate { - const BONUS_BOUNDARY: i32 = 10; - const BONUS_CONSECUTIVE: i32 = 8; - const BONUS_FIRST_CHAR: i32 = 5; - const PENALTY_GAP_START: i32 = 3; - const PENALTY_GAP_EXTEND: i32 = 1; + const BONUS_BOUNDARY: i32 = 10; + const BONUS_CONSECUTIVE: i32 = 8; + const BONUS_FIRST_CHAR: i32 = 5; + const PENALTY_GAP_START: i32 = 3; + const PENALTY_GAP_EXTEND: i32 = 1; - pub fn new(content: String) -> Self { - Self { content, score: None } - } - fn is_word_bound(prev: char, curr: char) -> bool { - match prev { - '/' | '_' | '-' | '.' | ' ' => true, - c if c.is_lowercase() && curr.is_uppercase() => true, // camelCase boundary - _ => false, - } - } - pub fn fuzzy_score(&mut self, other: &str) -> i32 { - if other.is_empty() { - self.score = Some(0); - return 0; - } + pub fn new(content: String) -> Self { + Self { + content, + score: None, + } + } + fn is_word_bound(prev: char, curr: char) -> bool { + match prev { + '/' | '_' | '-' | '.' | ' ' => true, + c if c.is_lowercase() && curr.is_uppercase() => true, // camelCase boundary + _ => false, + } + } + pub fn fuzzy_score(&mut self, other: &str) -> i32 { + if other.is_empty() { + self.score = Some(0); + return 0; + } - let query_chars: Vec = other.chars().collect(); - let content_chars: Vec = self.content.chars().collect(); - let mut indices = vec![]; - let mut qi = 0; - for (ci, c_ch) in self.content.chars().enumerate() { - if qi < query_chars.len() && c_ch.eq_ignore_ascii_case(&query_chars[qi]) { - indices.push(ci); - qi += 1; - } - } + let query_chars: Vec = other.chars().collect(); + let content_chars: Vec = self.content.chars().collect(); + let mut indices = vec![]; + let mut qi = 0; + for (ci, c_ch) in self.content.chars().enumerate() { + if qi < query_chars.len() && c_ch.eq_ignore_ascii_case(&query_chars[qi]) { + indices.push(ci); + qi += 1; + } + } - if indices.len() != query_chars.len() { - self.score = Some(i32::MIN); - return i32::MIN; - } + if indices.len() != query_chars.len() { + self.score = Some(i32::MIN); + return i32::MIN; + } - let mut score: i32 = 0; + let mut score: i32 = 0; - for (i, &idx) in indices.iter().enumerate() { - if idx == 0 { - score += Self::BONUS_FIRST_CHAR; - } + for (i, &idx) in indices.iter().enumerate() { + if idx == 0 { + score += Self::BONUS_FIRST_CHAR; + } + if idx == 0 || Self::is_word_bound(content_chars[idx - 1], content_chars[idx]) { + score += Self::BONUS_BOUNDARY; + } - if idx == 0 || Self::is_word_bound(content_chars[idx - 1], content_chars[idx]) { - score += Self::BONUS_BOUNDARY; - } + if i > 0 { + let gap = idx - indices[i - 1] - 1; + if gap == 0 { + score += Self::BONUS_CONSECUTIVE; + } else { + score -= Self::PENALTY_GAP_START + (gap as i32 - 1) * Self::PENALTY_GAP_EXTEND; + } + } + } - if i > 0 { - let gap = idx - indices[i - 1] - 1; - if gap == 0 { - score += Self::BONUS_CONSECUTIVE; - } else { - score -= Self::PENALTY_GAP_START + (gap as i32 - 1) * Self::PENALTY_GAP_EXTEND; - } - } - } - - self.score = Some(score); - score - } + self.score = Some(score); + score + } } impl From for ScoredCandidate { - fn from(content: String) -> Self { - Self { content, score: None } - } + fn from(content: String) -> Self { + Self { + content, + score: None, + } + } } #[derive(Debug, Clone)] pub struct FuzzyLayout { - rows: u16, - cols: u16, - cursor_col: u16, - /// Width of the prompt line above the `\n` that starts the fuzzy window. - /// If PSR was drawn, this is `t_cols`; otherwise the content width. - preceding_line_width: u16, - /// Cursor column on the prompt line before the fuzzy window was drawn. - preceding_cursor_col: u16, + rows: u16, + cols: u16, + cursor_col: u16, + /// Width of the prompt line above the `\n` that starts the fuzzy window. + /// If PSR was drawn, this is `t_cols`; otherwise the content width. + preceding_line_width: u16, + /// Cursor column on the prompt line before the fuzzy window was drawn. + preceding_cursor_col: u16, } #[derive(Default, Debug, Clone)] pub struct QueryEditor { - mode: ViInsert, - scroll_offset: usize, - available_width: usize, - linebuf: LineBuf + mode: ViInsert, + scroll_offset: usize, + available_width: usize, + linebuf: LineBuf, } impl QueryEditor { - pub fn clear(&mut self) { - self.linebuf = LineBuf::new(); - self.mode = ViInsert::default(); - self.scroll_offset = 0; - } - pub fn set_available_width(&mut self, width: usize) { - self.available_width = width; - } - pub fn update_scroll_offset(&mut self) { - self.linebuf.update_graphemes(); - let cursor_pos = self.linebuf.cursor.get(); - if cursor_pos < self.scroll_offset + 1 { - self.scroll_offset = self.linebuf.cursor.ret_sub(1); - } - if cursor_pos >= self.scroll_offset + self.available_width.saturating_sub(1) { - self.scroll_offset = self.linebuf.cursor.ret_sub(self.available_width.saturating_sub(1)); - } - let max_offset = self.linebuf.grapheme_indices().len().saturating_sub(self.available_width); - self.scroll_offset = self.scroll_offset.min(max_offset); - } - pub fn get_window(&mut self) -> String { - self.linebuf.update_graphemes(); - let buf_len = self.linebuf.grapheme_indices().len(); - if buf_len <= self.available_width { - return self.linebuf.as_str().to_string(); - } - let start = self.scroll_offset.min(buf_len.saturating_sub(self.available_width)); - let end = (start + self.available_width).min(buf_len); - self.linebuf.slice(start..end).unwrap_or("").to_string() - } - pub fn handle_key(&mut self, key: K) -> ShResult<()> { - let Some(cmd) = self.mode.handle_key(key) else { - return Ok(()) - }; - self.linebuf.exec_cmd(cmd) - } + pub fn clear(&mut self) { + self.linebuf = LineBuf::new(); + self.mode = ViInsert::default(); + self.scroll_offset = 0; + } + pub fn set_available_width(&mut self, width: usize) { + self.available_width = width; + } + pub fn update_scroll_offset(&mut self) { + self.linebuf.update_graphemes(); + let cursor_pos = self.linebuf.cursor.get(); + if cursor_pos < self.scroll_offset + 1 { + self.scroll_offset = self.linebuf.cursor.ret_sub(1); + } + if cursor_pos >= self.scroll_offset + self.available_width.saturating_sub(1) { + self.scroll_offset = self + .linebuf + .cursor + .ret_sub(self.available_width.saturating_sub(1)); + } + let max_offset = self + .linebuf + .grapheme_indices() + .len() + .saturating_sub(self.available_width); + self.scroll_offset = self.scroll_offset.min(max_offset); + } + pub fn get_window(&mut self) -> String { + self.linebuf.update_graphemes(); + let buf_len = self.linebuf.grapheme_indices().len(); + if buf_len <= self.available_width { + return self.linebuf.as_str().to_string(); + } + let start = self + .scroll_offset + .min(buf_len.saturating_sub(self.available_width)); + let end = (start + self.available_width).min(buf_len); + self.linebuf.slice(start..end).unwrap_or("").to_string() + } + pub fn handle_key(&mut self, key: K) -> ShResult<()> { + let Some(cmd) = self.mode.handle_key(key) else { + return Ok(()); + }; + self.linebuf.exec_cmd(cmd) + } } - #[derive(Clone, Debug)] pub struct FuzzyCompleter { - completer: SimpleCompleter, - query: QueryEditor, - filtered: Vec, - candidates: Vec, - cursor: ClampedUsize, - old_layout: Option, - max_height: usize, - scroll_offset: usize, - active: bool, - /// Context from the prompt: width of the line above the fuzzy window - prompt_line_width: u16, - /// Context from the prompt: cursor column on the line above the fuzzy window - prompt_cursor_col: u16, + completer: SimpleCompleter, + query: QueryEditor, + filtered: Vec, + candidates: Vec, + cursor: ClampedUsize, + old_layout: Option, + max_height: usize, + scroll_offset: usize, + active: bool, + /// Context from the prompt: width of the line above the fuzzy window + prompt_line_width: u16, + /// Context from the prompt: cursor column on the line above the fuzzy window + prompt_cursor_col: u16, } impl FuzzyCompleter { - const BOT_LEFT: &str = "\x1b[90m╰\x1b[0m"; - const BOT_RIGHT: &str = "\x1b[90m╯\x1b[0m"; - const TOP_LEFT: &str = "\x1b[90m╭\x1b[0m"; - const TOP_RIGHT: &str = "\x1b[90m╮\x1b[0m"; - const HOR_LINE: &str = "\x1b[90m─\x1b[0m"; - const VERT_LINE: &str = "\x1b[90m│\x1b[0m"; - const SELECTOR_GRAY: &str = "\x1b[90m▌\x1b[0m"; - const SELECTOR_HL: &str = "\x1b[38;2;200;0;120m▌\x1b[1;39;48;5;237m"; - const PROMPT_ARROW: &str = "\x1b[1;36m>\x1b[0m"; - const TREE_LEFT: &str = "\x1b[90m├\x1b[0m"; - const TREE_RIGHT: &str = "\x1b[90m┤\x1b[0m"; - //const TREE_BOT: &str = "\x1b[90m┴\x1b[0m"; - //const TREE_TOP: &str = "\x1b[90m┬\x1b[0m"; - //const CROSS: &str = "\x1b[90m┼\x1b[0m"; + const BOT_LEFT: &str = "\x1b[90m╰\x1b[0m"; + const BOT_RIGHT: &str = "\x1b[90m╯\x1b[0m"; + const TOP_LEFT: &str = "\x1b[90m╭\x1b[0m"; + const TOP_RIGHT: &str = "\x1b[90m╮\x1b[0m"; + const HOR_LINE: &str = "\x1b[90m─\x1b[0m"; + const VERT_LINE: &str = "\x1b[90m│\x1b[0m"; + const SELECTOR_GRAY: &str = "\x1b[90m▌\x1b[0m"; + const SELECTOR_HL: &str = "\x1b[38;2;200;0;120m▌\x1b[1;39;48;5;237m"; + const PROMPT_ARROW: &str = "\x1b[1;36m>\x1b[0m"; + const TREE_LEFT: &str = "\x1b[90m├\x1b[0m"; + const TREE_RIGHT: &str = "\x1b[90m┤\x1b[0m"; + //const TREE_BOT: &str = "\x1b[90m┴\x1b[0m"; + //const TREE_TOP: &str = "\x1b[90m┬\x1b[0m"; + //const CROSS: &str = "\x1b[90m┼\x1b[0m"; + fn get_window(&mut self) -> &[ScoredCandidate] { + let height = self.filtered.len().min(self.max_height); - fn get_window(&mut self) -> &[ScoredCandidate] { - let height = self.filtered.len().min(self.max_height); + self.update_scroll_offset(); - self.update_scroll_offset(); - - &self.filtered[self.scroll_offset..self.scroll_offset + height] - } - pub fn update_scroll_offset(&mut self) { - let height = self.filtered.len().min(self.max_height); - if self.cursor.get() < self.scroll_offset + 1 { - self.scroll_offset = self.cursor.ret_sub(1); - } - if self.cursor.get() >= self.scroll_offset + height.saturating_sub(1) { - self.scroll_offset = self.cursor.ret_sub(height.saturating_sub(2)); - } - self.scroll_offset = self.scroll_offset.min(self.filtered.len().saturating_sub(height)); - } - pub fn score_candidates(&mut self) { - let mut scored: Vec<_> = self.candidates - .clone() - .into_iter() - .filter_map(|c| { - let mut sc = ScoredCandidate::new(c); - let score = sc.fuzzy_score(self.query.linebuf.as_str()); - if score > i32::MIN { - Some(sc) - } else { - None - } - }).collect(); - scored.sort_by_key(|sc| sc.score.unwrap_or(i32::MIN)); - scored.reverse(); - self.cursor.set_max(scored.len()); - self.filtered = scored; - } + &self.filtered[self.scroll_offset..self.scroll_offset + height] + } + pub fn update_scroll_offset(&mut self) { + let height = self.filtered.len().min(self.max_height); + if self.cursor.get() < self.scroll_offset + 1 { + self.scroll_offset = self.cursor.ret_sub(1); + } + if self.cursor.get() >= self.scroll_offset + height.saturating_sub(1) { + self.scroll_offset = self.cursor.ret_sub(height.saturating_sub(2)); + } + self.scroll_offset = self + .scroll_offset + .min(self.filtered.len().saturating_sub(height)); + } + pub fn score_candidates(&mut self) { + let mut scored: Vec<_> = self + .candidates + .clone() + .into_iter() + .filter_map(|c| { + let mut sc = ScoredCandidate::new(c); + let score = sc.fuzzy_score(self.query.linebuf.as_str()); + if score > i32::MIN { Some(sc) } else { None } + }) + .collect(); + scored.sort_by_key(|sc| sc.score.unwrap_or(i32::MIN)); + scored.reverse(); + self.cursor.set_max(scored.len()); + self.filtered = scored; + } } impl Default for FuzzyCompleter { - fn default() -> Self { - Self { - max_height: 8, - completer: SimpleCompleter::default(), - query: QueryEditor::default(), - filtered: vec![], - candidates: vec![], - cursor: ClampedUsize::new(0, 0, true), - old_layout: None, - scroll_offset: 0, - active: false, - prompt_line_width: 0, - prompt_cursor_col: 0, - } - } + fn default() -> Self { + Self { + max_height: 8, + completer: SimpleCompleter::default(), + query: QueryEditor::default(), + filtered: vec![], + candidates: vec![], + cursor: ClampedUsize::new(0, 0, true), + old_layout: None, + scroll_offset: 0, + active: false, + prompt_line_width: 0, + prompt_cursor_col: 0, + } + } } impl Completer for FuzzyCompleter { - fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) { - self.prompt_line_width = line_width; - self.prompt_cursor_col = cursor_col; - } - fn reset_stay_active(&mut self) { - if self.is_active() { - self.query.clear(); - self.score_candidates(); - } - } - fn get_completed_line(&self, _candidate: &str) -> String { - log::debug!("Getting completed line for candidate: {}", _candidate); + fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) { + self.prompt_line_width = line_width; + self.prompt_cursor_col = cursor_col; + } + fn reset_stay_active(&mut self) { + if self.is_active() { + self.query.clear(); + self.score_candidates(); + } + } + fn get_completed_line(&self, _candidate: &str) -> String { + log::debug!("Getting completed line for candidate: {}", _candidate); let selected = &self.filtered[self.cursor.get()].content; - log::debug!("Selected candidate: {}", selected); + log::debug!("Selected candidate: {}", selected); let (start, end) = self.completer.token_span; - log::debug!("Token span: ({}, {})", start, end); + log::debug!("Token span: ({}, {})", start, end); let ret = format!( "{}{}{}", &self.completer.original_input[..start], selected, &self.completer.original_input[end..] ); - log::debug!("Completed line: {}", ret); - ret - } - fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult> { - self.completer.complete(line, cursor_pos, direction)?; - let candidates: Vec<_> = self.completer.candidates.clone(); - if candidates.is_empty() { - self.completer.reset(); - self.active = false; - return Ok(None); - } else if candidates.len() == 1 { - self.filtered = candidates.into_iter().map(ScoredCandidate::from).collect(); - let completed = self.get_completed_line(&self.filtered[0].content); - self.active = false; - return Ok(Some(completed)); - } - self.active = true; - self.candidates = candidates; - self.score_candidates(); - Ok(None) - } + log::debug!("Completed line: {}", ret); + ret + } + fn complete( + &mut self, + line: String, + cursor_pos: usize, + direction: i32, + ) -> ShResult> { + self.completer.complete(line, cursor_pos, direction)?; + let candidates: Vec<_> = self.completer.candidates.clone(); + if candidates.is_empty() { + self.completer.reset(); + self.active = false; + return Ok(None); + } else if candidates.len() == 1 { + self.filtered = candidates.into_iter().map(ScoredCandidate::from).collect(); + let completed = self.get_completed_line(&self.filtered[0].content); + self.active = false; + return Ok(Some(completed)); + } + self.active = true; + self.candidates = candidates; + self.score_candidates(); + Ok(None) + } - fn handle_key(&mut self, key: K) -> ShResult { - match key { - K(C::Char('D'), M::CTRL) | - K(C::Esc, M::NONE) => { - self.active = false; - self.filtered.clear(); - Ok(CompResponse::Dismiss) - } - K(C::Enter, M::NONE) => { - self.active = false; - if let Some(selected) = self.filtered.get(self.cursor.get()).map(|c| c.content.clone()) { - Ok(CompResponse::Accept(selected)) - } else { - Ok(CompResponse::Dismiss) - } - } - K(C::Tab, M::SHIFT) | - K(C::Up, M::NONE) => { - self.cursor.wrap_sub(1); - self.update_scroll_offset(); - Ok(CompResponse::Consumed) - } - K(C::Tab, M::NONE) | - K(C::Down, M::NONE) => { - self.cursor.wrap_add(1); - self.update_scroll_offset(); - Ok(CompResponse::Consumed) - } - _ => { - self.query.handle_key(key)?; - self.score_candidates(); - Ok(CompResponse::Consumed) - } - } - } - fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> { - if let Some(layout) = self.old_layout.take() { - let (new_cols, _) = get_win_size(*TTY_FILENO); - // The fuzzy window is one continuous auto-wrapped block (no hard - // newlines between rows). After a resize the terminal re-joins - // soft wraps and re-wraps as a flat buffer. - let total_cells = layout.rows as u32 * layout.cols as u32; - let physical_rows = if new_cols > 0 { - total_cells.div_ceil(new_cols as u32) as u16 - } else { - layout.rows - }; - let cursor_offset = layout.cols as u32 + layout.cursor_col as u32; - let cursor_phys_row = if new_cols > 0 { - (cursor_offset / new_cols as u32) as u16 - } else { - 1 - }; - let lines_below = physical_rows.saturating_sub(cursor_phys_row + 1); + fn handle_key(&mut self, key: K) -> ShResult { + match key { + K(C::Char('D'), M::CTRL) | K(C::Esc, M::NONE) => { + self.active = false; + self.filtered.clear(); + Ok(CompResponse::Dismiss) + } + K(C::Enter, M::NONE) => { + self.active = false; + if let Some(selected) = self + .filtered + .get(self.cursor.get()) + .map(|c| c.content.clone()) + { + Ok(CompResponse::Accept(selected)) + } else { + Ok(CompResponse::Dismiss) + } + } + K(C::Tab, M::SHIFT) | K(C::Up, M::NONE) => { + self.cursor.wrap_sub(1); + self.update_scroll_offset(); + Ok(CompResponse::Consumed) + } + K(C::Tab, M::NONE) | K(C::Down, M::NONE) => { + self.cursor.wrap_add(1); + self.update_scroll_offset(); + Ok(CompResponse::Consumed) + } + _ => { + self.query.handle_key(key)?; + self.score_candidates(); + Ok(CompResponse::Consumed) + } + } + } + fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> { + if let Some(layout) = self.old_layout.take() { + let (new_cols, _) = get_win_size(*TTY_FILENO); + // The fuzzy window is one continuous auto-wrapped block (no hard + // newlines between rows). After a resize the terminal re-joins + // soft wraps and re-wraps as a flat buffer. + let total_cells = layout.rows as u32 * layout.cols as u32; + let physical_rows = if new_cols > 0 { + total_cells.div_ceil(new_cols as u32) as u16 + } else { + layout.rows + }; + let cursor_offset = layout.cols as u32 + layout.cursor_col as u32; + let cursor_phys_row = if new_cols > 0 { + (cursor_offset / new_cols as u32) as u16 + } else { + 1 + }; + let lines_below = physical_rows.saturating_sub(cursor_phys_row + 1); - // The prompt line above the \n may have wrapped (e.g. due to PSR - // filling to t_cols). Compute how many extra rows that adds - // between the prompt cursor and the fuzzy content. - let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols { - let wrap_rows = (layout.preceding_line_width as u32).div_ceil(new_cols as u32) as u16; - let cursor_wrap_row = layout.preceding_cursor_col / new_cols; - wrap_rows.saturating_sub(cursor_wrap_row + 1) - } else { - 0 - }; + // The prompt line above the \n may have wrapped (e.g. due to PSR + // filling to t_cols). Compute how many extra rows that adds + // between the prompt cursor and the fuzzy content. + let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols { + let wrap_rows = (layout.preceding_line_width as u32).div_ceil(new_cols as u32) as u16; + let cursor_wrap_row = layout.preceding_cursor_col / new_cols; + wrap_rows.saturating_sub(cursor_wrap_row + 1) + } else { + 0 + }; - let mut buf = String::new(); - if lines_below > 0 { - write!(buf, "\x1b[{}B", lines_below).unwrap(); - } - for _ in 0..physical_rows { - buf.push_str("\x1b[2K\x1b[A"); - } - buf.push_str("\x1b[2K"); - // Clear extra rows from prompt line wrapping (PSR) - for _ in 0..gap_extra { - buf.push_str("\x1b[A\x1b[2K"); - } - writer.flush_write(&buf)?; - } - Ok(()) - } - fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> { - if !self.active { - return Ok(()); - } - let (cols,_) = get_win_size(*TTY_FILENO); + let mut buf = String::new(); + if lines_below > 0 { + write!(buf, "\x1b[{}B", lines_below).unwrap(); + } + for _ in 0..physical_rows { + buf.push_str("\x1b[2K\x1b[A"); + } + buf.push_str("\x1b[2K"); + // Clear extra rows from prompt line wrapping (PSR) + for _ in 0..gap_extra { + buf.push_str("\x1b[A\x1b[2K"); + } + writer.flush_write(&buf)?; + } + Ok(()) + } + fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> { + if !self.active { + return Ok(()); + } + let (cols, _) = get_win_size(*TTY_FILENO); - let mut buf = String::new(); - let cursor_pos = self.cursor.get(); - let offset = self.scroll_offset; - self.query.set_available_width(cols.saturating_sub(6) as usize); - self.query.update_scroll_offset(); - let query = self.query.get_window(); - let num_filtered = format!("\x1b[33m{}\x1b[0m",self.filtered.len()); - let num_candidates = format!("\x1b[33m{}\x1b[0m",self.candidates.len()); - let visible = self.get_window(); - let mut rows: u16 = 0; - let top_bar = format!("\n{}{} \x1b[1mComplete\x1b[0m {}{}", - Self::TOP_LEFT, - Self::HOR_LINE, - Self::HOR_LINE.repeat(cols.saturating_sub(13) as usize), - Self::TOP_RIGHT - ); - buf.push_str(&top_bar); - rows += 1; - for _ in 0..rows { - } + let mut buf = String::new(); + let cursor_pos = self.cursor.get(); + let offset = self.scroll_offset; + self + .query + .set_available_width(cols.saturating_sub(6) as usize); + self.query.update_scroll_offset(); + let query = self.query.get_window(); + let num_filtered = format!("\x1b[33m{}\x1b[0m", self.filtered.len()); + let num_candidates = format!("\x1b[33m{}\x1b[0m", self.candidates.len()); + let visible = self.get_window(); + let mut rows: u16 = 0; + let top_bar = format!( + "\n{}{} \x1b[1mComplete\x1b[0m {}{}", + Self::TOP_LEFT, + Self::HOR_LINE, + Self::HOR_LINE.repeat(cols.saturating_sub(13) as usize), + Self::TOP_RIGHT + ); + buf.push_str(&top_bar); + rows += 1; + for _ in 0..rows {} - let prompt = format!("{} {} {}", Self::VERT_LINE, Self::PROMPT_ARROW, &query); - let cols_used = calc_str_width(&prompt); - let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize); - let prompt_line_final = format!("{}{}{}", prompt, right_pad, Self::VERT_LINE); - buf.push_str(&prompt_line_final); - rows += 1; + let prompt = format!("{} {} {}", Self::VERT_LINE, Self::PROMPT_ARROW, &query); + let cols_used = calc_str_width(&prompt); + let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize); + let prompt_line_final = format!("{}{}{}", prompt, right_pad, Self::VERT_LINE); + buf.push_str(&prompt_line_final); + rows += 1; - let sep_line_left = format!("{}{}{}/{}", - Self::TREE_LEFT, - Self::HOR_LINE.repeat(2), - &num_filtered, - &num_candidates - ); - let cols_used = calc_str_width(&sep_line_left); - let right_pad = Self::HOR_LINE.repeat(cols.saturating_sub(cols_used + 1) as usize); - let sep_line_final = format!("{}{}{}", sep_line_left, right_pad, Self::TREE_RIGHT); - buf.push_str(&sep_line_final); - rows += 1; + let sep_line_left = format!( + "{}{}{}/{}", + Self::TREE_LEFT, + Self::HOR_LINE.repeat(2), + &num_filtered, + &num_candidates + ); + let cols_used = calc_str_width(&sep_line_left); + let right_pad = Self::HOR_LINE.repeat(cols.saturating_sub(cols_used + 1) as usize); + let sep_line_final = format!("{}{}{}", sep_line_left, right_pad, Self::TREE_RIGHT); + buf.push_str(&sep_line_final); + rows += 1; + for (i, candidate) in visible.iter().enumerate() { + let selector = if i + offset == cursor_pos { + Self::SELECTOR_HL + } else { + Self::SELECTOR_GRAY + }; + let mut content = candidate.content.clone(); + let col_lim = cols.saturating_sub(3); + if calc_str_width(&content) > col_lim { + content.truncate(col_lim.saturating_sub(6) as usize); // ui bars + elipses length + content.push_str("..."); + } + let left = format!("{} {}{}\x1b[0m", Self::VERT_LINE, &selector, &content); + let cols_used = calc_str_width(&left); + let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize); + let hl_cand_line = format!("{}{}{}", left, right_pad, Self::VERT_LINE); + buf.push_str(&hl_cand_line); + rows += 1; + } - for (i, candidate) in visible.iter().enumerate() { - let selector = if i + offset == cursor_pos { - Self::SELECTOR_HL - } else { - Self::SELECTOR_GRAY - }; - let mut content = candidate.content.clone(); - let col_lim = cols.saturating_sub(3); - if calc_str_width(&content) > col_lim { - content.truncate(col_lim.saturating_sub(6) as usize); // ui bars + elipses length - content.push_str("..."); - } - let left = format!("{} {}{}\x1b[0m", - Self::VERT_LINE, - &selector, - &content - ); - let cols_used = calc_str_width(&left); - let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize); - let hl_cand_line = format!("{}{}{}", left, right_pad, Self::VERT_LINE); - buf.push_str(&hl_cand_line); - rows += 1; - } + let bot_bar = format!( + "{}{}{}", + Self::BOT_LEFT, + Self::HOR_LINE + .to_string() + .repeat(cols.saturating_sub(2) as usize), + Self::BOT_RIGHT + ); + buf.push_str(&bot_bar); + rows += 1; - let bot_bar = format!("{}{}{}", - Self::BOT_LEFT, - Self::HOR_LINE.to_string().repeat(cols.saturating_sub(2) as usize), - Self::BOT_RIGHT - ); - buf.push_str(&bot_bar); - rows += 1; + // Move cursor back up to the prompt line (skip: separator + candidates + bottom border) + let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt + let cursor_in_window = self + .query + .linebuf + .cursor + .get() + .saturating_sub(self.query.scroll_offset); + let cursor_col = (cursor_in_window + 4) as u16; // "| > ".len() == 4 + write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap(); - // Move cursor back up to the prompt line (skip: separator + candidates + bottom border) - let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt - let cursor_in_window = self.query.linebuf.cursor.get().saturating_sub(self.query.scroll_offset); - let cursor_col = (cursor_in_window + 4) as u16; // "| > ".len() == 4 - write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap(); + let new_layout = FuzzyLayout { + rows, + cols, + cursor_col, + preceding_line_width: self.prompt_line_width, + preceding_cursor_col: self.prompt_cursor_col, + }; + writer.flush_write(&buf)?; + self.old_layout = Some(new_layout); - let new_layout = FuzzyLayout { - rows, - cols, - cursor_col, - preceding_line_width: self.prompt_line_width, - preceding_cursor_col: self.prompt_cursor_col, - }; - writer.flush_write(&buf)?; - self.old_layout = Some(new_layout); - - Ok(()) - } - fn reset(&mut self) { - *self = Self::default(); - } - fn token_span(&self) -> (usize, usize) { - self.completer.token_span() - } - fn is_active(&self) -> bool { - self.active - } - fn selected_candidate(&self) -> Option { - self.filtered.get(self.cursor.get()).map(|c| c.content.clone()) - } - fn original_input(&self) -> &str { - &self.completer.original_input - } + Ok(()) + } + fn reset(&mut self) { + *self = Self::default(); + } + fn token_span(&self) -> (usize, usize) { + self.completer.token_span() + } + fn is_active(&self) -> bool { + self.active + } + fn selected_candidate(&self) -> Option { + self + .filtered + .get(self.cursor.get()) + .map(|c| c.content.clone()) + } + fn original_input(&self) -> &str { + &self.completer.original_input + } } #[derive(Default, Debug, Clone)] @@ -1012,49 +1058,54 @@ pub struct SimpleCompleter { } impl Completer for SimpleCompleter { - fn reset_stay_active(&mut self) { - let active = self.is_active(); - self.reset(); - self.active = active; - } - fn get_completed_line(&self, _candidate: &str) -> String { - self.get_completed_line() - } - fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult> { - if self.active { - Ok(Some(self.cycle_completion(direction))) - } else { - self.start_completion(line, cursor_pos) - } - } + fn reset_stay_active(&mut self) { + let active = self.is_active(); + self.reset(); + self.active = active; + } + fn get_completed_line(&self, _candidate: &str) -> String { + self.get_completed_line() + } + fn complete( + &mut self, + line: String, + cursor_pos: usize, + direction: i32, + ) -> ShResult> { + if self.active { + Ok(Some(self.cycle_completion(direction))) + } else { + self.start_completion(line, cursor_pos) + } + } - fn reset(&mut self) { - *self = Self::default(); - } + fn reset(&mut self) { + *self = Self::default(); + } - fn is_active(&self) -> bool { - self.active - } + fn is_active(&self) -> bool { + self.active + } - fn selected_candidate(&self) -> Option { - self.candidates.get(self.selected_idx).cloned() - } + fn selected_candidate(&self) -> Option { + self.candidates.get(self.selected_idx).cloned() + } - fn token_span(&self) -> (usize, usize) { - self.token_span - } + fn token_span(&self) -> (usize, usize) { + self.token_span + } - fn draw(&mut self, _writer: &mut TermWriter) -> ShResult<()> { - Ok(()) - } + fn draw(&mut self, _writer: &mut TermWriter) -> ShResult<()> { + Ok(()) + } - fn original_input(&self) -> &str { - &self.original_input - } + fn original_input(&self) -> &str { + &self.original_input + } - fn handle_key(&mut self, _key: K) -> ShResult { - Ok(CompResponse::Passthrough) - } + fn handle_key(&mut self, _key: K) -> ShResult { + Ok(CompResponse::Passthrough) + } } impl SimpleCompleter { @@ -1295,16 +1346,17 @@ impl SimpleCompleter { let (mut marker_ctx, token_start) = self.get_subtoken_completion(&line, cursor_pos); if marker_ctx.last() == Some(&markers::VAR_SUB) - && let Some(cur) = ctx.words.get(ctx.cword) { - self.token_span.0 = token_start; - let mut span = cur.span.clone(); - span.set_range(token_start..self.token_span.1); - let raw_tk = span.as_str(); - let candidates = complete_vars(raw_tk); - if !candidates.is_empty() { - return Ok(CompResult::from_candidates(candidates)); - } - } + && let Some(cur) = ctx.words.get(ctx.cword) + { + self.token_span.0 = token_start; + let mut span = cur.span.clone(); + span.set_range(token_start..self.token_span.1); + let raw_tk = span.as_str(); + let candidates = complete_vars(raw_tk); + if !candidates.is_empty() { + return Ok(CompResult::from_candidates(candidates)); + } + } // Try programmable completion match self.try_comp_spec(&ctx)? { diff --git a/src/readline/highlight.rs b/src/readline/highlight.rs index dc39621..49e6266 100644 --- a/src/readline/highlight.rs +++ b/src/readline/highlight.rs @@ -26,7 +26,7 @@ pub struct Highlighter { style_stack: Vec, last_was_reset: bool, in_selection: bool, - only_hl_visual: bool + only_hl_visual: bool, } impl Highlighter { @@ -39,12 +39,12 @@ impl Highlighter { style_stack: Vec::new(), last_was_reset: true, // start as true so we don't emit a leading reset in_selection: false, - only_hl_visual: false + only_hl_visual: false, } } - pub fn only_visual(&mut self, only_visual: bool) { - self.only_hl_visual = only_visual; - } + pub fn only_visual(&mut self, only_visual: bool) { + self.only_hl_visual = only_visual; + } /// Loads raw input text and annotates it with syntax markers /// @@ -66,25 +66,25 @@ impl Highlighter { out } - pub fn expand_control_chars(&mut self) { - let mut expanded = String::new(); - let mut chars = self.input.chars().peekable(); + pub fn expand_control_chars(&mut self) { + let mut expanded = String::new(); + let mut chars = self.input.chars().peekable(); - while let Some(ch) = chars.next() { - match ch { - '\n' | '\t' | '\r' => expanded.push(ch), - c if c as u32 <= 0x1F => { - let display = (c as u8 + b'@') as char; - expanded.push_str("\x1b[7m^"); - expanded.push(display); - expanded.push_str("\x1b[0m"); - } - _ => expanded.push(ch), - } - } + while let Some(ch) = chars.next() { + match ch { + '\n' | '\t' | '\r' => expanded.push(ch), + c if c as u32 <= 0x1F => { + let display = (c as u8 + b'@') as char; + expanded.push_str("\x1b[7m^"); + expanded.push(display); + expanded.push_str("\x1b[0m"); + } + _ => expanded.push(ch), + } + } - self.input = expanded; - } + self.input = expanded; + } /// Processes the annotated input and generates ANSI-styled output /// @@ -104,9 +104,9 @@ impl Highlighter { self.reapply_style(); self.in_selection = false; } - _ if self.only_hl_visual => { - self.output.push(ch); - } + _ if self.only_hl_visual => { + self.output.push(ch); + } markers::STRING_DQ_END | markers::STRING_SQ_END | markers::VAR_SUB_END @@ -471,7 +471,7 @@ impl Highlighter { } impl Default for Highlighter { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } diff --git a/src/readline/history.rs b/src/readline/history.rs index befcbd9..1b44a3c 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -70,10 +70,10 @@ impl HistEntry { impl FromStr for HistEntry { type Err = ShErr; fn from_str(s: &str) -> Result { - let err = Err(ShErr::simple( - ShErrKind::HistoryReadErr, - format!("Bad formatting on history entry '{s}'"), - )); + let err = Err(ShErr::simple( + ShErrKind::HistoryReadErr, + format!("Bad formatting on history entry '{s}'"), + )); //: 248972349;148;echo foo; echo bar let Some(cleaned) = s.strip_prefix(": ") else { @@ -132,10 +132,10 @@ impl FromStr for HistEntries { while let Some((i, line)) = lines.next() { if !line.starts_with(": ") { - return Err(ShErr::simple( - ShErrKind::HistoryReadErr, - format!("Bad formatting on line {i}"), - )); + return Err(ShErr::simple( + ShErrKind::HistoryReadErr, + format!("Bad formatting on line {i}"), + )); } let mut chars = line.chars().peekable(); let mut feeding_lines = true; @@ -161,10 +161,10 @@ impl FromStr for HistEntries { } if feeding_lines { let Some((_, line)) = lines.next() else { - return Err(ShErr::simple( - ShErrKind::HistoryReadErr, - format!("Bad formatting on line {i}"), - )); + return Err(ShErr::simple( + ShErrKind::HistoryReadErr, + format!("Bad formatting on line {i}"), + )); }; chars = line.chars().peekable(); } diff --git a/src/readline/keys.rs b/src/readline/keys.rs index 9f66579..4815eb0 100644 --- a/src/readline/keys.rs +++ b/src/readline/keys.rs @@ -89,110 +89,112 @@ impl KeyEvent { } } } - pub fn as_vim_seq(&self) -> ShResult { - let mut seq = String::new(); - let KeyEvent(event, mods) = self; - let mut needs_angle_bracket = false; + pub fn as_vim_seq(&self) -> ShResult { + 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; - } + 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; - } + 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), - KeyCode::Verbatim(s) => seq.push_str(s), - } + 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), + KeyCode::Verbatim(s) => seq.push_str(s), + } - if needs_angle_bracket { - Ok(format!("<{}>", seq)) - } else { - Ok(seq) - } - } + if needs_angle_bracket { + Ok(format!("<{}>", seq)) + } else { + Ok(seq) + } + } } #[derive(Clone, PartialEq, Eq, Debug)] @@ -204,7 +206,7 @@ pub enum KeyCode { BracketedPasteEnd, Char(char), Grapheme(Arc), - Verbatim(Arc), // For sequences that should be treated as literal input, not parsed into a KeyCode + Verbatim(Arc), // For sequences that should be treated as literal input, not parsed into a KeyCode Delete, Down, End, diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index 871cd9a..b2fe6a5 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -1,5 +1,7 @@ use std::{ - collections::HashSet, fmt::Display, ops::{Range, RangeInclusive} + collections::HashSet, + fmt::Display, + ops::{Range, RangeInclusive}, }; use unicode_segmentation::UnicodeSegmentation; @@ -11,7 +13,10 @@ use super::vicmd::{ }; use crate::{ libsh::{error::ShResult, guards::var_ctx_guard}, - parse::{execute::exec_input, lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}}, + parse::{ + execute::exec_input, + lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, + }, prelude::*, readline::{ markers, @@ -297,20 +302,20 @@ impl ClampedUsize { pub fn sub(&mut self, value: usize) { self.value = self.value.saturating_sub(value) } - pub fn wrap_add(&mut self, value: usize) { - self.value = self.ret_wrap_add(value); - } - pub fn wrap_sub(&mut self, value: usize) { - self.value = self.ret_wrap_sub(value); - } - pub fn ret_wrap_add(&self, value: usize) -> usize { - let max = self.upper_bound(); - (self.value + value) % (max + 1) - } - pub fn ret_wrap_sub(&self, value: usize) -> usize { - let max = self.upper_bound(); - (self.value + (max + 1) - (value % (max + 1))) % (max + 1) - } + pub fn wrap_add(&mut self, value: usize) { + self.value = self.ret_wrap_add(value); + } + pub fn wrap_sub(&mut self, value: usize) { + self.value = self.ret_wrap_sub(value); + } + pub fn ret_wrap_add(&self, value: usize) -> usize { + let max = self.upper_bound(); + (self.value + value) % (max + 1) + } + pub fn ret_wrap_sub(&self, value: usize) -> usize { + let max = self.upper_bound(); + (self.value + (max + 1) - (value % (max + 1))) % (max + 1) + } /// Add a value to the wrapped usize, return the result /// /// Returns the result instead of mutating the inner value @@ -352,12 +357,12 @@ pub struct LineBuf { impl LineBuf { pub fn new() -> Self { - let mut new = Self { - grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None - ..Default::default() - }; - new.update_graphemes(); - new + let mut new = Self { + grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None + ..Default::default() + }; + new.update_graphemes(); + new } /// Only update self.grapheme_indices if it is None pub fn update_graphemes_lazy(&mut self) { @@ -437,17 +442,17 @@ impl LineBuf { self.cursor.set_max(indices.len()); self.grapheme_indices = Some(indices) } - #[track_caller] + #[track_caller] pub fn grapheme_indices(&self) -> &[usize] { - if self.grapheme_indices.is_none() { - let caller = std::panic::Location::caller(); - panic!( - "grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}", - caller.file(), - caller.line(), - caller.column(), - ); - } + if self.grapheme_indices.is_none() { + let caller = std::panic::Location::caller(); + panic!( + "grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}", + caller.file(), + caller.line(), + caller.column(), + ); + } self.grapheme_indices.as_ref().unwrap() } pub fn grapheme_indices_owned(&self) -> Vec { @@ -806,19 +811,19 @@ impl LineBuf { } Some(self.line_bounds(line_no)) } - pub fn this_word(&mut self, word: Word) -> (usize, usize) { - let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { - self.cursor.get() - } else { - self.start_of_word_backward(self.cursor.get(), word) - }; - 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) - }; - (start, end) - } + pub fn this_word(&mut self, word: Word) -> (usize, usize) { + let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { + self.cursor.get() + } else { + self.start_of_word_backward(self.cursor.get(), word) + }; + 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) + }; + (start, end) + } pub fn this_line_exclusive(&mut self) -> (usize, usize) { let line_no = self.cursor_line_number(); let (start, mut end) = self.line_bounds(line_no); @@ -827,10 +832,10 @@ impl LineBuf { } (start, end) } - pub fn this_line_content(&mut self) -> Option<&str> { - let (start,end) = self.this_line_exclusive(); - self.slice(start..end) - } + pub fn this_line_content(&mut self) -> Option<&str> { + let (start, end) = self.this_line_exclusive(); + self.slice(start..end) + } pub fn this_line(&mut self) -> (usize, usize) { let line_no = self.cursor_line_number(); self.line_bounds(line_no) @@ -940,7 +945,7 @@ impl LineBuf { } pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool { let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true); - log::debug!("clamped_pos: {}", clamped_pos.get()); + log::debug!("clamped_pos: {}", clamped_pos.get()); let cur_char = self .grapheme_at(clamped_pos.get()) .map(|c| c.to_string()) @@ -1011,11 +1016,11 @@ impl LineBuf { } else { self.start_of_word_backward(self.cursor.get(), word) }; - 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) - }; + 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)) } Bound::Around => { @@ -1994,9 +1999,9 @@ impl LineBuf { let mut level: usize = 0; - if to_cursor.ends_with("\\\n") { - level += 1; // Line continuation, so we need to add an extra level - } + if to_cursor.ends_with("\\\n") { + level += 1; // Line continuation, so we need to add an extra level + } let input = Arc::new(to_cursor); let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::>>() @@ -2004,23 +2009,23 @@ impl LineBuf { log::error!("Failed to lex buffer for indent calculation"); return; }; - let mut last_keyword: Option = None; + let mut last_keyword: Option = None; for tk in tokens { if tk.flags.contains(TkFlags::KEYWORD) { match tk.as_str() { - "in" => { - if last_keyword.as_deref() == Some("case") { - level += 1; - } else { - // 'in' is also used in for loops, but we already increment level on 'do' for those - // so we just skip it here - } - } + "in" => { + if last_keyword.as_deref() == Some("case") { + level += 1; + } else { + // 'in' is also used in for loops, but we already increment level on 'do' for those + // so we just skip it here + } + } "then" | "do" => level += 1, "done" | "fi" | "esac" => level = level.saturating_sub(1), _ => { /* Continue */ } } - last_keyword = Some(tk.to_string()); + last_keyword = Some(tk.to_string()); } else if tk.class == TkRule::BraceGrpStart { level += 1; } else if tk.class == TkRule::BraceGrpEnd { @@ -2362,12 +2367,12 @@ impl LineBuf { } MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0), MotionCmd(_count, Motion::EndOfBuffer) => { - if self.cursor.exclusive { - MotionKind::On(self.grapheme_indices().len().saturating_sub(1)) - } else { - MotionKind::On(self.grapheme_indices().len()) - } - }, + if self.cursor.exclusive { + MotionKind::On(self.grapheme_indices().len().saturating_sub(1)) + } else { + MotionKind::On(self.grapheme_indices().len()) + } + } MotionCmd(_count, Motion::ToColumn) => todo!(), MotionCmd(count, Motion::Range(start, end)) => { let mut final_end = end; @@ -2794,7 +2799,11 @@ impl LineBuf { match content { RegisterContent::Span(ref text) => { let insert_idx = match anchor { - Anchor::After => self.cursor.get().saturating_add(1).min(self.grapheme_indices().len()), + Anchor::After => self + .cursor + .get() + .saturating_add(1) + .min(self.grapheme_indices().len()), Anchor::Before => self.cursor.get(), }; self.insert_str_at(insert_idx, text); @@ -2859,34 +2868,35 @@ impl LineBuf { Verb::InsertChar(ch) => { self.insert_at_cursor(ch); self.cursor.add(1); - let before = self.auto_indent_level; - if read_shopts(|o| o.prompt.auto_indent) - && let Some(line_content) = self.this_line_content() { - match line_content.trim() { - "esac" | "done" | "fi" | "}" => { - self.calc_indent_level(); - if self.auto_indent_level < before { - let delta = before - self.auto_indent_level; - let line_start = self.start_of_line(); - for _ in 0..delta { - if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { - self.remove(line_start); - if !self.cursor_at_max() { - self.cursor.sub(1); - } - } - } - } - } - _ => { /* nothing to see here */ } - } - } + let before = self.auto_indent_level; + if read_shopts(|o| o.prompt.auto_indent) + && let Some(line_content) = self.this_line_content() + { + match line_content.trim() { + "esac" | "done" | "fi" | "}" => { + self.calc_indent_level(); + if self.auto_indent_level < before { + let delta = before - self.auto_indent_level; + let line_start = self.start_of_line(); + for _ in 0..delta { + if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { + self.remove(line_start); + if !self.cursor_at_max() { + self.cursor.sub(1); + } + } + } + } + } + _ => { /* nothing to see here */ } + } + } } Verb::Insert(string) => { self.push_str(&string); let graphemes = string.graphemes(true).count(); - log::debug!("Inserted string: {string:?}, graphemes: {graphemes}"); - log::debug!("buffer after insert: {:?}", self.buffer); + log::debug!("Inserted string: {string:?}, graphemes: {graphemes}"); + log::debug!("buffer after insert: {:?}", self.buffer); self.cursor.add(graphemes); } Verb::Indent => { @@ -3039,71 +3049,82 @@ impl LineBuf { } } } - Verb::IncrementNumber(n) | - Verb::DecrementNumber(n) => { - let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) }; - let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal)); - let end = if self.select_range().is_some() { - if e < self.grapheme_indices().len() - 1 { - e - } else { - e + 1 - } - } else { - (e + 1).min(self.grapheme_indices().len()) - }; // inclusive → exclusive, capped at buffer len - let word = self.slice(s..end).unwrap_or_default().to_lowercase(); + Verb::IncrementNumber(n) | Verb::DecrementNumber(n) => { + let inc = if matches!(verb, Verb::IncrementNumber(_)) { + n as i64 + } else { + -(n as i64) + }; + let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal)); + let end = if self.select_range().is_some() { + if e < self.grapheme_indices().len() - 1 { + e + } else { + e + 1 + } + } else { + (e + 1).min(self.grapheme_indices().len()) + }; // inclusive → exclusive, capped at buffer len + let word = self.slice(s..end).unwrap_or_default().to_lowercase(); - let byte_start = self.index_byte_pos(s); - let byte_end = if end >= self.grapheme_indices().len() { - self.buffer.len() - } else { - self.index_byte_pos(end) - }; + let byte_start = self.index_byte_pos(s); + let byte_end = if end >= self.grapheme_indices().len() { + self.buffer.len() + } else { + self.index_byte_pos(end) + }; - if word.starts_with("0x") { - let body = word.strip_prefix("0x").unwrap(); - let width = body.len(); - if let Ok(num) = i64::from_str_radix(body, 16) { - let new_num = num + inc; - self.buffer.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}")); - self.update_graphemes(); - self.cursor.set(s); - } - } else if word.starts_with("0b") { - let body = word.strip_prefix("0b").unwrap(); - let width = body.len(); - if let Ok(num) = i64::from_str_radix(body, 2) { - let new_num = num + inc; - self.buffer.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}")); - self.update_graphemes(); - self.cursor.set(s); - } - } else if word.starts_with("0o") { - let body = word.strip_prefix("0o").unwrap(); - let width = body.len(); - if let Ok(num) = i64::from_str_radix(body, 8) { - let new_num = num + inc; - self.buffer.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}")); - self.update_graphemes(); - self.cursor.set(s); - } - } else if let Ok(num) = word.parse::() { - let width = word.len(); - let new_num = num + inc; - self.buffer.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}")); - self.update_graphemes(); - self.cursor.set(s); - } - } + if word.starts_with("0x") { + let body = word.strip_prefix("0x").unwrap(); + let width = body.len(); + if let Ok(num) = i64::from_str_radix(body, 16) { + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}")); + self.update_graphemes(); + self.cursor.set(s); + } + } else if word.starts_with("0b") { + let body = word.strip_prefix("0b").unwrap(); + let width = body.len(); + if let Ok(num) = i64::from_str_radix(body, 2) { + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}")); + self.update_graphemes(); + self.cursor.set(s); + } + } else if word.starts_with("0o") { + let body = word.strip_prefix("0o").unwrap(); + let width = body.len(); + if let Ok(num) = i64::from_str_radix(body, 8) { + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}")); + self.update_graphemes(); + self.cursor.set(s); + } + } else if let Ok(num) = word.parse::() { + let width = word.len(); + let new_num = num + inc; + self + .buffer + .replace_range(byte_start..byte_end, &format!("{new_num:0>width$}")); + self.update_graphemes(); + self.cursor.set(s); + } + } Verb::Complete - | Verb::ExMode + | Verb::ExMode | Verb::EndOfFile | Verb::InsertMode | Verb::NormalMode | Verb::VisualMode - | Verb::VerbatimMode + | Verb::VerbatimMode | Verb::ReplaceMode | Verb::VisualModeLine | Verb::VisualModeBlock @@ -3111,46 +3132,63 @@ impl LineBuf { | Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these Verb::ShellCmd(cmd) => { - log::debug!("Executing ex-mode command from widget: {cmd}"); - let mut vars = HashSet::new(); - vars.insert("_BUFFER".into()); - vars.insert("_CURSOR".into()); - vars.insert("_ANCHOR".into()); - let _guard = var_ctx_guard(vars); + log::debug!("Executing ex-mode command from widget: {cmd}"); + let mut vars = HashSet::new(); + vars.insert("_BUFFER".into()); + vars.insert("_CURSOR".into()); + vars.insert("_ANCHOR".into()); + let _guard = var_ctx_guard(vars); - let mut buf = self.as_str().to_string(); - 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); + let mut buf = self.as_str().to_string(); + 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| { - v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; - v.set_var("_CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)?; - v.set_var("_ANCHOR", VarKind::Str(anchor.to_string()), VarFlags::EXPORT) - })?; + write_vars(|v| { + v.set_var("_BUFFER", VarKind::Str(buf.clone()), 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("".into())))?; + RawModeGuard::with_cooked_mode(|| { + exec_input(cmd, None, true, Some("".into())) + })?; - let keys = write_vars(|v| { - buf = v.take_var("_BUFFER"); - cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor); - anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor); - v.take_var("_KEYS") - }); + let keys = write_vars(|v| { + buf = v.take_var("_BUFFER"); + 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.update_graphemes(); - self.cursor.set_max(self.buffer.graphemes(true).count()); - 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)) - } - - } + self.set_buffer(buf); + self.update_graphemes(); + self.cursor.set_max(self.buffer.graphemes(true).count()); + 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::Read(_) | Verb::Write(_) diff --git a/src/readline/mod.rs b/src/readline/mod.rs index cc8e1c9..16b63c2 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -1,7 +1,7 @@ -use std::fmt::Write; use history::History; use keys::{KeyCode, KeyEvent, ModKeys}; use linebuf::{LineBuf, SelectAnchor, SelectMode}; +use std::fmt::Write; use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size}; use unicode_width::UnicodeWidthStr; use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; @@ -12,16 +12,21 @@ use crate::expand::expand_prompt; use crate::libsh::sys::TTY_FILENO; use crate::libsh::utils::AutoCmdVecUtils; use crate::parse::lex::{LexStream, QuoteState}; -use crate::{prelude::*, state}; use crate::readline::complete::FuzzyCompleter; use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::vimode::{ViEx, ViVerbatim}; -use crate::state::{AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars}; +use crate::state::{ + AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, write_meta, write_vars, +}; use crate::{ libsh::error::ShResult, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, - readline::{complete::{CompResponse, Completer}, highlight::Highlighter}, + readline::{ + complete::{CompResponse, Completer}, + highlight::Highlighter, + }, }; +use crate::{prelude::*, state}; pub mod complete; pub mod highlight; @@ -150,8 +155,8 @@ impl Prompt { let Ok(ps1_raw) = env::var("PS1") else { 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(); + // 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 { return Self::default(); @@ -164,8 +169,8 @@ impl Prompt { .ok() .flatten(); - // Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt - state::set_status(saved_status); + // Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt + state::set_status(saved_status); Self { ps1_expanded, ps1_raw, @@ -208,10 +213,10 @@ impl Prompt { if let Ok(expanded) = expand_prompt(&self.ps1_raw) { self.ps1_expanded = expanded; } - if let Some(psr_raw) = &self.psr_raw { - if let Ok(expanded) = expand_prompt(psr_raw) { - self.psr_expanded = Some(expanded); - } + if let Some(psr_raw) = &self.psr_raw + && let Ok(expanded) = expand_prompt(psr_raw) + { + self.psr_expanded = Some(expanded); } state::set_status(saved_status); self.dirty = false; @@ -244,12 +249,12 @@ pub struct ShedVi { pub completer: Box, pub mode: Box, - pub saved_mode: Option>, - pub pending_keymap: Vec, + pub saved_mode: Option>, + pub pending_keymap: Vec, pub repeat_action: Option, pub repeat_motion: Option, pub editor: LineBuf, - pub next_is_escaped: bool, + pub next_is_escaped: bool, pub old_layout: Option, pub history: History, @@ -266,9 +271,9 @@ impl ShedVi { completer: Box::new(FuzzyCompleter::default()), highlighter: Highlighter::new(), mode: Box::new(ViInsert::new()), - next_is_escaped: false, - saved_mode: None, - pending_keymap: Vec::new(), + next_is_escaped: false, + saved_mode: None, + pending_keymap: Vec::new(), old_layout: None, repeat_action: None, repeat_motion: None, @@ -276,8 +281,14 @@ impl ShedVi { history: History::new()?, needs_redraw: true, }; - write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(new.mode.report_mode().to_string()), VarFlags::NONE))?; - new.prompt.refresh(); + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(new.mode.report_mode().to_string()), + VarFlags::NONE, + ) + })?; + new.prompt.refresh(); new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.print_line(false)?; Ok(new) @@ -293,7 +304,7 @@ impl ShedVi { /// Feed raw bytes from stdin into the reader's buffer pub fn feed_bytes(&mut self, bytes: &[u8]) { - let verbatim = self.mode.report_mode() == ModeReport::Verbatim; + let verbatim = self.mode.report_mode() == ModeReport::Verbatim; self.reader.feed_bytes(bytes, verbatim); } @@ -302,19 +313,21 @@ impl ShedVi { self.needs_redraw = true; } - pub fn fix_column(&mut self) -> ShResult<()> { - self.writer.fix_cursor_column(&mut TermReader::new(*TTY_FILENO)) - } + pub fn fix_column(&mut self) -> ShResult<()> { + self + .writer + .fix_cursor_column(&mut TermReader::new(*TTY_FILENO)) + } - pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> { - if self.completer.is_active() { - self.completer.reset_stay_active(); - self.needs_redraw = true; - Ok(()) - } else { - self.reset(full_redraw) - } - } + pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> { + if self.completer.is_active() { + self.completer.reset_stay_active(); + self.needs_redraw = true; + Ok(()) + } else { + self.reset(full_redraw) + } + } /// Reset readline state for a new prompt pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> { @@ -322,7 +335,7 @@ impl ShedVi { // so print_line can call clear_rows with the full multi-line layout self.prompt.refresh(); self.editor = Default::default(); - self.swap_mode(&mut (Box::new(ViInsert::new()) as Box)); + self.swap_mode(&mut (Box::new(ViInsert::new()) as Box)); self.needs_redraw = true; if full_redraw { self.old_layout = None; @@ -340,24 +353,24 @@ impl ShedVi { &mut self.prompt } - pub fn curr_keymap_flags(&self) -> KeyMapFlags { - let mut flags = KeyMapFlags::empty(); - match self.mode.report_mode() { - ModeReport::Insert => flags |= KeyMapFlags::INSERT, - ModeReport::Normal => flags |= KeyMapFlags::NORMAL, - ModeReport::Ex => flags |= KeyMapFlags::EX, - ModeReport::Visual => flags |= KeyMapFlags::VISUAL, - ModeReport::Replace => flags |= KeyMapFlags::REPLACE, - ModeReport::Verbatim => flags |= KeyMapFlags::VERBATIM, - ModeReport::Unknown => todo!(), - } + pub fn curr_keymap_flags(&self) -> KeyMapFlags { + let mut flags = KeyMapFlags::empty(); + match self.mode.report_mode() { + ModeReport::Insert => flags |= KeyMapFlags::INSERT, + ModeReport::Normal => flags |= KeyMapFlags::NORMAL, + ModeReport::Ex => flags |= KeyMapFlags::EX, + ModeReport::Visual => flags |= KeyMapFlags::VISUAL, + ModeReport::Replace => flags |= KeyMapFlags::REPLACE, + ModeReport::Verbatim => flags |= KeyMapFlags::VERBATIM, + ModeReport::Unknown => todo!(), + } - if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) { - flags |= KeyMapFlags::OP_PENDING; - } + if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) { + flags |= KeyMapFlags::OP_PENDING; + } - flags - } + flags + } fn should_submit(&mut self) -> ShResult { if self.mode.report_mode() == ModeReport::Normal { @@ -398,7 +411,7 @@ impl ShedVi { while let Some(key) = self.reader.read_key()? { // If completer is active, delegate input to it if self.completer.is_active() { - self.print_line(false)?; + self.print_line(false)?; match self.completer.handle_key(key.clone())? { CompResponse::Accept(candidate) => { let span_start = self.completer.token_span().0; @@ -416,57 +429,58 @@ impl ShedVi { .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); let hint = self.history.get_hint(); self.editor.set_hint(hint); - self.completer.clear(&mut self.writer)?; - self.needs_redraw = true; - self.completer.reset(); - continue; + self.completer.clear(&mut self.writer)?; + self.needs_redraw = true; + self.completer.reset(); + continue; } CompResponse::Dismiss => { let hint = self.history.get_hint(); self.editor.set_hint(hint); - self.completer.clear(&mut self.writer)?; - self.completer.reset(); - continue; + self.completer.clear(&mut self.writer)?; + self.completer.reset(); + continue; } CompResponse::Consumed => { - /* just redraw */ - self.needs_redraw = true; - continue; - } + /* just redraw */ + self.needs_redraw = true; + continue; + } CompResponse::Passthrough => { /* fall through to normal handling below */ } } - } else { - let keymap_flags = self.curr_keymap_flags(); - self.pending_keymap.push(key.clone()); - - let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap)); - if matches.is_empty() { - // No matches. Drain the buffered keys and execute them. - for key in std::mem::take(&mut self.pending_keymap) { - if let Some(event) = self.handle_key(key)? { - return Ok(event); - } - } - self.needs_redraw = true; - continue; - } else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact { - // We have a single exact match. Execute it. - let keymap = matches[0].clone(); - self.pending_keymap.clear(); - let action = keymap.action_expanded(); - for key in action { - if let Some(event) = self.handle_key(key)? { - return Ok(event); - } - } - self.needs_redraw = true; - continue; - } else { - // There is ambiguity. Allow the timeout in the main loop to handle this. - continue; - } - } + } else { + let keymap_flags = self.curr_keymap_flags(); + self.pending_keymap.push(key.clone()); + let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap)); + if matches.is_empty() { + // No matches. Drain the buffered keys and execute them. + for key in std::mem::take(&mut self.pending_keymap) { + if let Some(event) = self.handle_key(key)? { + return Ok(event); + } + } + self.needs_redraw = true; + continue; + } else if matches.len() == 1 + && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact + { + // We have a single exact match. Execute it. + let keymap = matches[0].clone(); + self.pending_keymap.clear(); + let action = keymap.action_expanded(); + for key in action { + if let Some(event) = self.handle_key(key)? { + return Ok(event); + } + } + self.needs_redraw = true; + continue; + } else { + // There is ambiguity. Allow the timeout in the main loop to handle this. + continue; + } + } if let Some(event) = self.handle_key(key)? { return Ok(event); @@ -542,12 +556,13 @@ impl ShedVi { 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; - } + 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 { // it's an ex mode error @@ -567,9 +582,10 @@ impl ShedVi { } if cmd.is_submit_action() - && !self.next_is_escaped - && !self.editor.buffer.ends_with('\\') - && (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.cursor.set(self.editor.cursor_max()); self.print_line(true)?; @@ -600,11 +616,11 @@ impl ShedVi { let before = self.editor.buffer.clone(); self.exec_cmd(cmd)?; - if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) { - for key in keys { - self.handle_key(key)?; - } - } + 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(); if before != after { @@ -670,7 +686,7 @@ impl ShedVi { || (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() && matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE))) } - ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false, + ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false, } } else { false @@ -692,13 +708,15 @@ impl ShedVi { pub fn line_text(&mut self) -> String { let line = self.editor.to_string(); let hint = self.editor.get_hint_text(); - let do_hl = state::read_shopts(|s| s.prompt.highlight); - self.highlighter.only_visual(!do_hl); - self.highlighter.load_input(&line, self.editor.cursor_byte_pos()); - self.highlighter.expand_control_chars(); - self.highlighter.highlight(); - let highlighted = self.highlighter.take(); - format!("{highlighted}{hint}") + let do_hl = state::read_shopts(|s| s.prompt.highlight); + self.highlighter.only_visual(!do_hl); + self + .highlighter + .load_input(&line, self.editor.cursor_byte_pos()); + self.highlighter.expand_control_chars(); + self.highlighter.highlight(); + let highlighted = self.highlighter.take(); + format!("{highlighted}{hint}") } pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { @@ -716,7 +734,7 @@ impl ShedVi { prompt_string_right = prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string()); } - let mut buf = String::new(); + let mut buf = String::new(); let row0_used = self .prompt @@ -734,8 +752,8 @@ impl ShedVi { self.writer.clear_rows(layout)?; } - let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt)); - pre_prompt.exec(); + let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt)); + pre_prompt.exec(); self .writer @@ -753,7 +771,8 @@ impl ShedVi { && !seq.is_empty() && !(prompt_string_right.is_some() && one_line) && seq_fits - && self.mode.report_mode() != ModeReport::Ex { + && self.mode.report_mode() != ModeReport::Ex + { let to_col = self.writer.t_cols - calc_str_width(&seq); let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt @@ -765,10 +784,11 @@ impl ShedVi { // Save cursor, move up to top row, move right to column, write sequence, // restore cursor - write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap(); + write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap(); } else if !final_draw && let Some(psr) = prompt_string_right - && psr_fits { + && psr_fits + { let to_col = self.writer.t_cols - calc_str_width(&psr); let down = new_layout.end.row - new_layout.cursor.row; let move_down = if down > 0 { @@ -781,19 +801,28 @@ impl ShedVi { // Record where the PSR ends so clear_rows can account for wrapping // if the terminal shrinks. - let psr_start = Pos { row: new_layout.end.row, col: to_col }; - new_layout.psr_end = Some(Layout::calc_pos(self.writer.t_cols, &psr, psr_start, 0, false)); + let psr_start = Pos { + row: new_layout.end.row, + col: to_col, + }; + new_layout.psr_end = Some(Layout::calc_pos( + self.writer.t_cols, + &psr, + psr_start, + 0, + false, + )); } - if let ModeReport::Ex = self.mode.report_mode() { - let pending_seq = self.mode.pending_seq().unwrap_or_default(); - write!(buf, "\n: {pending_seq}").unwrap(); - new_layout.end.row += 1; - } + if let ModeReport::Ex = self.mode.report_mode() { + let pending_seq = self.mode.pending_seq().unwrap_or_default(); + write!(buf, "\n: {pending_seq}").unwrap(); + new_layout.end.row += 1; + } write!(buf, "{}", &self.mode.cursor_style()).unwrap(); - self.writer.flush_write(&buf)?; + self.writer.flush_write(&buf)?; // Tell the completer the width of the prompt line above its \n so it can // account for wrapping when clearing after a resize. @@ -803,30 +832,39 @@ impl ShedVi { // Without PSR, use the content width on the cursor's row (new_layout.end.col + 1).max(new_layout.cursor.col + 1) }; - self.completer.set_prompt_line_context(preceding_width, new_layout.cursor.col); + self + .completer + .set_prompt_line_context(preceding_width, new_layout.cursor.col); self.completer.draw(&mut self.writer)?; self.old_layout = Some(new_layout); self.needs_redraw = false; - let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt)); - post_prompt.exec(); + let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt)); + post_prompt.exec(); Ok(()) } - pub fn swap_mode(&mut self, mode: &mut Box) { - let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange)); - pre_mode_change.exec(); + pub fn swap_mode(&mut self, mode: &mut Box) { + let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange)); + pre_mode_change.exec(); - std::mem::swap(&mut self.mode, mode); - self.editor.set_cursor_clamp(self.mode.clamp_cursor()); - write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok(); - self.prompt.refresh(); + std::mem::swap(&mut self.mode, mode); + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + }) + .ok(); + self.prompt.refresh(); - let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange)); - post_mode_change.exec(); - } + let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange)); + post_mode_change.exec(); + } pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { let mut select_mode = None; @@ -834,63 +872,72 @@ impl ShedVi { if cmd.is_mode_transition() { let count = cmd.verb_count(); - let mut mode: Box = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { - if let Some(saved) = self.saved_mode.take() { - saved - } else { - Box::new(ViNormal::new()) - } - } else { - match cmd.verb().unwrap().1 { - Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { - is_insert_mode = true; - Box::new(ViInsert::new().with_count(count as u16)) - } + let mut mode: Box = if matches!( + self.mode.report_mode(), + ModeReport::Ex | ModeReport::Verbatim + ) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) + { + if let Some(saved) = self.saved_mode.take() { + saved + } else { + Box::new(ViNormal::new()) + } + } else { + match cmd.verb().unwrap().1 { + Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { + is_insert_mode = true; + Box::new(ViInsert::new().with_count(count as u16)) + } - Verb::ExMode => { - Box::new(ViEx::new()) - } + Verb::ExMode => Box::new(ViEx::new()), - Verb::VerbatimMode => { - Box::new(ViVerbatim::new().with_count(count as u16)) - } + Verb::VerbatimMode => Box::new(ViVerbatim::new().with_count(count as u16)), - Verb::NormalMode => Box::new(ViNormal::new()), + Verb::NormalMode => Box::new(ViNormal::new()), - Verb::ReplaceMode => Box::new(ViReplace::new()), + Verb::ReplaceMode => Box::new(ViReplace::new()), - Verb::VisualModeSelectLast => { - if self.mode.report_mode() != ModeReport::Visual { - self - .editor - .start_selecting(SelectMode::Char(SelectAnchor::End)); - } - let mut mode: Box = Box::new(ViVisual::new()); - self.swap_mode(&mut mode); + Verb::VisualModeSelectLast => { + if self.mode.report_mode() != ModeReport::Visual { + self + .editor + .start_selecting(SelectMode::Char(SelectAnchor::End)); + } + let mut mode: Box = Box::new(ViVisual::new()); + self.swap_mode(&mut mode); - return self.editor.exec_cmd(cmd); - } - Verb::VisualMode => { - select_mode = Some(SelectMode::Char(SelectAnchor::End)); - Box::new(ViVisual::new()) - } - Verb::VisualModeLine => { - select_mode = Some(SelectMode::Line(SelectAnchor::End)); - Box::new(ViVisual::new()) - } + return self.editor.exec_cmd(cmd); + } + Verb::VisualMode => { + select_mode = Some(SelectMode::Char(SelectAnchor::End)); + Box::new(ViVisual::new()) + } + Verb::VisualModeLine => { + select_mode = Some(SelectMode::Line(SelectAnchor::End)); + Box::new(ViVisual::new()) + } - _ => unreachable!(), - } - }; + _ => unreachable!(), + } + }; - self.swap_mode(&mut mode); + self.swap_mode(&mut mode); - if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) { - self.saved_mode = Some(mode); - write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; - self.prompt.refresh(); - return Ok(()); - } + if matches!( + self.mode.report_mode(), + ModeReport::Ex | ModeReport::Verbatim + ) { + self.saved_mode = Some(mode); + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + })?; + self.prompt.refresh(); + return Ok(()); + } if mode.is_repeatable() { self.repeat_action = mode.as_replay(); @@ -912,9 +959,14 @@ impl ShedVi { self.editor.clear_insert_mode_start_pos(); } - write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; - self.prompt.refresh(); - + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + })?; + self.prompt.refresh(); return Ok(()); } else if cmd.is_cmd_repeat() { @@ -989,22 +1041,21 @@ impl ShedVi { } } - if self.mode.report_mode() == ModeReport::Visual - && self.editor.select_range().is_none() { - self.editor.stop_selecting(); - let mut mode: Box = Box::new(ViNormal::new()); - self.swap_mode(&mut mode); - } + if self.mode.report_mode() == ModeReport::Visual && self.editor.select_range().is_none() { + self.editor.stop_selecting(); + let mut mode: Box = Box::new(ViNormal::new()); + self.swap_mode(&mut mode); + } if cmd.is_repeatable() { if self.mode.report_mode() == ModeReport::Visual { // 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 if let Some(range) = self.editor.select_range() { - cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) - } else { - log::warn!("You're in visual mode with no select range??"); - }; + 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())); } @@ -1018,24 +1069,27 @@ impl ShedVi { if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { self.editor.stop_selecting(); let mut mode: Box = Box::new(ViNormal::new()); - self.swap_mode(&mut mode); + self.swap_mode(&mut mode); } - if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() { - self.editor.stop_selecting(); - } + 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) { - let mut mode: Box = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) { - if let Some(saved) = self.saved_mode.take() { - saved - } else { - Box::new(ViNormal::new()) - } - } else { - Box::new(ViNormal::new()) - }; - self.swap_mode(&mut mode); + let mut mode: Box = if matches!( + self.mode.report_mode(), + ModeReport::Ex | ModeReport::Verbatim + ) { + if let Some(saved) = self.saved_mode.take() { + saved + } else { + Box::new(ViNormal::new()) + } + } else { + Box::new(ViNormal::new()) + }; + self.swap_mode(&mut mode); } Ok(()) @@ -1069,7 +1123,6 @@ pub fn annotate_input(input: &str) -> String { .filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null)) .collect(); - for tk in tokens.into_iter().rev() { let insertions = annotate_token(tk); for (pos, marker) in insertions { @@ -1112,9 +1165,7 @@ pub fn annotate_input_recursive(input: &str) -> String { markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) { Some('>') => ">(", Some('<') => "<(", - _ => { - "<(" - } + _ => "<(", }, markers::CMD_SUB => "$(", markers::SUBSH => "(", @@ -1256,7 +1307,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { let mut insertions: Vec<(usize, Marker)> = vec![]; if token.class != TkRule::Str - && let Some(marker) = marker_for(&token.class) { + && let Some(marker) = marker_for(&token.class) + { insertions.push((token.span.range().end, markers::RESET)); insertions.push((token.span.range().start, marker)); return insertions; @@ -1279,7 +1331,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { let span_start = token.span.range().start; - let mut qt_state = QuoteState::default(); + let mut qt_state = QuoteState::default(); let mut cmd_sub_depth = 0; let mut proc_sub_depth = 0; @@ -1350,7 +1402,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { || *br_ch == '=' || *br_ch == '/' // parameter expansion symbols || *br_ch == '?' - || *br_ch == '$' // we're in some expansion like $foo$bar or ${foo$bar} + || *br_ch == '$' + // we're in some expansion like $foo$bar or ${foo$bar} { token_chars.next(); } else if *br_ch == '}' { @@ -1370,8 +1423,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { // consume the var name while let Some((cur_i, var_ch)) = token_chars.peek() { if var_ch.is_ascii_alphanumeric() - || ShellParam::from_char(var_ch).is_some() - || *var_ch == '_' { + || ShellParam::from_char(var_ch).is_some() + || *var_ch == '_' + { end_pos = *cur_i + 1; token_chars.next(); } else { @@ -1397,12 +1451,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { token_chars.next(); // consume the escaped char } } - '\\' if qt_state.in_single() => { - token_chars.next(); - if let Some(&(_,'\'')) = token_chars.peek() { - token_chars.next(); // consume the escaped single quote - } - } + '\\' if qt_state.in_single() => { + token_chars.next(); + if let Some(&(_, '\'')) = token_chars.peek() { + token_chars.next(); // consume the escaped single quote + } + } '<' | '>' if !qt_state.in_quote() && cmd_sub_depth == 0 && proc_sub_depth == 0 => { token_chars.next(); if let Some((_, proc_sub_ch)) = token_chars.peek() @@ -1421,7 +1475,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { } else { insertions.push((span_start + *i, markers::STRING_DQ)); } - qt_state.toggle_double(); + qt_state.toggle_double(); token_chars.next(); // consume the quote } '\'' if !qt_state.in_double() => { @@ -1430,7 +1484,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { } else { insertions.push((span_start + *i, markers::STRING_SQ)); } - qt_state.toggle_single(); + qt_state.toggle_single(); token_chars.next(); // consume the quote } '[' if !qt_state.in_quote() && !token.flags.contains(TkFlags::ASSIGN) => { diff --git a/src/readline/term.rs b/src/readline/term.rs index ab6b794..ba874a6 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -3,7 +3,8 @@ use std::{ env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, - os::fd::{AsFd, BorrowedFd, RawFd}, sync::Arc, + os::fd::{AsFd, BorrowedFd, RawFd}, + sync::Arc, }; use nix::{ @@ -17,14 +18,12 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use vte::{Parser, Perform}; pub use crate::libsh::guards::{RawModeGuard, raw_mode}; +use crate::state::{read_meta, write_meta}; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, readline::keys::{KeyCode, ModKeys}, state::read_shopts, }; -use crate::{ - state::{read_meta, write_meta}, -}; use super::keys::KeyEvent; @@ -165,13 +164,13 @@ fn width(s: &str, esc_seq: &mut u8) -> u16 { 0 } else if *esc_seq == 2 { if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { - /*} else if s == "m" { - // last - *esc_seq = 0;*/ - } else { - // not supported - *esc_seq = 0; - } + /*} else if s == "m" { + // last + *esc_seq = 0;*/ + } else { + // not supported + *esc_seq = 0; + } 0 } else if s == "\x1b" { @@ -457,27 +456,27 @@ impl Perform for KeyCollector { }; KeyEvent(key, mods) } - ([],'u') => { - let codepoint = params.first().copied().unwrap_or(0); - let mods = params - .get(1) - .map(|&m| Self::parse_modifiers(m)) - .unwrap_or(ModKeys::empty()); - let key = match codepoint { - 9 => KeyCode::Tab, - 13 => KeyCode::Enter, - 27 => KeyCode::Esc, - 127 => KeyCode::Backspace, - _ => { - if let Some(ch) = char::from_u32(codepoint as u32) { - KeyCode::Char(ch) - } else { - return - } - } - }; - KeyEvent(key, mods) - } + ([], 'u') => { + let codepoint = params.first().copied().unwrap_or(0); + let mods = params + .get(1) + .map(|&m| Self::parse_modifiers(m)) + .unwrap_or(ModKeys::empty()); + let key = match codepoint { + 9 => KeyCode::Tab, + 13 => KeyCode::Enter, + 27 => KeyCode::Esc, + 127 => KeyCode::Backspace, + _ => { + if let Some(ch) = char::from_u32(codepoint as u32) { + KeyCode::Char(ch) + } else { + return; + } + } + }; + KeyEvent(key, mods) + } // SGR mouse: CSI < button;x;y M/m (ignore mouse events for now) ([b'<'], 'M') | ([b'<'], 'm') => { return; @@ -516,18 +515,21 @@ impl PollReader { } pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) { - if verbatim { - let seq = String::from_utf8_lossy(bytes).to_string(); - self.collector.push(KeyEvent(KeyCode::Verbatim(Arc::from(seq.as_str())), ModKeys::empty())); - } else if bytes == [b'\x1b'] { + if verbatim { + let seq = String::from_utf8_lossy(bytes).to_string(); + self.collector.push(KeyEvent( + KeyCode::Verbatim(Arc::from(seq.as_str())), + ModKeys::empty(), + )); + } else if bytes == [b'\x1b'] { // Single escape byte - user pressed ESC key self .collector .push(KeyEvent(KeyCode::Esc, ModKeys::empty())); } else { - // Feed all bytes through vte parser - self.parser.advance(&mut self.collector, bytes); - } + // Feed all bytes through vte parser + self.parser.advance(&mut self.collector, bytes); + } } } @@ -748,13 +750,9 @@ impl Layout { } } - fn is_ctl_char(gr: &str) -> bool { - gr.len() > 0 && - gr.as_bytes()[0] <= 0x1F && - gr != "\n" && - gr != "\t" && - gr != "\r" - } + fn is_ctl_char(gr: &str) -> bool { + !gr.is_empty() && gr.as_bytes()[0] <= 0x1F && gr != "\n" && gr != "\t" && gr != "\r" + } pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16, raw_calc: bool) -> Pos { const TAB_STOP: u16 = 8; @@ -767,8 +765,8 @@ impl Layout { } let c_width = if c == "\t" { TAB_STOP - (pos.col % TAB_STOP) - } else if raw_calc && Self::is_ctl_char(c) { - 2 + } else if raw_calc && Self::is_ctl_char(c) { + 2 } else { width(c, &mut esc_seq) }; @@ -867,21 +865,21 @@ impl TermWriter { self.t_cols = t_cols; } - /// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'. - /// - /// Aping zsh with this but it's a nice feature. - pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> { - let Some((_,c)) = self.get_cursor_pos(rdr)? else { - return Ok(()); - }; + /// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'. + /// + /// Aping zsh with this but it's a nice feature. + pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> { + let Some((_, c)) = self.get_cursor_pos(rdr)? else { + return Ok(()); + }; - if c != 1 { - self.flush_write("\x1b[7m%\x1b[0m\n\r")?; - } - Ok(()) - } + if c != 1 { + self.flush_write("\x1b[7m%\x1b[0m\n\r")?; + } + Ok(()) + } - pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult> { + pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult> { // Ping the cursor's position self.flush_write("\x1b[6n")?; @@ -900,14 +898,16 @@ impl TermWriter { let row = read_digits_until(rdr, ';')?; let col = read_digits_until(rdr, 'R')?; - let pos = if let Some(row) = row && let Some(col) = col { - Some((row as usize, col as usize)) - } else { - None - }; + let pos = if let Some(row) = row + && let Some(col) = col + { + Some((row as usize, col as usize)) + } else { + None + }; - Ok(pos) - } + Ok(pos) + } pub fn move_cursor_at_leftmost( &mut self, @@ -996,7 +996,7 @@ impl LineWriter for TermWriter { ) }; self.buffer.clear(); - self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt + self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt let end = new_layout.end; let cursor = new_layout.cursor; diff --git a/src/readline/vicmd.rs b/src/readline/vicmd.rs index bce7756..8763f36 100644 --- a/src/readline/vicmd.rs +++ b/src/readline/vicmd.rs @@ -178,8 +178,8 @@ impl ViCmd { matches!( v.1, Verb::Change - | Verb::VerbatimMode - | Verb::ExMode + | Verb::VerbatimMode + | Verb::ExMode | Verb::InsertMode | Verb::InsertModeLineBreak(_) | Verb::NormalMode @@ -221,8 +221,8 @@ pub enum Verb { ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace ToggleCaseInplace(u16), // Number of chars to toggle ToggleCaseRange, - IncrementNumber(u16), - DecrementNumber(u16), + IncrementNumber(u16), + DecrementNumber(u16), ToLower, ToUpper, Complete, @@ -232,7 +232,7 @@ pub enum Verb { RepeatLast, Put(Anchor), ReplaceMode, - VerbatimMode, + VerbatimMode, InsertMode, InsertModeLineBreak(Anchor), NormalMode, @@ -303,8 +303,8 @@ impl Verb { | Self::Insert(_) | Self::Rot13 | Self::EndOfFile - | Self::IncrementNumber(_) - | Self::DecrementNumber(_) + | Self::IncrementNumber(_) + | Self::DecrementNumber(_) ) } pub fn is_char_insert(&self) -> bool { diff --git a/src/readline/vimode/ex.rs b/src/readline/vimode/ex.rs index 2dda7f9..03843e7 100644 --- a/src/readline/vimode/ex.rs +++ b/src/readline/vimode/ex.rs @@ -9,373 +9,399 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::readline::keys::KeyEvent; use crate::readline::linebuf::LineBuf; use crate::readline::vicmd::{ - Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd, - ViCmd, WriteDest, + Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd, ViCmd, + WriteDest, }; use crate::readline::vimode::{ModeReport, ViInsert, ViMode}; use crate::state::write_meta; bitflags! { - #[derive(Debug,Clone,Copy,PartialEq,Eq)] - pub struct SubFlags: u16 { - const GLOBAL = 1 << 0; // g - const CONFIRM = 1 << 1; // c (probably not implemented) - const IGNORE_CASE = 1 << 2; // i - const NO_IGNORE_CASE = 1 << 3; // I - const SHOW_COUNT = 1 << 4; // n - const PRINT_RESULT = 1 << 5; // p - const PRINT_NUMBERED = 1 << 6; // # - const PRINT_LEFT_ALIGN = 1 << 7; // l - } + #[derive(Debug,Clone,Copy,PartialEq,Eq)] + pub struct SubFlags: u16 { + const GLOBAL = 1 << 0; // g + const CONFIRM = 1 << 1; // c (probably not implemented) + const IGNORE_CASE = 1 << 2; // i + const NO_IGNORE_CASE = 1 << 3; // I + const SHOW_COUNT = 1 << 4; // n + const PRINT_RESULT = 1 << 5; // p + const PRINT_NUMBERED = 1 << 6; // # + const PRINT_LEFT_ALIGN = 1 << 7; // l + } } - #[derive(Default, Clone, Debug)] struct ExEditor { - buf: LineBuf, - mode: ViInsert + buf: LineBuf, + mode: ViInsert, } impl ExEditor { - pub fn clear(&mut self) { - *self = Self::default() - } - pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> { - let Some(cmd) = self.mode.handle_key(key) else { - return Ok(()) - }; - self.buf.exec_cmd(cmd) - } + pub fn clear(&mut self) { + *self = Self::default() + } + pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> { + let Some(cmd) = self.mode.handle_key(key) else { + return Ok(()); + }; + self.buf.exec_cmd(cmd) + } } - #[derive(Default, Clone, Debug)] pub struct ViEx { - pending_cmd: ExEditor, + pending_cmd: ExEditor, } impl ViEx { - pub fn new() -> Self { - Self::default() - } + pub fn new() -> Self { + Self::default() + } } impl ViMode for ViEx { - // 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> { - use crate::readline::keys::{KeyEvent as E, KeyCode as C, ModKeys as M}; - log::debug!("[ViEx] handle_key_fallible: key={:?}", key); - match key { - E(C::Char('\r'), M::NONE) | - E(C::Enter, M::NONE) => { - let input = self.pending_cmd.buf.as_str(); - log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input); - match parse_ex_cmd(input) { - Ok(cmd) => { - log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd); - Ok(cmd) - } - Err(e) => { - log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e); - let msg = e.unwrap_or(format!("Not an editor command: {}", input)); - write_meta(|m| m.post_system_message(msg.clone())); - Err(ShErr::simple(ShErrKind::ParseErr, msg)) - } - } - } - E(C::Char('C'), M::CTRL) => { - log::debug!("[ViEx] Ctrl-C, clearing"); - self.pending_cmd.clear(); - Ok(None) - } - E(C::Esc, M::NONE) => { - log::debug!("[ViEx] Esc, returning to normal mode"); - Ok(Some(ViCmd { - register: RegisterName::default(), - verb: Some(VerbCmd(1, Verb::NormalMode)), - motion: None, - flags: CmdFlags::empty(), - raw_seq: "".into(), - })) - } - _ => { - log::debug!("[ViEx] forwarding key to ExEditor"); - self.pending_cmd.handle_key(key).map(|_| None) - } - } - } - fn handle_key(&mut self, key: KeyEvent) -> Option { - let result = self.handle_key_fallible(key); - log::debug!("[ViEx] handle_key result: {:?}", result); - result.ok().flatten() - } - fn is_repeatable(&self) -> bool { - false - } + // 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> { + use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M}; + log::debug!("[ViEx] handle_key_fallible: key={:?}", key); + match key { + E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => { + let input = self.pending_cmd.buf.as_str(); + log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input); + match parse_ex_cmd(input) { + Ok(cmd) => { + log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd); + Ok(cmd) + } + Err(e) => { + log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e); + let msg = e.unwrap_or(format!("Not an editor command: {}", input)); + write_meta(|m| m.post_system_message(msg.clone())); + Err(ShErr::simple(ShErrKind::ParseErr, msg)) + } + } + } + E(C::Char('C'), M::CTRL) => { + log::debug!("[ViEx] Ctrl-C, clearing"); + self.pending_cmd.clear(); + Ok(None) + } + E(C::Esc, M::NONE) => { + log::debug!("[ViEx] Esc, returning to normal mode"); + Ok(Some(ViCmd { + register: RegisterName::default(), + verb: Some(VerbCmd(1, Verb::NormalMode)), + motion: None, + flags: CmdFlags::empty(), + raw_seq: "".into(), + })) + } + _ => { + log::debug!("[ViEx] forwarding key to ExEditor"); + self.pending_cmd.handle_key(key).map(|_| None) + } + } + } + fn handle_key(&mut self, key: KeyEvent) -> Option { + let result = self.handle_key_fallible(key); + log::debug!("[ViEx] handle_key result: {:?}", result); + result.ok().flatten() + } + fn is_repeatable(&self) -> bool { + false + } - fn as_replay(&self) -> Option { - None - } + fn as_replay(&self) -> Option { + None + } - fn cursor_style(&self) -> String { - "\x1b[3 q".to_string() - } + fn cursor_style(&self) -> String { + "\x1b[3 q".to_string() + } - fn pending_seq(&self) -> Option { - Some(self.pending_cmd.buf.as_str().to_string()) - } + fn pending_seq(&self) -> Option { + Some(self.pending_cmd.buf.as_str().to_string()) + } - fn pending_cursor(&self) -> Option { - Some(self.pending_cmd.buf.cursor.get()) - } + fn pending_cursor(&self) -> Option { + Some(self.pending_cmd.buf.cursor.get()) + } - fn move_cursor_on_undo(&self) -> bool { - false - } + fn move_cursor_on_undo(&self) -> bool { + false + } - fn clamp_cursor(&self) -> bool { - true - } + fn clamp_cursor(&self) -> bool { + true + } - fn hist_scroll_start_pos(&self) -> Option { - None - } + fn hist_scroll_start_pos(&self) -> Option { + None + } - fn report_mode(&self) -> super::ModeReport { - ModeReport::Ex - } + fn report_mode(&self) -> super::ModeReport { + ModeReport::Ex + } } -fn parse_ex_cmd(raw: &str) -> Result,Option> { - let raw = raw.trim(); - if raw.is_empty() { - return Ok(None) - } - let mut chars = raw.chars().peekable(); - let (verb, motion) = { - if chars.peek() == Some(&'g') { - let mut cmd_name = String::new(); - while let Some(ch) = chars.peek() { - if ch.is_alphanumeric() { - cmd_name.push(*ch); - chars.next(); - } else { - break - } - } - if !"global".starts_with(&cmd_name) { - return Err(None) - } - let Some(result) = parse_global(&mut chars)? else { return Ok(None) }; - (Some(VerbCmd(1,result.1)), Some(MotionCmd(1,result.0))) - } else { - (parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None) - } - }; +fn parse_ex_cmd(raw: &str) -> Result, Option> { + let raw = raw.trim(); + if raw.is_empty() { + return Ok(None); + } + let mut chars = raw.chars().peekable(); + let (verb, motion) = { + if chars.peek() == Some(&'g') { + let mut cmd_name = String::new(); + while let Some(ch) = chars.peek() { + if ch.is_alphanumeric() { + cmd_name.push(*ch); + chars.next(); + } else { + break; + } + } + if !"global".starts_with(&cmd_name) { + return Err(None); + } + let Some(result) = parse_global(&mut chars)? else { + return Ok(None); + }; + (Some(VerbCmd(1, result.1)), Some(MotionCmd(1, result.0))) + } else { + (parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None) + } + }; - Ok(Some(ViCmd { - register: RegisterName::default(), - verb, - motion, - raw_seq: raw.to_string(), - flags: CmdFlags::EXIT_CUR_MODE, - })) + Ok(Some(ViCmd { + register: RegisterName::default(), + verb, + motion, + raw_seq: raw.to_string(), + flags: CmdFlags::EXIT_CUR_MODE, + })) } /// Unescape shell command arguments fn unescape_shell_cmd(cmd: &str) -> String { - // The pest grammar uses double quotes for vicut commands - // So shell commands need to escape double quotes - // We will be removing a single layer of escaping from double quotes - let mut result = String::new(); - let mut chars = cmd.chars().peekable(); - while let Some(ch) = chars.next() { - if ch == '\\' { - if let Some(&'"') = chars.peek() { - chars.next(); - result.push('"'); - } else { - result.push(ch); - } - } else { - result.push(ch); - } - } - result + // The pest grammar uses double quotes for vicut commands + // So shell commands need to escape double quotes + // We will be removing a single layer of escaping from double quotes + let mut result = String::new(); + let mut chars = cmd.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\\' { + if let Some(&'"') = chars.peek() { + chars.next(); + result.push('"'); + } else { + result.push(ch); + } + } else { + result.push(ch); + } + } + result } -fn parse_ex_command(chars: &mut Peekable>) -> Result,Option> { - let mut cmd_name = String::new(); +fn parse_ex_command(chars: &mut Peekable>) -> Result, Option> { + let mut cmd_name = String::new(); - while let Some(ch) = chars.peek() { - if ch == &'!' { - cmd_name.push(*ch); - chars.next(); - break - } else if !ch.is_alphanumeric() { - break - } - cmd_name.push(*ch); - chars.next(); - } + while let Some(ch) = chars.peek() { + if ch == &'!' { + cmd_name.push(*ch); + chars.next(); + break; + } else if !ch.is_alphanumeric() { + break; + } + cmd_name.push(*ch); + chars.next(); + } - match cmd_name.as_str() { - "!" => { - let cmd = chars.collect::(); - let cmd = unescape_shell_cmd(&cmd); - Ok(Some(Verb::ShellCmd(cmd))) - } - "normal!" => parse_normal(chars), - _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), - _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), - _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), - _ if "read".starts_with(&cmd_name) => parse_read(chars), - _ if "write".starts_with(&cmd_name) => parse_write(chars), - _ if "substitute".starts_with(&cmd_name) => parse_substitute(chars), - _ => Err(None) - } + match cmd_name.as_str() { + "!" => { + let cmd = chars.collect::(); + let cmd = unescape_shell_cmd(&cmd); + Ok(Some(Verb::ShellCmd(cmd))) + } + "normal!" => parse_normal(chars), + _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), + _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), + _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), + _ if "read".starts_with(&cmd_name) => parse_read(chars), + _ if "write".starts_with(&cmd_name) => parse_write(chars), + _ if "substitute".starts_with(&cmd_name) => parse_substitute(chars), + _ => Err(None), + } } -fn parse_normal(chars: &mut Peekable>) -> Result,Option> { - chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); +fn parse_normal(chars: &mut Peekable>) -> Result, Option> { + chars + .peeking_take_while(|c| c.is_whitespace()) + .for_each(drop); - let seq: String = chars.collect(); - Ok(Some(Verb::Normal(seq))) + let seq: String = chars.collect(); + Ok(Some(Verb::Normal(seq))) } -fn parse_read(chars: &mut Peekable>) -> Result,Option> { - chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); +fn parse_read(chars: &mut Peekable>) -> Result, Option> { + chars + .peeking_take_while(|c| c.is_whitespace()) + .for_each(drop); - let is_shell_read = if chars.peek() == Some(&'!') { chars.next(); true } else { false }; - let arg: String = chars.collect(); + let is_shell_read = if chars.peek() == Some(&'!') { + chars.next(); + true + } else { + false + }; + let arg: String = chars.collect(); - if arg.trim().is_empty() { - return Err(Some("Expected file path or shell command after ':r'".into())) - } + if arg.trim().is_empty() { + return Err(Some( + "Expected file path or shell command after ':r'".into(), + )); + } - if is_shell_read { - Ok(Some(Verb::Read(ReadSrc::Cmd(arg)))) - } else { - let arg_path = get_path(arg.trim()); - Ok(Some(Verb::Read(ReadSrc::File(arg_path)))) - } + if is_shell_read { + Ok(Some(Verb::Read(ReadSrc::Cmd(arg)))) + } else { + let arg_path = get_path(arg.trim()); + Ok(Some(Verb::Read(ReadSrc::File(arg_path)))) + } } fn get_path(path: &str) -> PathBuf { - if let Some(stripped) = path.strip_prefix("~/") - && let Some(home) = std::env::var_os("HOME") { - return PathBuf::from(home).join(stripped) - } - if path == "~" - && let Some(home) = std::env::var_os("HOME") { - return PathBuf::from(home) - } - PathBuf::from(path) + if let Some(stripped) = path.strip_prefix("~/") + && let Some(home) = std::env::var_os("HOME") + { + return PathBuf::from(home).join(stripped); + } + if path == "~" + && let Some(home) = std::env::var_os("HOME") + { + return PathBuf::from(home); + } + PathBuf::from(path) } -fn parse_write(chars: &mut Peekable>) -> Result,Option> { - chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); +fn parse_write(chars: &mut Peekable>) -> Result, Option> { + chars + .peeking_take_while(|c| c.is_whitespace()) + .for_each(drop); - let is_shell_write = chars.peek() == Some(&'!'); - if is_shell_write { - chars.next(); // consume '!' - let arg: String = chars.collect(); - return Ok(Some(Verb::Write(WriteDest::Cmd(arg)))); - } + let is_shell_write = chars.peek() == Some(&'!'); + if is_shell_write { + chars.next(); // consume '!' + let arg: String = chars.collect(); + return Ok(Some(Verb::Write(WriteDest::Cmd(arg)))); + } - // Check for >> - let mut append_check = chars.clone(); - let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>'); - if is_file_append { - *chars = append_check; - } + // Check for >> + let mut append_check = chars.clone(); + let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>'); + if is_file_append { + *chars = append_check; + } - let arg: String = chars.collect(); - let arg_path = get_path(arg.trim()); + let arg: String = chars.collect(); + let arg_path = get_path(arg.trim()); - let dest = if is_file_append { - WriteDest::FileAppend(arg_path) - } else { - WriteDest::File(arg_path) - }; + let dest = if is_file_append { + WriteDest::FileAppend(arg_path) + } else { + WriteDest::File(arg_path) + }; - Ok(Some(Verb::Write(dest))) + Ok(Some(Verb::Write(dest))) } -fn parse_global(chars: &mut Peekable>) -> Result,Option> { - let is_negated = if chars.peek() == Some(&'!') { chars.next(); true } else { false }; +fn parse_global(chars: &mut Peekable>) -> Result, Option> { + let is_negated = if chars.peek() == Some(&'!') { + chars.next(); + true + } else { + false + }; - chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); // Ignore whitespace + chars + .peeking_take_while(|c| c.is_whitespace()) + .for_each(drop); // Ignore whitespace - let Some(delimiter) = chars.next() else { - return Ok(Some((Motion::Null,Verb::RepeatGlobal))) - }; - if delimiter.is_alphanumeric() { - return Err(None) - } - let global_pat = parse_pattern(chars, delimiter)?; - let Some(command) = parse_ex_command(chars)? else { - return Err(Some("Expected a command after global pattern".into())) - }; - if is_negated { - Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command))) - } else { - Ok(Some((Motion::Global(Val::new_str(global_pat)), command))) - } + let Some(delimiter) = chars.next() else { + return Ok(Some((Motion::Null, Verb::RepeatGlobal))); + }; + if delimiter.is_alphanumeric() { + return Err(None); + } + let global_pat = parse_pattern(chars, delimiter)?; + let Some(command) = parse_ex_command(chars)? else { + return Err(Some("Expected a command after global pattern".into())); + }; + if is_negated { + Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command))) + } else { + Ok(Some((Motion::Global(Val::new_str(global_pat)), command))) + } } -fn parse_substitute(chars: &mut Peekable>) -> Result,Option> { - while chars.peek().is_some_and(|c| c.is_whitespace()) { chars.next(); } // Ignore whitespace +fn parse_substitute(chars: &mut Peekable>) -> Result, Option> { + while chars.peek().is_some_and(|c| c.is_whitespace()) { + chars.next(); + } // Ignore whitespace - let Some(delimiter) = chars.next() else { - return Ok(Some(Verb::RepeatSubstitute)) - }; - if delimiter.is_alphanumeric() { - return Err(None) - } - let old_pat = parse_pattern(chars, delimiter)?; - let new_pat = parse_pattern(chars, delimiter)?; - let mut flags = SubFlags::empty(); - while let Some(ch) = chars.next() { - match ch { - 'g' => flags |= SubFlags::GLOBAL, - 'i' => flags |= SubFlags::IGNORE_CASE, - 'I' => flags |= SubFlags::NO_IGNORE_CASE, - 'n' => flags |= SubFlags::SHOW_COUNT, - _ => return Err(None) - } - } - Ok(Some(Verb::Substitute(old_pat, new_pat, flags))) + let Some(delimiter) = chars.next() else { + return Ok(Some(Verb::RepeatSubstitute)); + }; + if delimiter.is_alphanumeric() { + return Err(None); + } + let old_pat = parse_pattern(chars, delimiter)?; + let new_pat = parse_pattern(chars, delimiter)?; + let mut flags = SubFlags::empty(); + while let Some(ch) = chars.next() { + match ch { + 'g' => flags |= SubFlags::GLOBAL, + 'i' => flags |= SubFlags::IGNORE_CASE, + 'I' => flags |= SubFlags::NO_IGNORE_CASE, + 'n' => flags |= SubFlags::SHOW_COUNT, + _ => return Err(None), + } + } + Ok(Some(Verb::Substitute(old_pat, new_pat, flags))) } -fn parse_pattern(chars: &mut Peekable>, delimiter: char) -> Result> { - let mut pat = String::new(); - let mut closed = false; - while let Some(ch) = chars.next() { - match ch { - '\\' => { - if chars.peek().is_some_and(|c| *c == delimiter) { - // We escaped the delimiter, so we consume the escape char and continue - pat.push(chars.next().unwrap()); - continue - } else { - // The escape char is probably for the regex in the pattern - pat.push(ch); - if let Some(esc_ch) = chars.next() { - pat.push(esc_ch) - } - } - } - _ if ch == delimiter => { - closed = true; - break - } - _ => pat.push(ch) - } - } - if !closed { - Err(Some("Unclosed pattern in ex command".into())) - } else { - Ok(pat) - } +fn parse_pattern( + chars: &mut Peekable>, + delimiter: char, +) -> Result> { + let mut pat = String::new(); + let mut closed = false; + while let Some(ch) = chars.next() { + match ch { + '\\' => { + if chars.peek().is_some_and(|c| *c == delimiter) { + // We escaped the delimiter, so we consume the escape char and continue + pat.push(chars.next().unwrap()); + continue; + } else { + // The escape char is probably for the regex in the pattern + pat.push(ch); + if let Some(esc_ch) = chars.next() { + pat.push(esc_ch) + } + } + } + _ if ch == delimiter => { + closed = true; + break; + } + _ => pat.push(ch), + } + } + if !closed { + Err(Some("Unclosed pattern in ex command".into())) + } else { + Ok(pat) + } } diff --git a/src/readline/vimode/insert.rs b/src/readline/vimode/insert.rs index e3c3670..5cb4867 100644 --- a/src/readline/vimode/insert.rs +++ b/src/readline/vimode/insert.rs @@ -1,8 +1,6 @@ -use super::{common_cmds, CmdReplay, ModeReport, ViMode}; +use super::{CmdReplay, ModeReport, ViMode, common_cmds}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; -use crate::readline::vicmd::{ - Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word, -}; +use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word}; #[derive(Default, Clone, Debug)] pub struct ViInsert { diff --git a/src/readline/vimode/mod.rs b/src/readline/vimode/mod.rs index b77929a..35c64a0 100644 --- a/src/readline/vimode/mod.rs +++ b/src/readline/vimode/mod.rs @@ -4,47 +4,45 @@ use unicode_segmentation::UnicodeSegmentation; use crate::libsh::error::ShResult; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; -use crate::readline::vicmd::{ - Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, -}; +use crate::readline::vicmd::{Motion, MotionCmd, To, Verb, VerbCmd, ViCmd}; +pub mod ex; pub mod insert; pub mod normal; pub mod replace; -pub mod visual; -pub mod ex; pub mod verbatim; +pub mod visual; pub use ex::ViEx; pub use insert::ViInsert; pub use normal::ViNormal; pub use replace::ViReplace; -pub use visual::ViVisual; pub use verbatim::ViVerbatim; +pub use visual::ViVisual; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ModeReport { Insert, Normal, - Ex, + Ex, Visual, Replace, - Verbatim, + Verbatim, Unknown, } impl Display for ModeReport { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ModeReport::Insert => write!(f, "INSERT"), - ModeReport::Normal => write!(f, "NORMAL"), - ModeReport::Ex => write!(f, "COMMAND"), - ModeReport::Visual => write!(f, "VISUAL"), - ModeReport::Replace => write!(f, "REPLACE"), - ModeReport::Verbatim => write!(f, "VERBATIM"), - ModeReport::Unknown => write!(f, "UNKNOWN"), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModeReport::Insert => write!(f, "INSERT"), + ModeReport::Normal => write!(f, "NORMAL"), + ModeReport::Ex => write!(f, "COMMAND"), + ModeReport::Visual => write!(f, "VISUAL"), + ModeReport::Replace => write!(f, "REPLACE"), + ModeReport::Verbatim => write!(f, "VERBATIM"), + ModeReport::Unknown => write!(f, "UNKNOWN"), + } + } } #[derive(Debug, Clone)] @@ -73,13 +71,17 @@ pub enum CmdState { } pub trait ViMode { - fn handle_key_fallible(&mut self, key: E) -> ShResult> { Ok(self.handle_key(key)) } + fn handle_key_fallible(&mut self, key: E) -> ShResult> { + Ok(self.handle_key(key)) + } fn handle_key(&mut self, key: E) -> Option; fn is_repeatable(&self) -> bool; fn as_replay(&self) -> Option; fn cursor_style(&self) -> String; fn pending_seq(&self) -> Option; - fn pending_cursor(&self) -> Option { None } + fn pending_cursor(&self) -> Option { + None + } fn move_cursor_on_undo(&self) -> bool; fn clamp_cursor(&self) -> bool; fn hist_scroll_start_pos(&self) -> Option; diff --git a/src/readline/vimode/normal.rs b/src/readline/vimode/normal.rs index 051693f..42fe439 100644 --- a/src/readline/vimode/normal.rs +++ b/src/readline/vimode/normal.rs @@ -1,7 +1,7 @@ use std::iter::Peekable; use std::str::Chars; -use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode}; +use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::vicmd::{ Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, @@ -309,15 +309,15 @@ impl ViNormal { flags: self.flags(), }); } - ':' => { - return Some(ViCmd { - register, - verb: Some(VerbCmd(count, Verb::ExMode)), - motion: None, - raw_seq: self.take_cmd(), - flags: self.flags(), - }) - } + ':' => { + return Some(ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ExMode)), + motion: None, + raw_seq: self.take_cmd(), + flags: self.flags(), + }); + } 'i' => { return Some(ViCmd { register, @@ -724,7 +724,7 @@ impl ViNormal { } }; - let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later + let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later let verb_ref = verb.as_ref().map(|v| &v.1); let motion_ref = motion.as_ref().map(|m| &m.1); @@ -756,28 +756,32 @@ impl ViMode for ViNormal { raw_seq: "".into(), flags: self.flags(), }), - E(K::Char('A'), M::CTRL) => { - let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; - self.pending_seq.clear(); - Some(ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::IncrementNumber(count))), - motion: None, - raw_seq: "".into(), - flags: self.flags(), - }) - }, - E(K::Char('X'), M::CTRL) => { - let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; - self.pending_seq.clear(); - Some(ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::DecrementNumber(count))), - motion: None, - raw_seq: "".into(), - flags: self.flags(), - }) - }, + E(K::Char('A'), M::CTRL) => { + let count = self + .parse_count(&mut self.pending_seq.chars().peekable()) + .unwrap_or(1) as u16; + self.pending_seq.clear(); + Some(ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::IncrementNumber(count))), + motion: None, + raw_seq: "".into(), + flags: self.flags(), + }) + } + E(K::Char('X'), M::CTRL) => { + let count = self + .parse_count(&mut self.pending_seq.chars().peekable()) + .unwrap_or(1) as u16; + self.pending_seq.clear(); + Some(ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::DecrementNumber(count))), + motion: None, + raw_seq: "".into(), + flags: self.flags(), + }) + } E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Backspace, M::NONE) => Some(ViCmd { diff --git a/src/readline/vimode/replace.rs b/src/readline/vimode/replace.rs index b9c277c..30c41ef 100644 --- a/src/readline/vimode/replace.rs +++ b/src/readline/vimode/replace.rs @@ -1,8 +1,6 @@ -use super::{common_cmds, CmdReplay, ModeReport, ViMode}; +use super::{CmdReplay, ModeReport, ViMode, common_cmds}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; -use crate::readline::vicmd::{ - Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word, -}; +use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word}; #[derive(Default, Debug)] pub struct ViReplace { diff --git a/src/readline/vimode/verbatim.rs b/src/readline/vimode/verbatim.rs index b68d6ab..0673133 100644 --- a/src/readline/vimode/verbatim.rs +++ b/src/readline/vimode/verbatim.rs @@ -1,39 +1,40 @@ -use super::{common_cmds, CmdReplay, ModeReport, ViMode}; -use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; -use crate::readline::register::Register; -use crate::readline::vicmd::{ - CmdFlags, Direction, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd, Word -}; +use super::{CmdReplay, ModeReport, ViMode, common_cmds}; +use crate::readline::keys::{KeyCode as K, KeyEvent as E}; +use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd}; #[derive(Default, Clone, Debug)] pub struct ViVerbatim { - sent_cmd: Vec, - repeat_count: u16 + sent_cmd: Vec, + repeat_count: u16, } impl ViVerbatim { pub fn new() -> Self { Self::default() } - pub fn with_count(self, repeat_count: u16) -> Self { - Self { repeat_count, ..self } - } + pub fn with_count(self, repeat_count: u16) -> Self { + Self { + repeat_count, + ..self + } + } } impl ViMode for ViVerbatim { fn handle_key(&mut self, key: E) -> Option { match key { - E(K::Verbatim(seq),_mods) => { - log::debug!("Received verbatim key sequence: {:?}", seq); - let cmd = ViCmd { register: RegisterName::default(), - verb: Some(VerbCmd(1,Verb::Insert(seq.to_string()))), - motion: None, - raw_seq: seq.to_string(), - flags: CmdFlags::EXIT_CUR_MODE - }; - self.sent_cmd.push(cmd.clone()); - Some(cmd) - } + E(K::Verbatim(seq), _mods) => { + log::debug!("Received verbatim key sequence: {:?}", seq); + let cmd = ViCmd { + register: RegisterName::default(), + verb: Some(VerbCmd(1, Verb::Insert(seq.to_string()))), + motion: None, + raw_seq: seq.to_string(), + flags: CmdFlags::EXIT_CUR_MODE, + }; + self.sent_cmd.push(cmd.clone()); + Some(cmd) + } _ => common_cmds(key), } } diff --git a/src/readline/vimode/visual.rs b/src/readline/vimode/visual.rs index 18fca27..87799a5 100644 --- a/src/readline/vimode/visual.rs +++ b/src/readline/vimode/visual.rs @@ -1,7 +1,7 @@ use std::iter::Peekable; use std::str::Chars; -use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode}; +use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::vicmd::{ Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, @@ -129,15 +129,15 @@ impl ViVisual { flags: CmdFlags::empty(), }); } - ':' => { - return Some(ViCmd { - register, - verb: Some(VerbCmd(count, Verb::ExMode)), - motion: None, - raw_seq: self.take_cmd(), - flags: CmdFlags::empty(), - }) - } + ':' => { + return Some(ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ExMode)), + motion: None, + raw_seq: self.take_cmd(), + flags: CmdFlags::empty(), + }); + } 'x' => { chars = chars_clone; break 'verb_parse Some(VerbCmd(count, Verb::Delete)); @@ -581,7 +581,7 @@ impl ViVisual { } }; - let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later + let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later let verb_ref = verb.as_ref().map(|v| &v.1); let motion_ref = motion.as_ref().map(|m| &m.1); @@ -614,28 +614,32 @@ impl ViMode for ViVisual { raw_seq: "".into(), flags: CmdFlags::empty(), }), - E(K::Char('A'), M::CTRL) => { - let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; - self.pending_seq.clear(); - Some(ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::IncrementNumber(count))), - motion: None, - raw_seq: "".into(), - flags: CmdFlags::empty(), - }) - }, - E(K::Char('X'), M::CTRL) => { - let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; - self.pending_seq.clear(); - Some(ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::DecrementNumber(count))), - motion: None, - raw_seq: "".into(), - flags: CmdFlags::empty(), - }) - } + E(K::Char('A'), M::CTRL) => { + let count = self + .parse_count(&mut self.pending_seq.chars().peekable()) + .unwrap_or(1) as u16; + self.pending_seq.clear(); + Some(ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::IncrementNumber(count))), + motion: None, + raw_seq: "".into(), + flags: CmdFlags::empty(), + }) + } + E(K::Char('X'), M::CTRL) => { + let count = self + .parse_count(&mut self.pending_seq.chars().peekable()) + .unwrap_or(1) as u16; + self.pending_seq.clear(); + Some(ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::DecrementNumber(count))), + motion: None, + raw_seq: "".into(), + flags: CmdFlags::empty(), + }) + } E(K::Char('R'), M::CTRL) => { let mut chars = self.pending_seq.chars().peekable(); let count = self.parse_count(&mut chars).unwrap_or(1); diff --git a/src/shopt.rs b/src/shopt.rs index f76dd0e..be49352 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -104,12 +104,10 @@ impl ShOpts { "core" => self.core.set(&remainder, val)?, "prompt" => self.prompt.set(&remainder, val)?, _ => { - return Err( - ShErr::simple( - ShErrKind::SyntaxErr, - "shopt: expected 'core' or 'prompt' in shopt key", - ) - ); + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'core' or 'prompt' in shopt key", + )); } } Ok(()) @@ -129,12 +127,10 @@ impl ShOpts { match key { "core" => self.core.get(&remainder), "prompt" => self.prompt.get(&remainder), - _ => Err( - ShErr::simple( - ShErrKind::SyntaxErr, - "shopt: Expected 'core' or 'prompt' in shopt key", - ) - ), + _ => Err(ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: Expected 'core' or 'prompt' in shopt key", + )), } } } @@ -227,12 +223,10 @@ impl ShOptCore { self.max_recurse_depth = val; } _ => { - return Err( - ShErr::simple( - ShErrKind::SyntaxErr, - format!("shopt: Unexpected 'core' option '{opt}'"), - ) - ); + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'core' option '{opt}'"), + )); } } Ok(()) @@ -289,12 +283,10 @@ impl ShOptCore { output.push_str(&format!("{}", self.max_recurse_depth)); Ok(Some(output)) } - _ => Err( - ShErr::simple( - ShErrKind::SyntaxErr, - format!("shopt: Unexpected 'core' option '{query}'"), - ) - ), + _ => Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'core' option '{query}'"), + )), } } } @@ -344,6 +336,7 @@ pub struct ShOptPrompt { pub auto_indent: bool, pub linebreak_on_incomplete: bool, pub leader: String, + pub line_numbers: bool, } impl ShOptPrompt { @@ -406,16 +399,23 @@ impl ShOptPrompt { "leader" => { self.leader = val.to_string(); } + "line_numbers" => { + let Ok(val) = val.parse::() else { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for line_numbers value", + )); + }; + self.line_numbers = val; + } "custom" => { todo!() } _ => { - return Err( - ShErr::simple( - ShErrKind::SyntaxErr, - format!("shopt: Unexpected 'prompt' option '{opt}'"), - ) - ); + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'prompt' option '{opt}'"), + )); } } Ok(()) @@ -464,17 +464,19 @@ impl ShOptPrompt { Ok(Some(output)) } "leader" => { - let mut output = - String::from("The leader key sequence used in keymap bindings\n"); + let mut output = String::from("The leader key sequence used in keymap bindings\n"); output.push_str(&self.leader); Ok(Some(output)) } - _ => Err( - ShErr::simple( - ShErrKind::SyntaxErr, - format!("shopt: Unexpected 'prompt' option '{query}'"), - ) - ), + "line_numbers" => { + let mut output = String::from("Whether to display line numbers in multiline input\n"); + output.push_str(&format!("{}", self.line_numbers)); + Ok(Some(output)) + } + _ => Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'prompt' option '{query}'"), + )), } } } @@ -493,6 +495,7 @@ impl Display for ShOptPrompt { self.linebreak_on_incomplete )); output.push(format!("leader = {}", self.leader)); + output.push(format!("line_numbers = {}", self.line_numbers)); let final_output = output.join("\n"); @@ -510,6 +513,7 @@ impl Default for ShOptPrompt { auto_indent: true, linebreak_on_incomplete: true, leader: "\\".to_string(), + line_numbers: true, } } } diff --git a/src/signal.rs b/src/signal.rs index 9ff2f9c..3a5655d 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -148,11 +148,10 @@ pub fn sig_setup(is_login: bool) { sigaction(Signal::SIGSYS, &action).unwrap(); } - - if is_login { - let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); - take_term().ok(); - } + if is_login { + let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); + take_term().ok(); + } } /// Reset all signal dispositions to SIG_DFL. @@ -307,29 +306,30 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { { if is_fg { take_term()?; - } else { - JOB_DONE.store(true, Ordering::SeqCst); - let job_order = read_jobs(|j| j.order().to_vec()); - let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); - if let Some(job) = result { - let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); + } else { + JOB_DONE.store(true, Ordering::SeqCst); + let job_order = read_jobs(|j| j.order().to_vec()); + let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); + if let Some(job) = result { + let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); - let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish)); - for cmd in post_job_hooks { - let AutoCmd { pattern, command } = cmd; - if let Some(pat) = pattern - && job.get_cmds().iter().all(|p| !pat.is_match(p)) { - continue; - } + let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish)); + for cmd in post_job_hooks { + let AutoCmd { pattern, command } = cmd; + if let Some(pat) = pattern + && job.get_cmds().iter().all(|p| !pat.is_match(p)) + { + continue; + } - if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { - e.print_error(); - } - } + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { + e.print_error(); + } + } - write_meta(|m| m.post_system_message(job_complete_msg)) - } - } + write_meta(|m| m.post_system_message(job_complete_msg)) + } + } } Ok(()) } diff --git a/src/state.rs b/src/state.rs index 0b5b79a..85abf5b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,14 +12,30 @@ use nix::unistd::{User, gethostname, getppid}; use regex::Regex; use crate::{ - builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{ - error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt - }, parse::{ + builtin::{ + BUILTINS, + keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, + map::MapNode, + trap::TrapTarget, + }, + exec_input, + expand::expand_keymap, + jobs::JobTab, + libsh::{ + error::{ShErr, ShErrKind, ShResult}, + utils::VecDequeExt, + }, + parse::{ ConjunctNode, NdRule, Node, ParsedSrc, lex::{LexFlags, LexStream, Span, Tk}, - }, prelude::*, readline::{ - complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers - }, shopt::ShOpts + }, + prelude::*, + readline::{ + complete::{BashCompSpec, CompSpec}, + keys::KeyEvent, + markers, + }, + shopt::ShOpts, }; pub struct Shed { @@ -71,18 +87,18 @@ impl ShellParam { ) } - pub fn from_char(c: &char) -> Option { - match c { - '?' => Some(Self::Status), - '$' => Some(Self::ShPid), - '!' => Some(Self::LastJob), - '0' => Some(Self::ShellName), - '@' => Some(Self::AllArgs), - '*' => Some(Self::AllArgsStr), - '#' => Some(Self::ArgCount), - _ => None, - } - } + pub fn from_char(c: &char) -> Option { + match c { + '?' => Some(Self::Status), + '$' => Some(Self::ShPid), + '!' => Some(Self::LastJob), + '0' => Some(Self::ShellName), + '@' => Some(Self::AllArgs), + '*' => Some(Self::AllArgsStr), + '#' => Some(Self::ArgCount), + _ => None, + } + } } impl Display for ShellParam { @@ -299,10 +315,12 @@ impl ScopeStack { { match var.kind_mut() { VarKind::Arr(items) => return Ok(items), - _ => return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' is not an array", var_name), - )), + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is not an array", var_name), + )); + } } } } @@ -358,38 +376,37 @@ impl ScopeStack { } Ok("".into()) } - pub fn remove_map(&mut self, map_name: &str) -> Option { - for scope in self.scopes.iter_mut().rev() { - if scope.get_map(map_name).is_some() { - return scope.remove_map(map_name); - } - } - None - } - pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { - for scope in self.scopes.iter().rev() { - if let Some(map) = scope.get_map(map_name) { - return Some(map) - } - } - None - } - pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { - for scope in self.scopes.iter_mut().rev() { - if let Some(map) = scope.get_map_mut(map_name) { - return Some(map) - } - } - None - } - pub fn set_map(&mut self, map_name: &str, map: MapNode, local: bool) { - if local - && let Some(scope) = self.scopes.last_mut() { - scope.set_map(map_name, map); - } else if let Some(scope) = self.scopes.first_mut() { - scope.set_map(map_name, map); - } - } + pub fn remove_map(&mut self, map_name: &str) -> Option { + for scope in self.scopes.iter_mut().rev() { + if scope.get_map(map_name).is_some() { + return scope.remove_map(map_name); + } + } + None + } + pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { + for scope in self.scopes.iter().rev() { + if let Some(map) = scope.get_map(map_name) { + return Some(map); + } + } + None + } + pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { + for scope in self.scopes.iter_mut().rev() { + if let Some(map) = scope.get_map_mut(map_name) { + return Some(map); + } + } + None + } + pub fn set_map(&mut self, map_name: &str, map: MapNode, local: bool) { + if local && let Some(scope) = self.scopes.last_mut() { + scope.set_map(map_name, map); + } else if let Some(scope) = self.scopes.first_mut() { + scope.set_map(map_name, map); + } + } pub fn try_get_var(&self, var_name: &str) -> Option { // This version of get_var() is mainly used internally // so that we have access to Option methods @@ -410,11 +427,11 @@ impl ScopeStack { 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 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 { if let Ok(param) = var_name.parse::() { return self.get_param(param); @@ -480,14 +497,14 @@ thread_local! { #[derive(Clone, Debug)] pub struct ShAlias { - pub body: String, - pub source: Span + pub body: String, + pub source: Span, } impl Display for ShAlias { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.body) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.body) + } } /// A shell function @@ -495,14 +512,14 @@ impl Display for ShAlias { /// Wraps the BraceGrp Node that forms the body of the function, and provides some helper methods to extract it from the parse tree #[derive(Clone, Debug)] pub struct ShFunc { - pub body: Node, - pub source: Span + pub body: Node, + pub source: Span, } impl ShFunc { pub fn new(mut src: ParsedSrc, source: Span) -> Self { let body = Self::extract_brc_grp_hack(src.extract_nodes()); - Self{ body, source } + Self { body, source } } fn extract_brc_grp_hack(mut tree: Vec) -> Node { // FIXME: find a better way to do this @@ -524,61 +541,61 @@ impl ShFunc { #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum AutoCmdKind { - PreCmd, - PostCmd, - PreChangeDir, - PostChangeDir, - OnJobFinish, - PrePrompt, - PostPrompt, - PreModeChange, - PostModeChange, - OnExit + PreCmd, + PostCmd, + PreChangeDir, + PostChangeDir, + OnJobFinish, + PrePrompt, + PostPrompt, + PreModeChange, + PostModeChange, + OnExit, } impl Display for AutoCmdKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::PreCmd => write!(f, "pre-cmd"), - Self::PostCmd => write!(f, "post-cmd"), - Self::PreChangeDir => write!(f, "pre-change-dir"), - Self::PostChangeDir => write!(f, "post-change-dir"), - Self::OnJobFinish => write!(f, "on-job-finish"), - Self::PrePrompt => write!(f, "pre-prompt"), - Self::PostPrompt => write!(f, "post-prompt"), - Self::PreModeChange => write!(f, "pre-mode-change"), - Self::PostModeChange => write!(f, "post-mode-change"), - Self::OnExit => write!(f, "on-exit"), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PreCmd => write!(f, "pre-cmd"), + Self::PostCmd => write!(f, "post-cmd"), + Self::PreChangeDir => write!(f, "pre-change-dir"), + Self::PostChangeDir => write!(f, "post-change-dir"), + Self::OnJobFinish => write!(f, "on-job-finish"), + Self::PrePrompt => write!(f, "pre-prompt"), + Self::PostPrompt => write!(f, "post-prompt"), + Self::PreModeChange => write!(f, "pre-mode-change"), + Self::PostModeChange => write!(f, "post-mode-change"), + Self::OnExit => write!(f, "on-exit"), + } + } } impl FromStr for AutoCmdKind { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "pre-cmd" => Ok(Self::PreCmd), - "post-cmd" => Ok(Self::PostCmd), - "pre-change-dir" => Ok(Self::PreChangeDir), - "post-change-dir" => Ok(Self::PostChangeDir), - "on-job-finish" => Ok(Self::OnJobFinish), - "pre-prompt" => Ok(Self::PrePrompt), - "post-prompt" => Ok(Self::PostPrompt), - "pre-mode-change" => Ok(Self::PreModeChange), - "post-mode-change" => Ok(Self::PostModeChange), - "on-exit" => Ok(Self::OnExit), - _ => Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid autocmd kind: {}", s), - )), - } - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "pre-cmd" => Ok(Self::PreCmd), + "post-cmd" => Ok(Self::PostCmd), + "pre-change-dir" => Ok(Self::PreChangeDir), + "post-change-dir" => Ok(Self::PostChangeDir), + "on-job-finish" => Ok(Self::OnJobFinish), + "pre-prompt" => Ok(Self::PrePrompt), + "post-prompt" => Ok(Self::PostPrompt), + "pre-mode-change" => Ok(Self::PreModeChange), + "post-mode-change" => Ok(Self::PostModeChange), + "on-exit" => Ok(Self::OnExit), + _ => Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid autocmd kind: {}", s), + )), + } + } } #[derive(Clone, Debug)] pub struct AutoCmd { - pub pattern: Option, - pub command: String, + pub pattern: Option, + pub command: String, } /// The logic table for the shell @@ -589,58 +606,59 @@ pub struct LogTab { functions: HashMap, aliases: HashMap, traps: HashMap, - keymaps: Vec, - autocmds: HashMap> + keymaps: Vec, + autocmds: HashMap>, } impl LogTab { pub fn new() -> Self { Self::default() } - pub fn autocmds(&self) -> &HashMap> { - &self.autocmds - } - pub fn autocmds_mut(&mut self) -> &mut HashMap> { - &mut self.autocmds - } - pub fn insert_autocmd(&mut self, kind: AutoCmdKind, cmd: AutoCmd) { - self.autocmds.entry(kind).or_default().push(cmd); - } - pub fn get_autocmds(&self, kind: AutoCmdKind) -> Vec { - self.autocmds.get(&kind).cloned().unwrap_or_default() - } - pub fn clear_autocmds(&mut self, kind: AutoCmdKind) { - self.autocmds.remove(&kind); - } - pub fn keymaps(&self) -> &Vec { - &self.keymaps - } - pub fn keymaps_mut(&mut self) -> &mut Vec { - &mut self.keymaps - } - pub fn insert_keymap(&mut self, keymap: KeyMap) { - let mut found_dup = false; - for map in self.keymaps.iter_mut() { - if map.keys == keymap.keys { - *map = keymap.clone(); - found_dup = true; - break; - } - } - if !found_dup { - self.keymaps.push(keymap); - } - } - pub fn remove_keymap(&mut self, keys: &str) { - self.keymaps.retain(|km| km.keys != keys); - } - pub fn keymaps_filtered(&self, flags: KeyMapFlags, pending: &[KeyEvent]) -> Vec { - self.keymaps - .iter() - .filter(|km| km.flags.intersects(flags) && km.compare(pending) != KeyMapMatch::NoMatch) - .cloned() - .collect() - } + pub fn autocmds(&self) -> &HashMap> { + &self.autocmds + } + pub fn autocmds_mut(&mut self) -> &mut HashMap> { + &mut self.autocmds + } + pub fn insert_autocmd(&mut self, kind: AutoCmdKind, cmd: AutoCmd) { + self.autocmds.entry(kind).or_default().push(cmd); + } + pub fn get_autocmds(&self, kind: AutoCmdKind) -> Vec { + self.autocmds.get(&kind).cloned().unwrap_or_default() + } + pub fn clear_autocmds(&mut self, kind: AutoCmdKind) { + self.autocmds.remove(&kind); + } + pub fn keymaps(&self) -> &Vec { + &self.keymaps + } + pub fn keymaps_mut(&mut self) -> &mut Vec { + &mut self.keymaps + } + pub fn insert_keymap(&mut self, keymap: KeyMap) { + let mut found_dup = false; + for map in self.keymaps.iter_mut() { + if map.keys == keymap.keys { + *map = keymap.clone(); + found_dup = true; + break; + } + } + if !found_dup { + self.keymaps.push(keymap); + } + } + pub fn remove_keymap(&mut self, keys: &str) { + self.keymaps.retain(|km| km.keys != keys); + } + pub fn keymaps_filtered(&self, flags: KeyMapFlags, pending: &[KeyEvent]) -> Vec { + self + .keymaps + .iter() + .filter(|km| km.flags.intersects(flags) && km.compare(pending) != KeyMapMatch::NoMatch) + .cloned() + .collect() + } pub fn insert_func(&mut self, name: &str, src: ShFunc) { self.functions.insert(name.into(), src); } @@ -666,7 +684,13 @@ impl LogTab { &self.aliases } pub fn insert_alias(&mut self, name: &str, body: &str, source: Span) { - self.aliases.insert(name.into(), ShAlias { body: body.into(), source }); + self.aliases.insert( + name.into(), + ShAlias { + body: body.into(), + source, + }, + ); } pub fn get_alias(&self, name: &str) -> Option { self.aliases.get(name).cloned() @@ -751,7 +775,7 @@ impl VarFlags { pub enum ArrIndex { Literal(usize), FromBack(usize), - ArgCount, + ArgCount, AllJoined, AllSplit, } @@ -762,7 +786,7 @@ impl FromStr for ArrIndex { match s { "@" => Ok(Self::AllSplit), "*" => Ok(Self::AllJoined), - "#" => Ok(Self::ArgCount), + "#" => Ok(Self::ArgCount), _ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => { let idx = s[1..].parse::().unwrap(); Ok(Self::FromBack(idx)) @@ -879,15 +903,15 @@ impl Display for Var { } impl From for Var { - fn from(value: String) -> Self { - Self::new(VarKind::Str(value), VarFlags::NONE) - } + fn from(value: String) -> Self { + Self::new(VarKind::Str(value), VarFlags::NONE) + } } impl From<&str> for Var { - fn from(value: &str) -> Self { - Self::new(VarKind::Str(value.into()), VarFlags::NONE) - } + fn from(value: &str) -> Self { + Self::new(VarKind::Str(value.into()), VarFlags::NONE) + } } #[derive(Default, Clone, Debug)] @@ -896,7 +920,7 @@ pub struct VarTab { params: HashMap, sh_argv: VecDeque, /* Using a VecDeque makes the implementation of `shift` straightforward */ - maps: HashMap + maps: HashMap, } impl VarTab { @@ -908,7 +932,7 @@ impl VarTab { vars, params, sh_argv: VecDeque::new(), - maps: HashMap::new(), + maps: HashMap::new(), }; var_tab.init_sh_argv(); var_tab @@ -1027,24 +1051,24 @@ impl VarTab { self.update_arg_params(); arg } - pub fn set_map(&mut self, map_name: &str, map: MapNode) { - self.maps.insert(map_name.to_string(), map); - } - pub fn remove_map(&mut self, map_name: &str) -> Option { - self.maps.remove(map_name) - } - pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { - self.maps.get(map_name) - } - pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { - self.maps.get_mut(map_name) - } - pub fn maps(&self) -> &HashMap { - &self.maps - } - pub fn maps_mut(&mut self) -> &mut HashMap { - &mut self.maps - } + pub fn set_map(&mut self, map_name: &str, map: MapNode) { + self.maps.insert(map_name.to_string(), map); + } + pub fn remove_map(&mut self, map_name: &str) -> Option { + self.maps.remove(map_name) + } + pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { + self.maps.get(map_name) + } + pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { + self.maps.get_mut(map_name) + } + pub fn maps(&self) -> &HashMap { + &self.maps + } + pub fn maps_mut(&mut self) -> &mut HashMap { + &mut self.maps + } pub fn vars(&self) -> &HashMap { &self.vars } @@ -1160,9 +1184,9 @@ impl VarTab { } Ok(()) } - pub fn map_exists(&self, map_name: &str) -> bool { - self.maps.contains_key(map_name) - } + pub fn map_exists(&self, map_name: &str) -> bool { + self.maps.contains_key(map_name) + } pub fn var_exists(&self, var_name: &str) -> bool { if let Ok(param) = var_name.parse::() { return self.params.contains_key(¶m); @@ -1205,8 +1229,8 @@ pub struct MetaTab { // pushd/popd stack dir_stack: VecDeque, - // getopts char offset for opts like -abc - getopts_offset: usize, + // getopts char offset for opts like -abc + getopts_offset: usize, old_path: Option, old_pwd: Option, @@ -1216,8 +1240,8 @@ pub struct MetaTab { // programmable completion specs comp_specs: HashMap>, - // pending keys from widget function - pending_widget_keys: Vec + // pending keys from widget function + pending_widget_keys: Vec, } impl MetaTab { @@ -1227,28 +1251,28 @@ impl MetaTab { ..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> { - 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 { - self.getopts_offset - } - pub fn inc_getopts_char_offset(&mut self) -> usize { - let offset = self.getopts_offset; - self.getopts_offset += 1; - offset - } - pub fn reset_getopts_char_offset(&mut self) { - self.getopts_offset = 0; - } + 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> { + 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 { + self.getopts_offset + } + pub fn inc_getopts_char_offset(&mut self) -> usize { + let offset = self.getopts_offset; + self.getopts_offset += 1; + offset + } + pub fn reset_getopts_char_offset(&mut self) { + self.getopts_offset = 0; + } pub fn get_builtin_comp_specs() -> HashMap> { let mut map = HashMap::new(); @@ -1280,14 +1304,14 @@ impl MetaTab { "disown".into(), Box::new(BashCompSpec::new().jobs(true)) as Box, ); - map.insert( - "alias".into(), - Box::new(BashCompSpec::new().aliases(true)) as Box, - ); - map.insert( - "trap".into(), - Box::new(BashCompSpec::new().signals(true)) as Box, - ); + map.insert( + "alias".into(), + Box::new(BashCompSpec::new().aliases(true)) as Box, + ); + map.insert( + "trap".into(), + Box::new(BashCompSpec::new().signals(true)) as Box, + ); map } @@ -1312,29 +1336,29 @@ impl MetaTab { pub fn remove_comp_spec(&mut self, cmd: &str) -> bool { self.comp_specs.remove(cmd).is_some() } - pub fn get_cmds_in_path() -> Vec { + pub fn get_cmds_in_path() -> Vec { let path = env::var("PATH").unwrap_or_default(); let paths = path.split(":").map(PathBuf::from); - let mut cmds = vec![]; - for path in paths { - if let Ok(entries) = path.read_dir() { - for entry in entries.flatten() { - let Ok(meta) = std::fs::metadata(entry.path()) else { - continue; - }; - let is_exec = meta.permissions().mode() & 0o111 != 0; + let mut cmds = vec![]; + for path in paths { + if let Ok(entries) = path.read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; + let is_exec = meta.permissions().mode() & 0o111 != 0; - if meta.is_file() - && is_exec - && let Some(name) = entry.file_name().to_str() - { - cmds.push(name.to_string()); - } - } - } - } - cmds - } + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + { + cmds.push(name.to_string()); + } + } + } + } + cmds + } pub fn try_rehash_commands(&mut self) { let path = env::var("PATH").unwrap_or_default(); let cwd = env::var("PWD").unwrap_or_default(); @@ -1350,10 +1374,10 @@ impl MetaTab { self.path_cache.clear(); self.old_path = Some(path.clone()); self.old_pwd = Some(cwd.clone()); - let cmds_in_path = Self::get_cmds_in_path(); - for cmd in cmds_in_path { - self.path_cache.insert(cmd); - } + let cmds_in_path = Self::get_cmds_in_path(); + for cmd in cmds_in_path { + self.path_cache.insert(cmd); + } if let Ok(entries) = Path::new(&cwd).read_dir() { for entry in entries.flatten() { let Ok(meta) = std::fs::metadata(entry.path()) else { @@ -1589,63 +1613,86 @@ pub fn get_shopt(path: &str) -> String { read_shopts(|s| s.get(path)).unwrap().unwrap() } -pub fn with_vars(vars: H, f: F) -> T +pub fn with_vars(vars: H, f: F) -> T where - F: FnOnce() -> T, - H: Into>, - V: Into { - - let snapshot = read_vars(|v| v.clone()); - let vars = vars.into(); - for (name, val) in vars { - let val = val.into(); - write_vars(|v| v.set_var(&name, val.kind, val.flags).unwrap()); - } - let _guard = scopeguard::guard(snapshot, |snap| { - write_vars(|v| *v = snap); - }); - f() + F: FnOnce() -> T, + H: Into>, + V: Into, +{ + let snapshot = read_vars(|v| v.clone()); + let vars = vars.into(); + for (name, val) in vars { + let val = val.into(); + write_vars(|v| v.set_var(&name, val.kind, val.flags).unwrap()); + } + let _guard = scopeguard::guard(snapshot, |snap| { + write_vars(|v| *v = snap); + }); + f() } pub fn change_dir>(dir: P) -> ShResult<()> { - let dir = dir.as_ref(); - let dir_raw = &dir.display().to_string(); - let pre_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PreChangeDir)); - let post_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PostChangeDir)); + let dir = dir.as_ref(); + let dir_raw = &dir.display().to_string(); + let pre_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PreChangeDir)); + let post_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PostChangeDir)); - let current_dir = env::current_dir()?.display().to_string(); - with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || { - for cmd in pre_cd { - let AutoCmd { command, pattern } = cmd; - if let Some(pat) = pattern - && !pat.is_match(dir_raw) { - continue; - } + let current_dir = env::current_dir()?.display().to_string(); + with_vars( + [ + ("_NEW_DIR".into(), dir_raw.as_str()), + ("_OLD_DIR".into(), current_dir.as_str()), + ], + || { + for cmd in pre_cd { + let AutoCmd { command, pattern } = cmd; + if let Some(pat) = pattern + && !pat.is_match(dir_raw) + { + continue; + } - if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (pre-changedir)".to_string())) { - e.print_error(); - }; - } - }); + if let Err(e) = exec_input( + command.clone(), + None, + false, + Some("autocmd (pre-changedir)".to_string()), + ) { + e.print_error(); + }; + } + }, + ); + env::set_current_dir(dir)?; - env::set_current_dir(dir)?; + with_vars( + [ + ("_NEW_DIR".into(), dir_raw.as_str()), + ("_OLD_DIR".into(), current_dir.as_str()), + ], + || { + for cmd in post_cd { + let AutoCmd { command, pattern } = cmd; + if let Some(pat) = pattern + && !pat.is_match(dir_raw) + { + continue; + } - with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || { - for cmd in post_cd { - let AutoCmd { command, pattern } = cmd; - if let Some(pat) = pattern - && !pat.is_match(dir_raw) { - continue; - } + if let Err(e) = exec_input( + command.clone(), + None, + false, + Some("autocmd (post-changedir)".to_string()), + ) { + e.print_error(); + }; + } + }, + ); - if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (post-changedir)".to_string())) { - e.print_error(); - }; - } - }); - - Ok(()) + Ok(()) } pub fn get_status() -> i32 { @@ -1671,7 +1718,7 @@ pub fn source_rc() -> ShResult<()> { } pub fn source_file(path: PathBuf) -> ShResult<()> { - let source_name = path.to_string_lossy().to_string(); + let source_name = path.to_string_lossy().to_string(); let mut file = OpenOptions::new().read(true).open(path)?; let mut buf = String::new();