Compare commits
10 Commits
34b780c644
...
62d651eb8d
| Author | SHA1 | Date | |
|---|---|---|---|
| 62d651eb8d | |||
| 55d47f6d90 | |||
| f55e204c82 | |||
| 05f731b8c5 | |||
| 4f834e9709 | |||
| d8fda01a5c | |||
| fdb182a0c5 | |||
| aed0e6fb8c | |||
| 1a44a783e0 | |||
| 96425fb48f |
@@ -84,12 +84,6 @@ in
|
|||||||
default = true;
|
default = true;
|
||||||
description = "Whether to enable syntax highlighting in the shell";
|
description = "Whether to enable syntax highlighting in the shell";
|
||||||
};
|
};
|
||||||
tabStop = lib.mkOption {
|
|
||||||
type = lib.types.int;
|
|
||||||
default = 4;
|
|
||||||
description = "The number of spaces to use for tab stop in the shell";
|
|
||||||
};
|
|
||||||
|
|
||||||
extraPostConfig = lib.mkOption {
|
extraPostConfig = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "";
|
default = "";
|
||||||
@@ -123,7 +117,6 @@ in
|
|||||||
"shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}"
|
"shopt prompt.trunc_prompt_path=${toString cfg.settings.promptPathSegments}"
|
||||||
"shopt prompt.comp_limit=${toString cfg.settings.completionLimit}"
|
"shopt prompt.comp_limit=${toString cfg.settings.completionLimit}"
|
||||||
"shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}"
|
"shopt prompt.highlight=${boolToString cfg.settings.syntaxHighlighting}"
|
||||||
"shopt prompt.tab_stop=${toString cfg.settings.tabStop}"
|
|
||||||
])
|
])
|
||||||
cfg.settings.extraPostConfig
|
cfg.settings.extraPostConfig
|
||||||
];
|
];
|
||||||
|
|||||||
34
src/builtin/eval.rs
Normal file
34
src/builtin/eval.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use nix::{errno::Errno, unistd::execvpe};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::ShResult,
|
||||||
|
parse::{NdRule, Node, execute::exec_input},
|
||||||
|
procio::IoStack,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||||
|
let NdRule::Command {
|
||||||
|
assignments: _,
|
||||||
|
argv,
|
||||||
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (expanded_argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
|
if expanded_argv.is_empty() {
|
||||||
|
state::set_status(0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let joined_argv = expanded_argv.into_iter()
|
||||||
|
.map(|(s, _)| s)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
exec_input(joined_argv, None, false)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoStack, borrow_fd},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, VarFlags, write_vars},
|
state::{self, VarFlags, read_vars, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::setup_builtin;
|
use super::setup_builtin;
|
||||||
@@ -45,3 +45,42 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
|
|||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||||
|
let NdRule::Command {
|
||||||
|
assignments: _,
|
||||||
|
argv,
|
||||||
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
|
if argv.is_empty() {
|
||||||
|
// Display the local variables
|
||||||
|
let vars_output = read_vars(|v| {
|
||||||
|
let mut vars = v.flatten_vars()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", k, v))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
vars.sort();
|
||||||
|
let mut vars_joined = vars.join("\n");
|
||||||
|
vars_joined.push('\n');
|
||||||
|
vars_joined
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = borrow_fd(STDOUT_FILENO);
|
||||||
|
write(stdout, vars_output.as_bytes())?; // Write it
|
||||||
|
} else {
|
||||||
|
for (arg, _) in argv {
|
||||||
|
if let Some((var, val)) = arg.split_once('=') {
|
||||||
|
write_vars(|v| v.set_var(var, val, VarFlags::LOCAL));
|
||||||
|
} else {
|
||||||
|
write_vars(|v| v.set_var(&arg, "", VarFlags::LOCAL)); // Create an uninitialized local variable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state::set_status(0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,3 +179,46 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||||
|
let blame = node.get_span().clone();
|
||||||
|
let NdRule::Command {
|
||||||
|
assignments: _,
|
||||||
|
argv,
|
||||||
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
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::full(ShErrKind::ExecFail, "disown: No jobs to disown", blame));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tabid = curr_job_id;
|
||||||
|
let mut nohup = false;
|
||||||
|
let mut disown_all = false;
|
||||||
|
|
||||||
|
while let Some((arg, span)) = argv.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-h" => nohup = true,
|
||||||
|
"-a" => disown_all = true,
|
||||||
|
_ => {
|
||||||
|
tabid = parse_job_id(&arg, span.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if disown_all {
|
||||||
|
write_jobs(|j| j.disown_all(nohup))?;
|
||||||
|
} else {
|
||||||
|
write_jobs(|j| j.disown(JobID::TableID(tabid), nohup))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state::set_status(0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
1
src/builtin/local.rs
Normal file
1
src/builtin/local.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -27,11 +27,12 @@ pub mod trap;
|
|||||||
pub mod zoltraak;
|
pub mod zoltraak;
|
||||||
pub mod dirstack;
|
pub mod dirstack;
|
||||||
pub mod exec;
|
pub mod exec;
|
||||||
|
pub mod eval;
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 25] = [
|
pub const BUILTINS: [&str; 28] = [
|
||||||
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias",
|
||||||
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
||||||
"pushd", "popd", "dirs", "exec",
|
"pushd", "popd", "dirs", "exec", "eval"
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sets up a builtin command
|
/// Sets up a builtin command
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule, is_field_sep,
|
|||||||
use crate::parse::{Redir, RedirType};
|
use crate::parse::{Redir, RedirType};
|
||||||
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars,
|
LogTab, VarFlags, read_logic, read_vars, write_jobs, write_meta, write_vars,
|
||||||
};
|
};
|
||||||
use crate::{jobs, prelude::*};
|
use crate::{jobs, prelude::*};
|
||||||
|
|
||||||
@@ -36,6 +36,10 @@ pub const PROC_SUB_OUT: char = '\u{fdd6}';
|
|||||||
/// arguments Without this marker, it would be handled like an empty string,
|
/// arguments Without this marker, it would be handled like an empty string,
|
||||||
/// which breaks some commands
|
/// which breaks some commands
|
||||||
pub const NULL_EXPAND: char = '\u{fdd7}';
|
pub const NULL_EXPAND: char = '\u{fdd7}';
|
||||||
|
/// Explicit marker for argument separation
|
||||||
|
/// This is used to join the arguments given by "$@", and preserves exact formatting
|
||||||
|
/// of the original arguments, including quoting
|
||||||
|
pub const ARG_SEP: char = '\u{fdd8}';
|
||||||
|
|
||||||
impl Tk {
|
impl Tk {
|
||||||
/// Create a new expanded token
|
/// Create a new expanded token
|
||||||
@@ -104,6 +108,9 @@ impl Expander {
|
|||||||
DUB_QUOTE | SNG_QUOTE | SUBSH => {
|
DUB_QUOTE | SNG_QUOTE | SUBSH => {
|
||||||
while let Some(q_ch) = chars.next() {
|
while let Some(q_ch) = chars.next() {
|
||||||
match q_ch {
|
match q_ch {
|
||||||
|
ARG_SEP if ch == DUB_QUOTE => {
|
||||||
|
words.push(mem::take(&mut cur_word));
|
||||||
|
}
|
||||||
_ if q_ch == ch => {
|
_ if q_ch == ch => {
|
||||||
was_quoted = true;
|
was_quoted = true;
|
||||||
continue 'outer; // Isn't rust cool
|
continue 'outer; // Isn't rust cool
|
||||||
@@ -112,7 +119,7 @@ impl Expander {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ if is_field_sep(ch) => {
|
_ if is_field_sep(ch) || ch == ARG_SEP => {
|
||||||
if cur_word.is_empty() && !was_quoted {
|
if cur_word.is_empty() && !was_quoted {
|
||||||
cur_word.clear();
|
cur_word.clear();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
54
src/jobs.rs
54
src/jobs.rs
@@ -373,7 +373,7 @@ impl JobTab {
|
|||||||
if job
|
if job
|
||||||
.get_stats()
|
.get_stats()
|
||||||
.iter()
|
.iter()
|
||||||
.all(|stat| matches!(stat, WtStat::Exited(_, _)))
|
.all(|stat| matches!(stat, WtStat::Exited(_, _) | WtStat::Signaled(_, _, _)))
|
||||||
{
|
{
|
||||||
jobs_to_remove.push(JobID::TableID(id));
|
jobs_to_remove.push(JobID::TableID(id));
|
||||||
}
|
}
|
||||||
@@ -383,6 +383,41 @@ impl JobTab {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hang_up(&mut self) {
|
||||||
|
for job in self.jobs_mut().iter_mut().flatten() {
|
||||||
|
if job.send_hup {
|
||||||
|
job.killpg(Signal::SIGHUP).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disown(&mut self, id: JobID, nohup: bool) -> ShResult<()> {
|
||||||
|
if let Some(job) = self.query_mut(id.clone()) {
|
||||||
|
if nohup {
|
||||||
|
job.no_hup();
|
||||||
|
} else {
|
||||||
|
self.remove_job(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disown_all(&mut self, nohup: bool) -> ShResult<()> {
|
||||||
|
let mut ids_to_remove = vec![];
|
||||||
|
for job in self.jobs_mut().iter_mut().flatten() {
|
||||||
|
if nohup {
|
||||||
|
job.no_hup();
|
||||||
|
} else {
|
||||||
|
ids_to_remove.push(JobID::TableID(job.tabid().unwrap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in ids_to_remove {
|
||||||
|
self.remove_job(id);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -390,6 +425,7 @@ pub struct JobBldr {
|
|||||||
table_id: Option<usize>,
|
table_id: Option<usize>,
|
||||||
pgid: Option<Pid>,
|
pgid: Option<Pid>,
|
||||||
children: Vec<ChildProc>,
|
children: Vec<ChildProc>,
|
||||||
|
send_hup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for JobBldr {
|
impl Default for JobBldr {
|
||||||
@@ -404,6 +440,7 @@ impl JobBldr {
|
|||||||
table_id: None,
|
table_id: None,
|
||||||
pgid: None,
|
pgid: None,
|
||||||
children: vec![],
|
children: vec![],
|
||||||
|
send_hup: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn with_id(self, id: usize) -> Self {
|
pub fn with_id(self, id: usize) -> Self {
|
||||||
@@ -411,6 +448,7 @@ impl JobBldr {
|
|||||||
table_id: Some(id),
|
table_id: Some(id),
|
||||||
pgid: self.pgid,
|
pgid: self.pgid,
|
||||||
children: self.children,
|
children: self.children,
|
||||||
|
send_hup: self.send_hup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn with_pgid(self, pgid: Pid) -> Self {
|
pub fn with_pgid(self, pgid: Pid) -> Self {
|
||||||
@@ -418,6 +456,7 @@ impl JobBldr {
|
|||||||
table_id: self.table_id,
|
table_id: self.table_id,
|
||||||
pgid: Some(pgid),
|
pgid: Some(pgid),
|
||||||
children: self.children,
|
children: self.children,
|
||||||
|
send_hup: self.send_hup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn set_pgid(&mut self, pgid: Pid) {
|
pub fn set_pgid(&mut self, pgid: Pid) {
|
||||||
@@ -426,11 +465,16 @@ impl JobBldr {
|
|||||||
pub fn pgid(&self) -> Option<Pid> {
|
pub fn pgid(&self) -> Option<Pid> {
|
||||||
self.pgid
|
self.pgid
|
||||||
}
|
}
|
||||||
|
pub fn no_hup(mut self) -> Self {
|
||||||
|
self.send_hup = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn with_children(self, children: Vec<ChildProc>) -> Self {
|
pub fn with_children(self, children: Vec<ChildProc>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
table_id: self.table_id,
|
table_id: self.table_id,
|
||||||
pgid: self.pgid,
|
pgid: self.pgid,
|
||||||
children,
|
children,
|
||||||
|
send_hup: self.send_hup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn push_child(&mut self, child: ChildProc) {
|
pub fn push_child(&mut self, child: ChildProc) {
|
||||||
@@ -441,6 +485,7 @@ impl JobBldr {
|
|||||||
table_id: self.table_id,
|
table_id: self.table_id,
|
||||||
pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
|
pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
|
||||||
children: self.children,
|
children: self.children,
|
||||||
|
send_hup: self.send_hup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,12 +514,16 @@ pub struct Job {
|
|||||||
table_id: Option<usize>,
|
table_id: Option<usize>,
|
||||||
pgid: Pid,
|
pgid: Pid,
|
||||||
children: Vec<ChildProc>,
|
children: Vec<ChildProc>,
|
||||||
|
send_hup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Job {
|
impl Job {
|
||||||
pub fn set_tabid(&mut self, id: usize) {
|
pub fn set_tabid(&mut self, id: usize) {
|
||||||
self.table_id = Some(id)
|
self.table_id = Some(id)
|
||||||
}
|
}
|
||||||
|
pub fn no_hup(&mut self) {
|
||||||
|
self.send_hup = false;
|
||||||
|
}
|
||||||
pub fn running(&self) -> bool {
|
pub fn running(&self) -> bool {
|
||||||
!self.children.iter().all(|chld| chld.exited())
|
!self.children.iter().all(|chld| chld.exited())
|
||||||
}
|
}
|
||||||
@@ -520,8 +569,7 @@ impl Job {
|
|||||||
let stat = match sig {
|
let stat = match sig {
|
||||||
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
|
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
|
||||||
Signal::SIGCONT => WtStat::Continued(self.pgid),
|
Signal::SIGCONT => WtStat::Continued(self.pgid),
|
||||||
Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false),
|
sig => WtStat::Signaled(self.pgid, sig, false),
|
||||||
_ => unimplemented!("{}", sig),
|
|
||||||
};
|
};
|
||||||
self.set_stats(stat);
|
self.set_stats(stat);
|
||||||
Ok(killpg(self.pgid, sig)?)
|
Ok(killpg(self.pgid, sig)?)
|
||||||
|
|||||||
21
src/main.rs
21
src/main.rs
@@ -36,7 +36,7 @@ use crate::prompt::get_prompt;
|
|||||||
use crate::prompt::readline::term::raw_mode;
|
use crate::prompt::readline::term::raw_mode;
|
||||||
use crate::prompt::readline::{FernVi, ReadlineEvent};
|
use crate::prompt::readline::{FernVi, ReadlineEvent};
|
||||||
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
|
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
|
||||||
use crate::state::{read_logic, source_rc, write_meta};
|
use crate::state::{read_logic, source_rc, write_jobs, write_meta};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use state::{read_vars, write_vars};
|
use state::{read_vars, write_vars};
|
||||||
|
|
||||||
@@ -74,9 +74,25 @@ fn kickstart_lazy_evals() {
|
|||||||
read_vars(|_| {});
|
read_vars(|_| {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// We need to make sure that even if we panic, our child processes get sighup
|
||||||
|
fn setup_panic_handler() {
|
||||||
|
let default_panic_hook = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
let _ = state::FERN.try_with(|fern| {
|
||||||
|
if let Ok(mut jobs) = fern.jobs.try_borrow_mut() {
|
||||||
|
jobs.hang_up();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default_panic_hook(info);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
kickstart_lazy_evals();
|
kickstart_lazy_evals();
|
||||||
|
setup_panic_handler();
|
||||||
|
|
||||||
let mut args = FernArgs::parse();
|
let mut args = FernArgs::parse();
|
||||||
if env::args().next().is_some_and(|a| a.starts_with('-')) {
|
if env::args().next().is_some_and(|a| a.starts_with('-')) {
|
||||||
// first arg is '-fern'
|
// first arg is '-fern'
|
||||||
@@ -84,7 +100,7 @@ fn main() -> ExitCode {
|
|||||||
args.login_shell = true;
|
args.login_shell = true;
|
||||||
}
|
}
|
||||||
if args.version {
|
if args.version {
|
||||||
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS);
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +120,7 @@ fn main() -> ExitCode {
|
|||||||
eprintln!("fern: error running EXIT trap: {e}");
|
eprintln!("fern: error running EXIT trap: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
write_jobs(|j| j.hang_up());
|
||||||
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
|
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use std::collections::{HashSet, VecDeque};
|
use std::{collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
builtin::{
|
||||||
alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, exec, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak
|
alias::{alias, unalias}, cd::cd, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, export::{export, local}, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak
|
||||||
},
|
},
|
||||||
expand::expand_aliases,
|
expand::expand_aliases,
|
||||||
jobs::{ChildProc, JobStack, dispatch_job},
|
jobs::{ChildProc, JobStack, dispatch_job},
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoMode, IoStack},
|
procio::{IoMode, IoStack},
|
||||||
state::{self, ShFunc, VarFlags, read_logic, write_jobs, write_logic, write_vars},
|
state::{self, ShFunc, VarFlags, read_logic, read_shopts, write_jobs, write_logic, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -22,6 +22,38 @@ thread_local! {
|
|||||||
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_in_path(name: &str) -> bool {
|
||||||
|
if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') {
|
||||||
|
let path = Path::new(name);
|
||||||
|
if path.exists() && path.is_file() && !path.is_dir() {
|
||||||
|
let meta = match path.metadata() {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
if meta.permissions().mode() & 0o111 != 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let Ok(path) = env::var("PATH") else { return false };
|
||||||
|
let paths = path.split(':');
|
||||||
|
for path in paths {
|
||||||
|
let full_path = Path::new(path).join(name);
|
||||||
|
if full_path.exists() && full_path.is_file() && !full_path.is_dir() {
|
||||||
|
let meta = match full_path.metadata() {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if meta.permissions().mode() & 0o111 != 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ScopeGuard;
|
pub struct ScopeGuard;
|
||||||
|
|
||||||
impl ScopeGuard {
|
impl ScopeGuard {
|
||||||
@@ -190,8 +222,9 @@ impl Dispatcher {
|
|||||||
self.exec_builtin(node)
|
self.exec_builtin(node)
|
||||||
} else if is_subsh(node.get_command().cloned()) {
|
} else if is_subsh(node.get_command().cloned()) {
|
||||||
self.exec_subsh(node)
|
self.exec_subsh(node)
|
||||||
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir()
|
} else if read_shopts(|s| s.core.autocd)
|
||||||
{
|
&& Path::new(cmd.span.as_str()).is_dir()
|
||||||
|
&& !is_in_path(cmd.span.as_str()) {
|
||||||
let dir = cmd.span.as_str().to_string();
|
let dir = cmd.span.as_str().to_string();
|
||||||
let stack = IoStack {
|
let stack = IoStack {
|
||||||
stack: self.io_stack.clone(),
|
stack: self.io_stack.clone(),
|
||||||
@@ -293,7 +326,7 @@ impl Dispatcher {
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_depth = crate::state::read_shopts(|s| s.core.max_recurse_depth);
|
let max_depth = read_shopts(|s| s.core.max_recurse_depth);
|
||||||
let depth = RECURSE_DEPTH.with(|d| {
|
let depth = RECURSE_DEPTH.with(|d| {
|
||||||
let cur = d.get();
|
let cur = d.get();
|
||||||
d.set(cur + 1);
|
d.set(cur + 1);
|
||||||
@@ -588,11 +621,13 @@ impl Dispatcher {
|
|||||||
"echo" => echo(cmd, io_stack_mut, curr_job_mut),
|
"echo" => echo(cmd, io_stack_mut, curr_job_mut),
|
||||||
"cd" => cd(cmd, curr_job_mut),
|
"cd" => cd(cmd, curr_job_mut),
|
||||||
"export" => export(cmd, io_stack_mut, curr_job_mut),
|
"export" => export(cmd, io_stack_mut, curr_job_mut),
|
||||||
|
"local" => local(cmd, io_stack_mut, curr_job_mut),
|
||||||
"pwd" => pwd(cmd, io_stack_mut, curr_job_mut),
|
"pwd" => pwd(cmd, io_stack_mut, curr_job_mut),
|
||||||
"source" => source(cmd, curr_job_mut),
|
"source" => source(cmd, curr_job_mut),
|
||||||
"shift" => shift(cmd, curr_job_mut),
|
"shift" => shift(cmd, curr_job_mut),
|
||||||
"fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound),
|
"fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound),
|
||||||
"bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background),
|
"bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background),
|
||||||
|
"disown" => disown(cmd, io_stack_mut, curr_job_mut),
|
||||||
"jobs" => jobs(cmd, io_stack_mut, curr_job_mut),
|
"jobs" => jobs(cmd, io_stack_mut, curr_job_mut),
|
||||||
"alias" => alias(cmd, io_stack_mut, curr_job_mut),
|
"alias" => alias(cmd, io_stack_mut, curr_job_mut),
|
||||||
"unalias" => unalias(cmd, io_stack_mut, curr_job_mut),
|
"unalias" => unalias(cmd, io_stack_mut, curr_job_mut),
|
||||||
@@ -608,6 +643,7 @@ impl Dispatcher {
|
|||||||
"popd" => popd(cmd, io_stack_mut, curr_job_mut),
|
"popd" => popd(cmd, io_stack_mut, curr_job_mut),
|
||||||
"dirs" => dirs(cmd, io_stack_mut, curr_job_mut),
|
"dirs" => dirs(cmd, io_stack_mut, curr_job_mut),
|
||||||
"exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut),
|
"exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut),
|
||||||
|
"eval" => eval::eval(cmd, io_stack_mut, curr_job_mut),
|
||||||
_ => unimplemented!(
|
_ => unimplemented!(
|
||||||
"Have not yet added support for builtin '{}'",
|
"Have not yet added support for builtin '{}'",
|
||||||
cmd_raw.span.as_str()
|
cmd_raw.span.as_str()
|
||||||
|
|||||||
@@ -265,10 +265,12 @@ impl LexStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
|
let span_start = self.cursor;
|
||||||
|
self.cursor = pos;
|
||||||
return Some(Err(ShErr::full(
|
return Some(Err(ShErr::full(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
"Invalid redirection",
|
"Invalid redirection",
|
||||||
Span::new(self.cursor..pos, self.source.clone()),
|
Span::new(span_start..pos, self.source.clone()),
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
@@ -625,6 +627,7 @@ impl LexStream {
|
|||||||
}
|
}
|
||||||
let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str);
|
let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str);
|
||||||
if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
|
self.cursor = pos;
|
||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
"Unterminated quote",
|
"Unterminated quote",
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ impl Highlighter {
|
|||||||
/// indicating token types and sub-token constructs (strings, variables, etc.)
|
/// indicating token types and sub-token constructs (strings, variables, etc.)
|
||||||
pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) {
|
pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) {
|
||||||
let input = annotate_input(input);
|
let input = annotate_input(input);
|
||||||
log::debug!("Annotated input: {:?}", input);
|
|
||||||
self.input = input;
|
self.input = input;
|
||||||
self.linebuf_cursor_pos = linebuf_cursor_pos;
|
self.linebuf_cursor_pos = linebuf_cursor_pos;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use std::{
|
|||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::linebuf::LineBuf};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
use super::vicmd::Direction; // surprisingly useful
|
use super::vicmd::Direction; // surprisingly useful
|
||||||
@@ -207,7 +207,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
|||||||
|
|
||||||
pub struct History {
|
pub struct History {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
pub pending: Option<(String, usize)>, // command, cursor_pos
|
pub pending: Option<LineBuf>, // command, cursor_pos
|
||||||
entries: Vec<HistEntry>,
|
entries: Vec<HistEntry>,
|
||||||
search_mask: Vec<HistEntry>,
|
search_mask: Vec<HistEntry>,
|
||||||
no_matches: bool,
|
no_matches: bool,
|
||||||
@@ -272,7 +272,7 @@ impl History {
|
|||||||
|
|
||||||
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
|
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
|
||||||
let cursor_pos = if let Some(pending) = &self.pending {
|
let cursor_pos = if let Some(pending) = &self.pending {
|
||||||
pending.1
|
pending.cursor.get()
|
||||||
} else {
|
} else {
|
||||||
buf.1
|
buf.1
|
||||||
};
|
};
|
||||||
@@ -282,7 +282,12 @@ impl History {
|
|||||||
term: cmd.clone(),
|
term: cmd.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pending = Some((cmd, cursor_pos));
|
if let Some(pending) = &mut self.pending {
|
||||||
|
pending.set_buffer(cmd);
|
||||||
|
pending.cursor.set(cursor_pos);
|
||||||
|
} else {
|
||||||
|
self.pending = Some(LineBuf::new().with_initial(&cmd, cursor_pos));
|
||||||
|
}
|
||||||
self.constrain_entries(constraint);
|
self.constrain_entries(constraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +345,7 @@ impl History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hint(&self) -> Option<String> {
|
pub fn get_hint(&self) -> Option<String> {
|
||||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) {
|
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.buffer.is_empty()) {
|
||||||
let entry = self.hint_entry()?;
|
let entry = self.hint_entry()?;
|
||||||
Some(entry.command().to_string())
|
Some(entry.command().to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -354,10 +359,6 @@ impl History {
|
|||||||
.saturating_add_signed(offset)
|
.saturating_add_signed(offset)
|
||||||
.clamp(0, self.search_mask.len());
|
.clamp(0, self.search_mask.len());
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"Scrolling history by offset {offset} from cursor at index {}",
|
|
||||||
self.cursor
|
|
||||||
);
|
|
||||||
self.search_mask.get(self.cursor)
|
self.search_mask.get(self.cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ use crate::{
|
|||||||
libsh::{
|
libsh::{
|
||||||
error::ShResult,
|
error::ShResult,
|
||||||
term::{Style, Styled},
|
term::{Style, Styled},
|
||||||
},
|
}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, prompt::readline::{markers, register::write_register}, state::read_shopts
|
||||||
prelude::*, prompt::readline::{markers, register::write_register},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
||||||
@@ -327,6 +326,7 @@ pub struct LineBuf {
|
|||||||
|
|
||||||
pub insert_mode_start_pos: Option<usize>,
|
pub insert_mode_start_pos: Option<usize>,
|
||||||
pub saved_col: Option<usize>,
|
pub saved_col: Option<usize>,
|
||||||
|
pub auto_indent_level: usize,
|
||||||
|
|
||||||
pub undo_stack: Vec<Edit>,
|
pub undo_stack: Vec<Edit>,
|
||||||
pub redo_stack: Vec<Edit>,
|
pub redo_stack: Vec<Edit>,
|
||||||
@@ -409,7 +409,6 @@ impl LineBuf {
|
|||||||
.unwrap_or(self.buffer.len())
|
.unwrap_or(self.buffer.len())
|
||||||
}
|
}
|
||||||
/// Update self.grapheme_indices with the indices of the current buffer
|
/// Update self.grapheme_indices with the indices of the current buffer
|
||||||
#[track_caller]
|
|
||||||
pub fn update_graphemes(&mut self) {
|
pub fn update_graphemes(&mut self) {
|
||||||
let indices: Vec<_> = self.buffer.grapheme_indices(true).map(|(i, _)| i).collect();
|
let indices: Vec<_> = self.buffer.grapheme_indices(true).map(|(i, _)| i).collect();
|
||||||
self.cursor.set_max(indices.len());
|
self.cursor.set_max(indices.len());
|
||||||
@@ -1884,6 +1883,29 @@ impl LineBuf {
|
|||||||
let end = start + gr.len();
|
let end = start + gr.len();
|
||||||
self.buffer.replace_range(start..end, new);
|
self.buffer.replace_range(start..end, new);
|
||||||
}
|
}
|
||||||
|
pub fn calc_indent_level(&mut self) {
|
||||||
|
let input = Arc::new(self.buffer.clone());
|
||||||
|
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
|
||||||
|
log::error!("Failed to lex buffer for indent calculation");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut level: usize = 0;
|
||||||
|
for tk in tokens {
|
||||||
|
if tk.flags.contains(TkFlags::KEYWORD) {
|
||||||
|
match tk.as_str() {
|
||||||
|
"then" | "do" => level += 1,
|
||||||
|
"done" | "fi" | "esac" => level = level.saturating_sub(1),
|
||||||
|
_ => { /* Continue */ }
|
||||||
|
}
|
||||||
|
} else if tk.class == TkRule::BraceGrpStart {
|
||||||
|
level += 1;
|
||||||
|
} else if tk.class == TkRule::BraceGrpEnd {
|
||||||
|
level = level.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auto_indent_level = level;
|
||||||
|
}
|
||||||
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
|
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
|
||||||
let buffer = self.buffer.clone();
|
let buffer = self.buffer.clone();
|
||||||
if self.has_hint() {
|
if self.has_hint() {
|
||||||
@@ -2055,9 +2077,10 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
MotionCmd(_, Motion::BeginningOfFirstWord) => {
|
MotionCmd(_, Motion::BeginningOfFirstWord) => {
|
||||||
let start = self.start_of_line();
|
let start = self.start_of_line();
|
||||||
let mut indices = self.directional_indices_iter_from(start, Direction::Forward);
|
self.update_graphemes_lazy();
|
||||||
|
let indices = self.grapheme_indices().to_vec();
|
||||||
let mut first_graphical = None;
|
let mut first_graphical = None;
|
||||||
while let Some(idx) = indices.next() {
|
for &idx in indices.iter().skip(start) {
|
||||||
let grapheme = self.grapheme_at(idx).unwrap();
|
let grapheme = self.grapheme_at(idx).unwrap();
|
||||||
if !is_whitespace(grapheme) {
|
if !is_whitespace(grapheme) {
|
||||||
first_graphical = Some(idx);
|
first_graphical = Some(idx);
|
||||||
@@ -2668,12 +2691,13 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Verb::Dedent => {
|
Verb::Dedent => {
|
||||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
let Some((start, mut end)) = self.range_from_motion(&motion) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if self.grapheme_at(start) == Some("\t") {
|
if self.grapheme_at(start) == Some("\t") {
|
||||||
self.remove(start);
|
self.remove(start);
|
||||||
}
|
}
|
||||||
|
end = end.min(self.grapheme_indices().len().saturating_sub(1));
|
||||||
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
|
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
|
||||||
while let Some(idx) = range_indices.next() {
|
while let Some(idx) = range_indices.next() {
|
||||||
let gr = self.grapheme_at(idx).unwrap();
|
let gr = self.grapheme_at(idx).unwrap();
|
||||||
@@ -2703,14 +2727,32 @@ impl LineBuf {
|
|||||||
Verb::Equalize => todo!(),
|
Verb::Equalize => todo!(),
|
||||||
Verb::InsertModeLineBreak(anchor) => {
|
Verb::InsertModeLineBreak(anchor) => {
|
||||||
let (mut start, end) = self.this_line();
|
let (mut start, end) = self.this_line();
|
||||||
|
let auto_indent = read_shopts(|o| o.prompt.auto_indent);
|
||||||
if start == 0 && end == self.cursor.max {
|
if start == 0 && end == self.cursor.max {
|
||||||
match anchor {
|
match anchor {
|
||||||
Anchor::After => {
|
Anchor::After => {
|
||||||
self.push('\n');
|
self.push('\n');
|
||||||
|
if auto_indent {
|
||||||
|
log::debug!("Calculating indent level for new line");
|
||||||
|
self.calc_indent_level();
|
||||||
|
log::debug!("Auto-indent level: {}", self.auto_indent_level);
|
||||||
|
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||||
|
for tab in tabs {
|
||||||
|
log::debug!("Pushing tab for auto-indent");
|
||||||
|
self.push(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.cursor.set(self.cursor_max());
|
self.cursor.set(self.cursor_max());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Anchor::Before => {
|
Anchor::Before => {
|
||||||
|
if auto_indent {
|
||||||
|
self.calc_indent_level();
|
||||||
|
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||||
|
for tab in tabs {
|
||||||
|
self.insert_at(0, tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.insert_at(0, '\n');
|
self.insert_at(0, '\n');
|
||||||
self.cursor.set(0);
|
self.cursor.set(0);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -2723,11 +2765,28 @@ impl LineBuf {
|
|||||||
Anchor::After => {
|
Anchor::After => {
|
||||||
self.cursor.set(end);
|
self.cursor.set(end);
|
||||||
self.insert_at_cursor('\n');
|
self.insert_at_cursor('\n');
|
||||||
|
self.cursor.add(1);
|
||||||
|
if auto_indent {
|
||||||
|
self.calc_indent_level();
|
||||||
|
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||||
|
for tab in tabs {
|
||||||
|
self.insert_at_cursor(tab);
|
||||||
|
self.cursor.add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Anchor::Before => {
|
Anchor::Before => {
|
||||||
self.cursor.set(start);
|
self.cursor.set(start);
|
||||||
self.insert_at_cursor('\n');
|
self.insert_at_cursor('\n');
|
||||||
self.cursor.add(1);
|
self.cursor.add(1);
|
||||||
|
if auto_indent {
|
||||||
|
self.calc_indent_level();
|
||||||
|
let tabs = (0..self.auto_indent_level).map(|_| '\t');
|
||||||
|
for tab in tabs {
|
||||||
|
self.insert_at_cursor(tab);
|
||||||
|
self.cursor.add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,8 @@ impl FernVi {
|
|||||||
|
|
||||||
if cmd.should_submit() {
|
if cmd.should_submit() {
|
||||||
self.editor.set_hint(None);
|
self.editor.set_hint(None);
|
||||||
self.print_line()?;
|
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
|
||||||
|
self.print_line()?; // Redraw
|
||||||
self.writer.flush_write("\n")?;
|
self.writer.flush_write("\n")?;
|
||||||
let buf = self.editor.take_buf();
|
let buf = self.editor.take_buf();
|
||||||
// Save command to history if auto_hist is enabled
|
// Save command to history if auto_hist is enabled
|
||||||
@@ -305,8 +306,7 @@ impl FernVi {
|
|||||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||||
let tab_stop = crate::state::read_shopts(|s| s.prompt.tab_stop) as u16;
|
Layout::from_parts(cols, &self.prompt, to_cursor, line)
|
||||||
Layout::from_parts(tab_stop, cols, &self.prompt, to_cursor, line)
|
|
||||||
}
|
}
|
||||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||||
/*
|
/*
|
||||||
@@ -323,22 +323,21 @@ impl FernVi {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let entry = self.history.scroll(count);
|
let entry = self.history.scroll(count);
|
||||||
log::info!("Scrolled history, got entry: {:?}", entry.as_ref());
|
|
||||||
if let Some(entry) = entry {
|
if let Some(entry) = entry {
|
||||||
let cursor_pos = self.editor.cursor.get();
|
let editor = std::mem::take(&mut self.editor);
|
||||||
log::info!("Saving pending command to history: {:?} at cursor pos {}", self.editor.as_str(), cursor_pos);
|
|
||||||
let pending = self.editor.take_buf();
|
|
||||||
self.editor.set_buffer(entry.command().to_string());
|
self.editor.set_buffer(entry.command().to_string());
|
||||||
if self.history.pending.is_none() {
|
if self.history.pending.is_none() {
|
||||||
self.history.pending = Some((pending, cursor_pos));
|
self.history.pending = Some(editor);
|
||||||
}
|
}
|
||||||
self.editor.set_hint(None);
|
self.editor.set_hint(None);
|
||||||
self.editor.move_cursor_to_end();
|
self.editor.move_cursor_to_end();
|
||||||
} else if let Some(pending) = self.history.pending.take() {
|
} else if let Some(pending) = self.history.pending.take() {
|
||||||
log::info!("Setting buffer to pending command: {:?}", &pending);
|
self.editor = pending;
|
||||||
self.editor.set_buffer(pending.0);
|
} else {
|
||||||
self.editor.cursor.set(pending.1);
|
// If we are here it should mean we are on our pending command
|
||||||
self.editor.set_hint(None);
|
// And the user tried to scroll history down
|
||||||
|
// Since there is no "future" history, we should just bell and do nothing
|
||||||
|
self.writer.send_bell().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||||
@@ -378,7 +377,6 @@ impl FernVi {
|
|||||||
self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
|
self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
|
||||||
self.highlighter.highlight();
|
self.highlighter.highlight();
|
||||||
let highlighted = self.highlighter.take();
|
let highlighted = self.highlighter.take();
|
||||||
log::info!("Highlighting line. highlighted: {:?}, hint: {:?}", highlighted, hint);
|
|
||||||
format!("{highlighted}{hint}")
|
format!("{highlighted}{hint}")
|
||||||
} else {
|
} else {
|
||||||
format!("{line}{hint}")
|
format!("{line}{hint}")
|
||||||
@@ -677,15 +675,19 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
|||||||
/// - Unimplemented features (comments, brace groups)
|
/// - Unimplemented features (comments, brace groups)
|
||||||
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||||
match class {
|
match class {
|
||||||
TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg => {
|
TkRule::Pipe |
|
||||||
|
TkRule::ErrPipe |
|
||||||
|
TkRule::And |
|
||||||
|
TkRule::Or |
|
||||||
|
TkRule::Bg |
|
||||||
|
TkRule::BraceGrpStart |
|
||||||
|
TkRule::BraceGrpEnd => {
|
||||||
Some(markers::OPERATOR)
|
Some(markers::OPERATOR)
|
||||||
}
|
}
|
||||||
TkRule::Sep => Some(markers::CMD_SEP),
|
TkRule::Sep => Some(markers::CMD_SEP),
|
||||||
TkRule::Redir => Some(markers::REDIRECT),
|
TkRule::Redir => Some(markers::REDIRECT),
|
||||||
TkRule::CasePattern => Some(markers::CASE_PAT),
|
TkRule::CasePattern => Some(markers::CASE_PAT),
|
||||||
TkRule::BraceGrpStart => todo!(),
|
TkRule::Comment => Some(markers::COMMENT),
|
||||||
TkRule::BraceGrpEnd => todo!(),
|
|
||||||
TkRule::Comment => todo!(),
|
|
||||||
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None,
|
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,30 @@ fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a string ends with a newline, ignoring any trailing ANSI escape sequences.
|
||||||
|
fn ends_with_newline(s: &str) -> bool {
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
let mut i = bytes.len();
|
||||||
|
while i > 0 {
|
||||||
|
// ANSI CSI sequences end with an alphabetic byte (e.g. \x1b[0m)
|
||||||
|
if bytes[i - 1].is_ascii_alphabetic() {
|
||||||
|
let term = i - 1;
|
||||||
|
let mut j = term;
|
||||||
|
// Walk back past parameter bytes (digits and ';')
|
||||||
|
while j > 0 && (bytes[j - 1].is_ascii_digit() || bytes[j - 1] == b';') {
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
// Check for CSI introducer \x1b[
|
||||||
|
if j >= 2 && bytes[j - 1] == b'[' && bytes[j - 2] == 0x1b {
|
||||||
|
i = j - 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i > 0 && bytes[i - 1] == b'\n'
|
||||||
|
}
|
||||||
|
|
||||||
// Big credit to rustyline for this
|
// Big credit to rustyline for this
|
||||||
fn width(s: &str, esc_seq: &mut u8) -> u16 {
|
fn width(s: &str, esc_seq: &mut u8) -> u16 {
|
||||||
let w_calc = width_calculator();
|
let w_calc = width_calculator();
|
||||||
@@ -734,15 +758,14 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn from_parts(
|
pub fn from_parts(
|
||||||
tab_stop: u16,
|
|
||||||
term_width: u16,
|
term_width: u16,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
to_cursor: &str,
|
to_cursor: &str,
|
||||||
to_end: &str,
|
to_end: &str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 });
|
let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 });
|
||||||
let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end);
|
let cursor = Self::calc_pos(term_width, to_cursor, prompt_end);
|
||||||
let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end);
|
let end = Self::calc_pos(term_width, to_end, prompt_end);
|
||||||
Layout {
|
Layout {
|
||||||
w_calc: width_calculator(),
|
w_calc: width_calculator(),
|
||||||
prompt_end,
|
prompt_end,
|
||||||
@@ -751,7 +774,8 @@ impl Layout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn calc_pos(tab_stop: u16, term_width: u16, s: &str, orig: Pos) -> Pos {
|
pub fn calc_pos(term_width: u16, s: &str, orig: Pos) -> Pos {
|
||||||
|
const TAB_STOP: u16 = 8;
|
||||||
let mut pos = orig;
|
let mut pos = orig;
|
||||||
let mut esc_seq = 0;
|
let mut esc_seq = 0;
|
||||||
for c in s.graphemes(true) {
|
for c in s.graphemes(true) {
|
||||||
@@ -760,7 +784,7 @@ impl Layout {
|
|||||||
pos.col = 0;
|
pos.col = 0;
|
||||||
}
|
}
|
||||||
let c_width = if c == "\t" {
|
let c_width = if c == "\t" {
|
||||||
tab_stop - (pos.col % tab_stop)
|
TAB_STOP - (pos.col % TAB_STOP)
|
||||||
} else {
|
} else {
|
||||||
width(c, &mut esc_seq)
|
width(c, &mut esc_seq)
|
||||||
};
|
};
|
||||||
@@ -790,7 +814,6 @@ pub struct TermWriter {
|
|||||||
t_cols: Col, // terminal width
|
t_cols: Col, // terminal width
|
||||||
buffer: String,
|
buffer: String,
|
||||||
w_calc: Box<dyn WidthCalculator>,
|
w_calc: Box<dyn WidthCalculator>,
|
||||||
tab_stop: u16,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TermWriter {
|
impl TermWriter {
|
||||||
@@ -802,7 +825,6 @@ impl TermWriter {
|
|||||||
t_cols,
|
t_cols,
|
||||||
buffer: String::new(),
|
buffer: String::new(),
|
||||||
w_calc,
|
w_calc,
|
||||||
tab_stop: 8, // TODO: add a way to configure this
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult<String> {
|
pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult<String> {
|
||||||
@@ -959,7 +981,7 @@ impl LineWriter for TermWriter {
|
|||||||
self.buffer.push_str(prompt);
|
self.buffer.push_str(prompt);
|
||||||
self.buffer.push_str(line);
|
self.buffer.push_str(line);
|
||||||
|
|
||||||
if end.col == 0 && end.row > 0 && !self.buffer.ends_with('\n') {
|
if end.col == 0 && end.row > 0 && !ends_with_newline(&self.buffer) {
|
||||||
// The line has wrapped. We need to use our own line break.
|
// The line has wrapped. We need to use our own line break.
|
||||||
self.buffer.push('\n');
|
self.buffer.push('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/shopt.rs
59
src/shopt.rs
@@ -187,7 +187,7 @@ impl ShOptCore {
|
|||||||
let Ok(val) = val.parse::<usize>() else {
|
let Ok(val) = val.parse::<usize>() else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"shopt: expected a positive integer for hist_ignore_dupes value",
|
"shopt: expected a positive integer for max_hist value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
self.max_hist = val;
|
self.max_hist = val;
|
||||||
@@ -210,16 +210,12 @@ impl ShOptCore {
|
|||||||
};
|
};
|
||||||
self.auto_hist = val;
|
self.auto_hist = val;
|
||||||
}
|
}
|
||||||
"bell_style" => {
|
"bell_enabled" => {
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
let Ok(val) = val.parse::<bool>() else {
|
||||||
return Err(
|
return Err(
|
||||||
ShErr::simple(
|
ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"shopt: expected a bell style for bell_style value",
|
"shopt: expected 'true' or 'false' for bell_enabled value",
|
||||||
)
|
|
||||||
.with_note(
|
|
||||||
Note::new("bell_style takes these options as values")
|
|
||||||
.with_sub_notes(vec!["audible", "visible", "disable"]),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -249,7 +245,7 @@ impl ShOptCore {
|
|||||||
"max_hist",
|
"max_hist",
|
||||||
"interactive_comments",
|
"interactive_comments",
|
||||||
"auto_hist",
|
"auto_hist",
|
||||||
"bell_style",
|
"bell_enabled",
|
||||||
"max_recurse_depth",
|
"max_recurse_depth",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@@ -300,7 +296,7 @@ impl ShOptCore {
|
|||||||
output.push_str(&format!("{}", self.auto_hist));
|
output.push_str(&format!("{}", self.auto_hist));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
"bell_style" => {
|
"bell_enabled" => {
|
||||||
let mut output = String::from("Whether or not to allow fern to trigger the terminal bell");
|
let mut output = String::from("Whether or not to allow fern to trigger the terminal bell");
|
||||||
output.push_str(&format!("{}", self.bell_enabled));
|
output.push_str(&format!("{}", self.bell_enabled));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
@@ -324,7 +320,7 @@ impl ShOptCore {
|
|||||||
"max_hist",
|
"max_hist",
|
||||||
"interactive_comments",
|
"interactive_comments",
|
||||||
"auto_hist",
|
"auto_hist",
|
||||||
"bell_style",
|
"bell_enabled",
|
||||||
"max_recurse_depth",
|
"max_recurse_depth",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@@ -375,7 +371,7 @@ pub struct ShOptPrompt {
|
|||||||
pub edit_mode: FernEditMode,
|
pub edit_mode: FernEditMode,
|
||||||
pub comp_limit: usize,
|
pub comp_limit: usize,
|
||||||
pub highlight: bool,
|
pub highlight: bool,
|
||||||
pub tab_stop: usize,
|
pub auto_indent: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShOptPrompt {
|
impl ShOptPrompt {
|
||||||
@@ -417,14 +413,14 @@ impl ShOptPrompt {
|
|||||||
};
|
};
|
||||||
self.highlight = val;
|
self.highlight = val;
|
||||||
}
|
}
|
||||||
"tab_stop" => {
|
"auto_indent" => {
|
||||||
let Ok(val) = val.parse::<usize>() else {
|
let Ok(val) = val.parse::<bool>() else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"shopt: expected a positive integer for tab_stop value",
|
"shopt: expected 'true' or 'false' for auto_indent value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
self.tab_stop = val;
|
self.auto_indent = val;
|
||||||
}
|
}
|
||||||
"custom" => {
|
"custom" => {
|
||||||
todo!()
|
todo!()
|
||||||
@@ -444,7 +440,7 @@ impl ShOptPrompt {
|
|||||||
"edit_mode",
|
"edit_mode",
|
||||||
"comp_limit",
|
"comp_limit",
|
||||||
"highlight",
|
"highlight",
|
||||||
"tab_stop",
|
"auto_indent",
|
||||||
"custom",
|
"custom",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@@ -484,27 +480,26 @@ impl ShOptPrompt {
|
|||||||
output.push_str(&format!("{}", self.highlight));
|
output.push_str(&format!("{}", self.highlight));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
"tab_stop" => {
|
"auto_indent" => {
|
||||||
let mut output = String::from("The number of spaces used by the tab character '\\t'\n");
|
let mut output = String::from("Whether to automatically indent new lines in multiline commands\n");
|
||||||
output.push_str(&format!("{}", self.tab_stop));
|
output.push_str(&format!("{}", self.auto_indent));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
_ => Err(
|
_ => Err(
|
||||||
ShErr::simple(
|
ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("shopt: Unexpected 'core' option '{query}'"),
|
format!("shopt: Unexpected 'prompt' option '{query}'"),
|
||||||
)
|
)
|
||||||
.with_note(Note::new("options can be accessed like 'core.option_name'"))
|
.with_note(Note::new(
|
||||||
|
"options can be accessed like 'prompt.option_name'",
|
||||||
|
))
|
||||||
.with_note(
|
.with_note(
|
||||||
Note::new("'core' contains the following options").with_sub_notes(vec![
|
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
||||||
"dotglob",
|
"trunc_prompt_path",
|
||||||
"autocd",
|
"edit_mode",
|
||||||
"hist_ignore_dupes",
|
"comp_limit",
|
||||||
"max_hist",
|
"highlight",
|
||||||
"interactive_comments",
|
"auto_indent",
|
||||||
"auto_hist",
|
|
||||||
"bell_style",
|
|
||||||
"max_recurse_depth",
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -520,7 +515,7 @@ impl Display for ShOptPrompt {
|
|||||||
output.push(format!("edit_mode = {}", self.edit_mode));
|
output.push(format!("edit_mode = {}", self.edit_mode));
|
||||||
output.push(format!("comp_limit = {}", self.comp_limit));
|
output.push(format!("comp_limit = {}", self.comp_limit));
|
||||||
output.push(format!("highlight = {}", self.highlight));
|
output.push(format!("highlight = {}", self.highlight));
|
||||||
output.push(format!("tab_stop = {}", self.tab_stop));
|
output.push(format!("auto_indent = {}", self.auto_indent));
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
let final_output = output.join("\n");
|
||||||
|
|
||||||
@@ -535,7 +530,7 @@ impl Default for ShOptPrompt {
|
|||||||
edit_mode: FernEditMode::Vi,
|
edit_mode: FernEditMode::Vi,
|
||||||
comp_limit: 100,
|
comp_limit: 100,
|
||||||
highlight: true,
|
highlight: true,
|
||||||
tab_stop: 4,
|
auto_indent: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,9 +147,7 @@ pub fn hang_up(_: libc::c_int) {
|
|||||||
SHOULD_QUIT.store(true, Ordering::SeqCst);
|
SHOULD_QUIT.store(true, Ordering::SeqCst);
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
write_jobs(|j| {
|
write_jobs(|j| {
|
||||||
for job in j.jobs_mut().iter_mut().flatten() {
|
j.hang_up();
|
||||||
job.killpg(Signal::SIGTERM).ok();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
src/state.rs
35
src/state.rs
@@ -10,16 +10,10 @@ use std::{
|
|||||||
use nix::unistd::{User, gethostname, getppid};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::trap::TrapTarget,
|
builtin::trap::TrapTarget, exec_input, expand::ARG_SEP, jobs::JobTab, libsh::{
|
||||||
exec_input,
|
|
||||||
jobs::JobTab,
|
|
||||||
libsh::{
|
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::VecDequeExt,
|
utils::VecDequeExt,
|
||||||
},
|
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts
|
||||||
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
|
|
||||||
prelude::*,
|
|
||||||
shopt::ShOpts,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Fern {
|
pub struct Fern {
|
||||||
@@ -194,7 +188,8 @@ impl ScopeStack {
|
|||||||
flat_vars
|
flat_vars
|
||||||
}
|
}
|
||||||
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
||||||
if flags.contains(VarFlags::LOCAL) {
|
let is_local = self.is_local_var(var_name);
|
||||||
|
if flags.contains(VarFlags::LOCAL) || is_local {
|
||||||
self.set_var_local(var_name, val, flags);
|
self.set_var_local(var_name, val, flags);
|
||||||
} else {
|
} else {
|
||||||
self.set_var_global(var_name, val, flags);
|
self.set_var_global(var_name, val, flags);
|
||||||
@@ -222,6 +217,21 @@ impl ScopeStack {
|
|||||||
// Fallback to env var
|
// Fallback to env var
|
||||||
std::env::var(var_name).unwrap_or_default()
|
std::env::var(var_name).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
pub fn is_local_var(&self, var_name: &str) -> bool {
|
||||||
|
self.scopes
|
||||||
|
.last()
|
||||||
|
.is_some_and(|s|
|
||||||
|
s.get_var_flags(var_name).is_some_and(|flags| flags.contains(VarFlags::LOCAL))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn get_var_flags(&self, var_name: &str) -> Option<VarFlags> {
|
||||||
|
for scope in self.scopes.iter().rev() {
|
||||||
|
if scope.var_exists(var_name) {
|
||||||
|
return scope.get_var_flags(var_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
pub fn get_param(&self, param: ShellParam) -> String {
|
pub fn get_param(&self, param: ShellParam) -> String {
|
||||||
if param.is_global()
|
if param.is_global()
|
||||||
&& let Some(val) = self.global_params.get(¶m.to_string())
|
&& let Some(val) = self.global_params.get(¶m.to_string())
|
||||||
@@ -587,7 +597,7 @@ impl VarTab {
|
|||||||
fn update_arg_params(&mut self) {
|
fn update_arg_params(&mut self) {
|
||||||
self.set_param(
|
self.set_param(
|
||||||
ShellParam::AllArgs,
|
ShellParam::AllArgs,
|
||||||
&self.sh_argv.clone().to_vec()[1..].join(" "),
|
&self.sh_argv.clone().to_vec()[1..].join(&ARG_SEP.to_string()),
|
||||||
);
|
);
|
||||||
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
||||||
}
|
}
|
||||||
@@ -644,6 +654,9 @@ impl VarTab {
|
|||||||
std::env::var(var).unwrap_or_default()
|
std::env::var(var).unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn get_var_flags(&self, var_name: &str) -> Option<VarFlags> {
|
||||||
|
self.vars.get(var_name).map(|var| var.flags)
|
||||||
|
}
|
||||||
pub fn unset_var(&mut self, var_name: &str) {
|
pub fn unset_var(&mut self, var_name: &str) {
|
||||||
self.vars.remove(var_name);
|
self.vars.remove(var_name);
|
||||||
unsafe { env::remove_var(var_name) };
|
unsafe { env::remove_var(var_name) };
|
||||||
@@ -658,7 +671,7 @@ impl VarTab {
|
|||||||
unsafe { env::set_var(var_name, val) };
|
unsafe { env::set_var(var_name, val) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut var = Var::new(VarKind::Str(val.to_string()), VarFlags::NONE);
|
let mut var = Var::new(VarKind::Str(val.to_string()), flags);
|
||||||
if flags.contains(VarFlags::EXPORT) {
|
if flags.contains(VarFlags::EXPORT) {
|
||||||
var.mark_for_export();
|
var.mark_for_export();
|
||||||
unsafe { env::set_var(var_name, var.to_string()) };
|
unsafe { env::set_var(var_name, var.to_string()) };
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ impl LineWriter for TestWriter {
|
|||||||
fn flush_write(&mut self, _buf: &str) -> libsh::error::ShResult<()> {
|
fn flush_write(&mut self, _buf: &str) -> libsh::error::ShResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_bell(&mut self) -> ShResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: FernVi structure has changed significantly and readline() method no
|
// NOTE: FernVi structure has changed significantly and readline() method no
|
||||||
@@ -598,6 +602,50 @@ fn editor_delete_line_up() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn editor_insert_at_line_start() {
|
||||||
|
// I should move cursor to position 0 when line starts with non-whitespace
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("I", "hello world", 5),
|
||||||
|
("hello world".into(), 0)
|
||||||
|
);
|
||||||
|
// I should skip leading whitespace
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("I", " hello world", 8),
|
||||||
|
(" hello world".into(), 2)
|
||||||
|
);
|
||||||
|
// I should move to the first non-whitespace on the current line in a multiline buffer
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("I", "first line\nsecond line", 14),
|
||||||
|
("first line\nsecond line".into(), 11)
|
||||||
|
);
|
||||||
|
// I should land on position 0 when cursor is already at 0
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("I", "hello", 0),
|
||||||
|
("hello".into(), 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn editor_f_char_from_position_zero() {
|
||||||
|
// f<char> at position 0 should skip the cursor and find the next occurrence
|
||||||
|
// Regression: previously at pos 0, f would match the char under the cursor itself
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("fa", "abcaef", 0),
|
||||||
|
("abcaef".into(), 3) // should find second 'a', not the 'a' at position 0
|
||||||
|
);
|
||||||
|
// f<char> from position 0 finding a char that only appears later
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("fo", "hello world", 0),
|
||||||
|
("hello world".into(), 4)
|
||||||
|
);
|
||||||
|
// f<char> from middle of buffer
|
||||||
|
assert_eq!(
|
||||||
|
normal_cmd("fd", "hello world", 5),
|
||||||
|
("hello world".into(), 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: These tests disabled because fernvi_test() helper is commented out
|
// NOTE: These tests disabled because fernvi_test() helper is commented out
|
||||||
/*
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -283,6 +283,50 @@ fn scopestack_flatten_vars() {
|
|||||||
assert!(flattened.contains_key("LOCAL1"));
|
assert!(flattened.contains_key("LOCAL1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scopestack_local_var_mutation() {
|
||||||
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
|
// Descend into function scope
|
||||||
|
stack.descend(None);
|
||||||
|
|
||||||
|
// `local foo="biz"` — create a local variable with initial value
|
||||||
|
stack.set_var("foo", "biz", VarFlags::LOCAL);
|
||||||
|
assert_eq!(stack.get_var("foo"), "biz");
|
||||||
|
|
||||||
|
// `foo="bar"` — reassign without LOCAL flag (plain assignment)
|
||||||
|
stack.set_var("foo", "bar", VarFlags::NONE);
|
||||||
|
assert_eq!(stack.get_var("foo"), "bar", "Local var should be mutated in place");
|
||||||
|
|
||||||
|
// Ascend back to global
|
||||||
|
stack.ascend();
|
||||||
|
|
||||||
|
// foo should not exist in global scope
|
||||||
|
assert_eq!(stack.get_var("foo"), "", "Local var should not leak to global scope");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scopestack_local_var_uninitialized() {
|
||||||
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
|
// Descend into function scope
|
||||||
|
stack.descend(None);
|
||||||
|
|
||||||
|
// `local foo` — declare without a value
|
||||||
|
stack.set_var("foo", "", VarFlags::LOCAL);
|
||||||
|
assert_eq!(stack.get_var("foo"), "");
|
||||||
|
|
||||||
|
// `foo="bar"` — assign a value later
|
||||||
|
stack.set_var("foo", "bar", VarFlags::NONE);
|
||||||
|
assert_eq!(stack.get_var("foo"), "bar", "Uninitialized local should be assignable");
|
||||||
|
|
||||||
|
// Ascend back to global
|
||||||
|
stack.ascend();
|
||||||
|
|
||||||
|
// foo should not exist in global scope
|
||||||
|
assert_eq!(stack.get_var("foo"), "", "Local var should not leak to global scope");
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LogTab Tests - Functions and Aliases
|
// LogTab Tests - Functions and Aliases
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user