Add Candidate type for case-insensitive completion, shopt_group macro, escape fixes, and vi mode tweaks
This commit is contained in:
@@ -32,7 +32,7 @@ pub fn shopt(node: Node) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (arg, span) in argv {
|
for (arg, span) in argv {
|
||||||
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
|
let Some(mut output) = write_shopts(|s| s.query(&arg)).promote_err(span)? else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ mod tests {
|
|||||||
assert!(out.contains("dotglob"));
|
assert!(out.contains("dotglob"));
|
||||||
assert!(out.contains("autocd"));
|
assert!(out.contains("autocd"));
|
||||||
assert!(out.contains("max_hist"));
|
assert!(out.contains("max_hist"));
|
||||||
assert!(out.contains("edit_mode"));
|
assert!(out.contains("comp_limit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -72,7 +72,7 @@ mod tests {
|
|||||||
assert!(out.contains("dotglob"));
|
assert!(out.contains("dotglob"));
|
||||||
assert!(out.contains("autocd"));
|
assert!(out.contains("autocd"));
|
||||||
// Should not contain prompt opts
|
// Should not contain prompt opts
|
||||||
assert!(!out.contains("edit_mode"));
|
assert!(!out.contains("comp_limit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -107,11 +107,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shopt_set_edit_mode() {
|
fn shopt_set_completion_ignore_case() {
|
||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
test_input("shopt prompt.edit_mode=emacs").unwrap();
|
test_input("shopt prompt.completion_ignore_case=true").unwrap();
|
||||||
let mode = read_shopts(|o| format!("{}", o.prompt.edit_mode));
|
assert!(read_shopts(|o| o.prompt.completion_ignore_case));
|
||||||
assert_eq!(mode, "emacs");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Error cases =====================
|
// ===================== Error cases =====================
|
||||||
|
|||||||
@@ -1224,11 +1224,13 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
while let Some(q_ch) = chars.next() {
|
while let Some(q_ch) = chars.next() {
|
||||||
match q_ch {
|
match q_ch {
|
||||||
'\\' => {
|
'\\' => {
|
||||||
if chars.peek() == Some(&'\'') {
|
match chars.peek() {
|
||||||
result.push('\'');
|
Some(&'\\') |
|
||||||
chars.next();
|
Some(&'\'') => {
|
||||||
} else {
|
let ch = chars.next().unwrap();
|
||||||
result.push('\\');
|
result.push(ch);
|
||||||
|
}
|
||||||
|
_ => result.push(q_ch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\'' => {
|
'\'' => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fmt::{Debug, Write},
|
fmt::{Debug, Display, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
@@ -29,7 +29,103 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn complete_signals(start: &str) -> Vec<String> {
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Candidate(pub String);
|
||||||
|
|
||||||
|
impl Eq for Candidate {}
|
||||||
|
|
||||||
|
impl PartialEq for Candidate {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Candidate {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Candidate {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.0.cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Candidate {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&String> for Candidate {
|
||||||
|
fn from(value: &String) -> Self {
|
||||||
|
Self(value.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Candidate {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Candidate {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", &self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for Candidate {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Candidate {
|
||||||
|
type Target = str;
|
||||||
|
fn deref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Candidate {
|
||||||
|
pub fn is_match(&self, other: &str) -> bool {
|
||||||
|
let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case);
|
||||||
|
if ignore_case {
|
||||||
|
let other_lower = other.to_lowercase();
|
||||||
|
let self_lower = self.0.to_lowercase();
|
||||||
|
self_lower.starts_with(&other_lower)
|
||||||
|
} else {
|
||||||
|
self.0.starts_with(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
self.0.as_bytes()
|
||||||
|
}
|
||||||
|
pub fn starts_with(&self, pat: char) -> bool {
|
||||||
|
self.0.starts_with(pat)
|
||||||
|
}
|
||||||
|
pub fn strip_prefix(&self, prefix: &str) -> Option<String> {
|
||||||
|
let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case);
|
||||||
|
if ignore_case {
|
||||||
|
let old_len = self.0.len();
|
||||||
|
let prefix_lower = prefix.to_lowercase();
|
||||||
|
let self_lower = self.0.to_lowercase();
|
||||||
|
let stripped = self_lower.strip_prefix(&prefix_lower)?;
|
||||||
|
let new_len = stripped.len();
|
||||||
|
let delta = old_len - new_len;
|
||||||
|
Some(self.0[delta..].to_string())
|
||||||
|
} else {
|
||||||
|
self.0.strip_prefix(prefix).map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete_signals(start: &str) -> Vec<Candidate> {
|
||||||
Signal::iterator()
|
Signal::iterator()
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
@@ -37,29 +133,31 @@ pub fn complete_signals(start: &str) -> Vec<String> {
|
|||||||
.unwrap_or(s.as_ref())
|
.unwrap_or(s.as_ref())
|
||||||
.to_string()
|
.to_string()
|
||||||
})
|
})
|
||||||
.filter(|s| s.starts_with(start))
|
.map(Candidate::from)
|
||||||
|
.filter(|s| s.is_match(start))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_aliases(start: &str) -> Vec<String> {
|
pub fn complete_aliases(start: &str) -> Vec<Candidate> {
|
||||||
read_logic(|l| {
|
read_logic(|l| {
|
||||||
l.aliases()
|
l.aliases()
|
||||||
.iter()
|
.keys()
|
||||||
.filter(|a| a.0.starts_with(start))
|
.map(Candidate::from)
|
||||||
.map(|a| a.0.clone())
|
.filter(|a| a.is_match(start))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_jobs(start: &str) -> Vec<String> {
|
pub fn complete_jobs(start: &str) -> Vec<Candidate> {
|
||||||
if let Some(prefix) = start.strip_prefix('%') {
|
if let Some(prefix) = start.strip_prefix('%') {
|
||||||
read_jobs(|j| {
|
read_jobs(|j| {
|
||||||
j.jobs()
|
j.jobs()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|j| j.as_ref())
|
.filter_map(|j| j.as_ref())
|
||||||
.filter_map(|j| j.name())
|
.filter_map(|j| j.name())
|
||||||
.filter(|name| name.starts_with(prefix))
|
.map(Candidate::from)
|
||||||
.map(|name| format!("%{name}"))
|
.filter(|name| name.is_match(prefix))
|
||||||
|
.map(|name| format!("%{name}").into())
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -67,26 +165,26 @@ pub fn complete_jobs(start: &str) -> Vec<String> {
|
|||||||
j.jobs()
|
j.jobs()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|j| j.as_ref())
|
.filter_map(|j| j.as_ref())
|
||||||
.map(|j| j.pgid().to_string())
|
.map(|j| Candidate::from(j.pgid().to_string()))
|
||||||
.filter(|pgid| pgid.starts_with(start))
|
.filter(|pgid| pgid.is_match(start))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_users(start: &str) -> Vec<String> {
|
pub fn complete_users(start: &str) -> Vec<Candidate> {
|
||||||
let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else {
|
let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
passwd
|
passwd
|
||||||
.lines()
|
.lines()
|
||||||
.filter_map(|line| line.split(':').next())
|
.filter_map(|line| line.split(':').next())
|
||||||
.filter(|username| username.starts_with(start))
|
.map(Candidate::from)
|
||||||
.map(|s| s.to_string())
|
.filter(|username| username.is_match(start))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_vars(start: &str) -> Vec<String> {
|
pub fn complete_vars(start: &str) -> Vec<Candidate> {
|
||||||
let Some((var_name, name_start, _end)) = extract_var_name(start) else {
|
let Some((var_name, name_start, _end)) = extract_var_name(start) else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
@@ -101,11 +199,12 @@ pub fn complete_vars(start: &str) -> Vec<String> {
|
|||||||
.keys()
|
.keys()
|
||||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||||
.map(|k| format!("{prefix}{k}"))
|
.map(|k| format!("{prefix}{k}"))
|
||||||
|
.map(Candidate::from)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_vars_raw(raw: &str) -> Vec<String> {
|
pub fn complete_vars_raw(raw: &str) -> Vec<Candidate> {
|
||||||
if !read_vars(|v| v.get_var(raw)).is_empty() {
|
if !read_vars(|v| v.get_var(raw)).is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
@@ -115,7 +214,7 @@ pub fn complete_vars_raw(raw: &str) -> Vec<String> {
|
|||||||
v.flatten_vars()
|
v.flatten_vars()
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|k| k.starts_with(raw) && *k != raw)
|
.filter(|k| k.starts_with(raw) && *k != raw)
|
||||||
.map(|k| k.to_string())
|
.map(Candidate::from)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -168,12 +267,12 @@ pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> {
|
|||||||
Some((name, name_start, name_end))
|
Some((name, name_start, name_end))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_commands(start: &str) -> Vec<String> {
|
fn complete_commands(start: &str) -> Vec<Candidate> {
|
||||||
let mut candidates: Vec<String> = read_meta(|m| {
|
let mut candidates: Vec<Candidate> = read_meta(|m| {
|
||||||
m.cached_cmds()
|
m.cached_cmds()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.starts_with(start))
|
.map(Candidate::from)
|
||||||
.cloned()
|
.filter(|c| c.is_match(start))
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,15 +285,16 @@ fn complete_commands(start: &str) -> Vec<String> {
|
|||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_dirs(start: &str) -> Vec<String> {
|
fn complete_dirs(start: &str) -> Vec<Candidate> {
|
||||||
let filenames = complete_filename(start);
|
let filenames = complete_filename(start);
|
||||||
|
|
||||||
filenames
|
filenames
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|f| std::fs::metadata(f).map(|m| m.is_dir()).unwrap_or(false))
|
.filter(|f| std::fs::metadata(&f.0).map(|m| m.is_dir()).unwrap_or(false))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_filename(start: &str) -> Vec<String> {
|
fn complete_filename(start: &str) -> Vec<Candidate> {
|
||||||
let mut candidates = vec![];
|
let mut candidates = vec![];
|
||||||
let has_dotslash = start.starts_with("./");
|
let has_dotslash = start.starts_with("./");
|
||||||
|
|
||||||
@@ -202,18 +302,18 @@ fn complete_filename(start: &str) -> Vec<String> {
|
|||||||
// Use "." if start is empty (e.g., after "foo=")
|
// Use "." if start is empty (e.g., after "foo=")
|
||||||
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
||||||
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
||||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
// Completing inside a directory: "src/" -> dir="src/", prefix=""
|
||||||
(path, "")
|
(path, "")
|
||||||
} else if let Some(parent) = path.parent()
|
} else if let Some(parent) = path.parent()
|
||||||
&& !parent.as_os_str().is_empty()
|
&& !parent.as_os_str().is_empty()
|
||||||
{
|
{
|
||||||
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
// Has directory component: "src/ma" -> dir="src", prefix="ma"
|
||||||
(
|
(
|
||||||
parent.to_path_buf(),
|
parent.to_path_buf(),
|
||||||
path.file_name().unwrap().to_str().unwrap_or(""),
|
path.file_name().unwrap().to_str().unwrap_or(""),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// No directory: "fil" → dir=".", prefix="fil"
|
// No directory: "fil" -> dir=".", prefix="fil"
|
||||||
(PathBuf::from("."), start)
|
(PathBuf::from("."), start)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,14 +323,16 @@ fn complete_filename(start: &str) -> Vec<String> {
|
|||||||
|
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let file_name = entry.file_name();
|
let file_name = entry.file_name();
|
||||||
let file_str = file_name.to_string_lossy();
|
let file_str: Candidate = file_name.to_string_lossy().to_string().into();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Skip hidden files unless explicitly requested
|
// Skip hidden files unless explicitly requested
|
||||||
if !prefix.starts_with('.') && file_str.starts_with('.') {
|
if !prefix.starts_with('.') && file_str.0.starts_with('.') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if file_str.starts_with(prefix) {
|
if file_str.is_match(prefix) {
|
||||||
// Reconstruct full path
|
// Reconstruct full path
|
||||||
let mut full_path = dir.join(&file_name);
|
let mut full_path = dir.join(&file_name);
|
||||||
|
|
||||||
@@ -244,7 +346,7 @@ fn complete_filename(start: &str) -> Vec<String> {
|
|||||||
path_raw = path_raw.trim_start_matches("./").to_string();
|
path_raw = path_raw.trim_start_matches("./").to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.push(path_raw);
|
candidates.push(path_raw.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +465,7 @@ impl BashCompSpec {
|
|||||||
source: String::new(),
|
source: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn exec_comp_func(&self, ctx: &CompContext) -> ShResult<Vec<String>> {
|
pub fn exec_comp_func(&self, ctx: &CompContext) -> ShResult<Vec<Candidate>> {
|
||||||
let mut vars_to_unset = HashSet::new();
|
let mut vars_to_unset = HashSet::new();
|
||||||
for var in [
|
for var in [
|
||||||
"COMP_WORDS",
|
"COMP_WORDS",
|
||||||
@@ -426,13 +528,19 @@ impl BashCompSpec {
|
|||||||
);
|
);
|
||||||
exec_input(input, None, false, Some("comp_function".into()))?;
|
exec_input(input, None, false, Some("comp_function".into()))?;
|
||||||
|
|
||||||
Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default())
|
let comp_reply = read_vars(|v| v.get_arr_elems("COMPREPLY"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(Candidate::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(comp_reply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompSpec for BashCompSpec {
|
impl CompSpec for BashCompSpec {
|
||||||
fn complete(&self, ctx: &CompContext) -> ShResult<Vec<String>> {
|
fn complete(&self, ctx: &CompContext) -> ShResult<Vec<Candidate>> {
|
||||||
let mut candidates = vec![];
|
let mut candidates: Vec<Candidate> = vec![];
|
||||||
let prefix = &ctx.words[ctx.cword];
|
let prefix = &ctx.words[ctx.cword];
|
||||||
|
|
||||||
let expanded = prefix.clone().expand()?.get_words().join(" ");
|
let expanded = prefix.clone().expand()?.get_words().join(" ");
|
||||||
@@ -461,7 +569,7 @@ impl CompSpec for BashCompSpec {
|
|||||||
candidates.extend(complete_signals(&expanded));
|
candidates.extend(complete_signals(&expanded));
|
||||||
}
|
}
|
||||||
if let Some(words) = &self.wordlist {
|
if let Some(words) = &self.wordlist {
|
||||||
candidates.extend(words.iter().filter(|w| w.starts_with(&expanded)).cloned());
|
candidates.extend(words.iter().map(Candidate::from).filter(|w| w.is_match(&expanded)));
|
||||||
}
|
}
|
||||||
if self.function.is_some() {
|
if self.function.is_some() {
|
||||||
candidates.extend(self.exec_comp_func(ctx)?);
|
candidates.extend(self.exec_comp_func(ctx)?);
|
||||||
@@ -469,12 +577,12 @@ impl CompSpec for BashCompSpec {
|
|||||||
candidates = candidates
|
candidates = candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
let stripped = c.strip_prefix(&expanded).unwrap_or_default();
|
let stripped = c.0.strip_prefix(&expanded).unwrap_or_default();
|
||||||
format!("{prefix}{stripped}")
|
format!("{prefix}{stripped}").into()
|
||||||
})
|
})
|
||||||
.collect();
|
.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.0.len()); // sort by length to prioritize shorter completions, ties are then sorted alphabetically
|
||||||
|
|
||||||
Ok(candidates)
|
Ok(candidates)
|
||||||
}
|
}
|
||||||
@@ -489,7 +597,7 @@ impl CompSpec for BashCompSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait CompSpec: Debug + CloneCompSpec {
|
pub trait CompSpec: Debug + CloneCompSpec {
|
||||||
fn complete(&self, ctx: &CompContext) -> ShResult<Vec<String>>;
|
fn complete(&self, ctx: &CompContext) -> ShResult<Vec<Candidate>>;
|
||||||
fn source(&self) -> &str;
|
fn source(&self) -> &str;
|
||||||
fn get_flags(&self) -> CompOptFlags {
|
fn get_flags(&self) -> CompOptFlags {
|
||||||
CompOptFlags::empty()
|
CompOptFlags::empty()
|
||||||
@@ -527,17 +635,17 @@ impl CompContext {
|
|||||||
|
|
||||||
pub enum CompResult {
|
pub enum CompResult {
|
||||||
NoMatch,
|
NoMatch,
|
||||||
Single { result: String },
|
Single { result: Candidate },
|
||||||
Many { candidates: Vec<String> },
|
Many { candidates: Vec<Candidate> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompResult {
|
impl CompResult {
|
||||||
pub fn from_candidates(candidates: Vec<String>) -> Self {
|
pub fn from_candidates(mut candidates: Vec<Candidate>) -> Self {
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
Self::NoMatch
|
Self::NoMatch
|
||||||
} else if candidates.len() == 1 {
|
} else if candidates.len() == 1 {
|
||||||
Self::Single {
|
Self::Single {
|
||||||
result: candidates[0].clone(),
|
result: candidates.remove(0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Self::Many { candidates }
|
Self::Many { candidates }
|
||||||
@@ -568,7 +676,7 @@ pub trait Completer {
|
|||||||
fn reset(&mut self);
|
fn reset(&mut self);
|
||||||
fn reset_stay_active(&mut self);
|
fn reset_stay_active(&mut self);
|
||||||
fn is_active(&self) -> bool;
|
fn is_active(&self) -> bool;
|
||||||
fn all_candidates(&self) -> Vec<String> {
|
fn all_candidates(&self) -> Vec<Candidate> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
fn selected_candidate(&self) -> Option<String>;
|
fn selected_candidate(&self) -> Option<String>;
|
||||||
@@ -671,6 +779,15 @@ impl From<String> for ScoredCandidate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Candidate> for ScoredCandidate {
|
||||||
|
fn from(candidate: Candidate) -> Self {
|
||||||
|
Self {
|
||||||
|
content: candidate.0,
|
||||||
|
score: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FuzzyLayout {
|
pub struct FuzzyLayout {
|
||||||
rows: u16,
|
rows: u16,
|
||||||
@@ -743,7 +860,7 @@ impl QueryEditor {
|
|||||||
pub struct FuzzySelector {
|
pub struct FuzzySelector {
|
||||||
query: QueryEditor,
|
query: QueryEditor,
|
||||||
filtered: Vec<ScoredCandidate>,
|
filtered: Vec<ScoredCandidate>,
|
||||||
candidates: Vec<String>,
|
candidates: Vec<Candidate>,
|
||||||
cursor: ClampedUsize,
|
cursor: ClampedUsize,
|
||||||
number_candidates: bool,
|
number_candidates: bool,
|
||||||
old_layout: Option<FuzzyLayout>,
|
old_layout: Option<FuzzyLayout>,
|
||||||
@@ -798,7 +915,7 @@ impl FuzzySelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn candidates(&self) -> &[String] {
|
pub fn candidates(&self) -> &[Candidate] {
|
||||||
&self.candidates
|
&self.candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,7 +931,7 @@ impl FuzzySelector {
|
|||||||
self.candidates.len()
|
self.candidates.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(&mut self, candidates: Vec<String>) {
|
pub fn activate(&mut self, candidates: Vec<Candidate>) {
|
||||||
self.active = true;
|
self.active = true;
|
||||||
self.candidates = candidates;
|
self.candidates = candidates;
|
||||||
self.score_candidates();
|
self.score_candidates();
|
||||||
@@ -913,7 +1030,7 @@ impl FuzzySelector {
|
|||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| {
|
.filter_map(|c| {
|
||||||
let mut sc = ScoredCandidate::new(c);
|
let mut sc = ScoredCandidate::new(c.to_string());
|
||||||
let score = sc.fuzzy_score(self.query.linebuf.as_str());
|
let score = sc.fuzzy_score(self.query.linebuf.as_str());
|
||||||
if score > i32::MIN { Some(sc) } else { None }
|
if score > i32::MIN { Some(sc) } else { None }
|
||||||
})
|
})
|
||||||
@@ -1167,7 +1284,7 @@ impl Default for FuzzyCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for FuzzyCompleter {
|
impl Completer for FuzzyCompleter {
|
||||||
fn all_candidates(&self) -> Vec<String> {
|
fn all_candidates(&self) -> Vec<Candidate> {
|
||||||
self.selector.candidates.clone()
|
self.selector.candidates.clone()
|
||||||
}
|
}
|
||||||
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
|
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
|
||||||
@@ -1188,12 +1305,34 @@ impl Completer for FuzzyCompleter {
|
|||||||
.original_input
|
.original_input
|
||||||
.get(start..end)
|
.get(start..end)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case);
|
||||||
|
let (prefix, completion) = if ignore_case {
|
||||||
|
// Replace the filename part (after last /) with the candidate's casing
|
||||||
|
// but preserve any unexpanded prefix like $VAR/
|
||||||
|
if let Some(last_sep) = slice.rfind('/') {
|
||||||
|
let prefix_end = start + last_sep + 1;
|
||||||
|
let trailing_slash = selected.ends_with('/');
|
||||||
|
let trimmed = selected.trim_end_matches('/');
|
||||||
|
let mut basename = trimmed.rsplit('/').next().unwrap_or(&selected).to_string();
|
||||||
|
if trailing_slash {
|
||||||
|
basename.push('/');
|
||||||
|
}
|
||||||
|
(
|
||||||
|
self.completer.original_input[..prefix_end].to_string(),
|
||||||
|
basename,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(self.completer.original_input[..start].to_string(), selected.clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
start += slice.width();
|
start += slice.width();
|
||||||
let completion = selected.strip_prefix(slice).unwrap_or(&selected);
|
let completion = selected.strip_prefix(slice).unwrap_or(&selected);
|
||||||
let escaped = escape_str(completion, false);
|
(self.completer.original_input[..start].to_string(), completion.to_string())
|
||||||
|
};
|
||||||
|
let escaped = escape_str(&completion, false);
|
||||||
let ret = format!(
|
let ret = format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
&self.completer.original_input[..start],
|
prefix,
|
||||||
escaped,
|
escaped,
|
||||||
&self.completer.original_input[end..]
|
&self.completer.original_input[end..]
|
||||||
);
|
);
|
||||||
@@ -1256,7 +1395,7 @@ impl Completer for FuzzyCompleter {
|
|||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct SimpleCompleter {
|
pub struct SimpleCompleter {
|
||||||
pub candidates: Vec<String>,
|
pub candidates: Vec<Candidate>,
|
||||||
pub selected_idx: usize,
|
pub selected_idx: usize,
|
||||||
pub original_input: String,
|
pub original_input: String,
|
||||||
pub token_span: (usize, usize),
|
pub token_span: (usize, usize),
|
||||||
@@ -1266,7 +1405,7 @@ pub struct SimpleCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for SimpleCompleter {
|
impl Completer for SimpleCompleter {
|
||||||
fn all_candidates(&self) -> Vec<String> {
|
fn all_candidates(&self) -> Vec<Candidate> {
|
||||||
self.candidates.clone()
|
self.candidates.clone()
|
||||||
}
|
}
|
||||||
fn reset_stay_active(&mut self) {
|
fn reset_stay_active(&mut self) {
|
||||||
@@ -1299,7 +1438,7 @@ impl Completer for SimpleCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn selected_candidate(&self) -> Option<String> {
|
fn selected_candidate(&self) -> Option<String> {
|
||||||
self.candidates.get(self.selected_idx).cloned()
|
self.candidates.get(self.selected_idx).map(|c| c.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_span(&self) -> (usize, usize) {
|
fn token_span(&self) -> (usize, usize) {
|
||||||
@@ -1407,7 +1546,7 @@ impl SimpleCompleter {
|
|||||||
&& !ends_with_unescaped(&c, " ")
|
&& !ends_with_unescaped(&c, " ")
|
||||||
{
|
{
|
||||||
// already has a space
|
// already has a space
|
||||||
format!("{} ", c)
|
Candidate::from(format!("{} ", c))
|
||||||
} else {
|
} else {
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
@@ -1449,12 +1588,32 @@ impl SimpleCompleter {
|
|||||||
let selected = &self.candidates[self.selected_idx];
|
let selected = &self.candidates[self.selected_idx];
|
||||||
let (mut start, end) = self.token_span;
|
let (mut start, end) = self.token_span;
|
||||||
let slice = self.original_input.get(start..end).unwrap_or("");
|
let slice = self.original_input.get(start..end).unwrap_or("");
|
||||||
|
let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case);
|
||||||
|
let (prefix, completion) = if ignore_case {
|
||||||
|
if let Some(last_sep) = slice.rfind('/') {
|
||||||
|
let prefix_end = start + last_sep + 1;
|
||||||
|
let trailing_slash = selected.ends_with('/');
|
||||||
|
let trimmed = selected.trim_end_matches('/');
|
||||||
|
let mut basename = trimmed.rsplit('/').next().unwrap_or(selected.as_str()).to_string();
|
||||||
|
if trailing_slash {
|
||||||
|
basename.push('/');
|
||||||
|
}
|
||||||
|
(
|
||||||
|
self.original_input[..prefix_end].to_string(),
|
||||||
|
basename,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(self.original_input[..start].to_string(), selected.to_string())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
start += slice.width();
|
start += slice.width();
|
||||||
let completion = selected.strip_prefix(slice).unwrap_or(selected);
|
let completion = selected.strip_prefix(slice).unwrap_or(selected.to_string());
|
||||||
let escaped = escape_str(completion, false);
|
(self.original_input[..start].to_string(), completion)
|
||||||
|
};
|
||||||
|
let escaped = escape_str(&completion, false);
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
&self.original_input[..start],
|
prefix,
|
||||||
escaped,
|
escaped,
|
||||||
&self.original_input[end..]
|
&self.original_input[end..]
|
||||||
)
|
)
|
||||||
@@ -1649,11 +1808,12 @@ impl SimpleCompleter {
|
|||||||
let is_var_completion = last_marker == Some(markers::VAR_SUB)
|
let is_var_completion = last_marker == Some(markers::VAR_SUB)
|
||||||
&& !candidates.is_empty()
|
&& !candidates.is_empty()
|
||||||
&& candidates.iter().any(|c| c.starts_with('$'));
|
&& candidates.iter().any(|c| c.starts_with('$'));
|
||||||
if !is_var_completion {
|
let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case);
|
||||||
|
if !is_var_completion && !ignore_case {
|
||||||
candidates = candidates
|
candidates = candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| match c.strip_prefix(&expanded) {
|
.map(|c| match c.strip_prefix(&expanded) {
|
||||||
Some(suffix) => format!("{raw_tk}{suffix}"),
|
Some(suffix) => Candidate::from(format!("{raw_tk}{suffix}")),
|
||||||
None => c,
|
None => c,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1781,7 +1941,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn complete_signals_int() {
|
fn complete_signals_int() {
|
||||||
let results = complete_signals("INT");
|
let results = complete_signals("INT");
|
||||||
assert!(results.contains(&"INT".to_string()));
|
assert!(results.contains(&Candidate::from("INT")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ impl History {
|
|||||||
.search_mask
|
.search_mask
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ent| ent.command().to_string());
|
.map(|ent| super::complete::Candidate::from(ent.command()));
|
||||||
self.fuzzy_finder.activate(raw_entries.collect());
|
self.fuzzy_finder.activate(raw_entries.collect());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -675,8 +675,7 @@ impl ShedVi {
|
|||||||
|
|
||||||
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
||||||
if self.mode.report_mode() != ModeReport::Ex
|
if self.mode.report_mode() != ModeReport::Ex
|
||||||
&& self.editor.attempt_history_expansion(&self.history)
|
&& self.editor.attempt_history_expansion(&self.history) {
|
||||||
{
|
|
||||||
// If history expansion occurred, don't attempt completion yet
|
// If history expansion occurred, don't attempt completion yet
|
||||||
// allow the user to see the expanded command and accept or edit it before completing
|
// allow the user to see the expanded command and accept or edit it before completing
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -888,12 +887,14 @@ impl ShedVi {
|
|||||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||||
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
|
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
|
||||||
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
|
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
|
||||||
|
log::debug!("is_ex_cmd: {is_ex_cmd}");
|
||||||
if is_shell_cmd {
|
if is_shell_cmd {
|
||||||
self.old_layout = None;
|
self.old_layout = None;
|
||||||
}
|
}
|
||||||
if is_ex_cmd {
|
if is_ex_cmd {
|
||||||
self.ex_history.push(cmd.raw_seq.clone());
|
self.ex_history.push(cmd.raw_seq.clone());
|
||||||
self.ex_history.reset();
|
self.ex_history.reset();
|
||||||
|
log::debug!("ex_history: {:?}", self.ex_history.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
let before = self.editor.buffer.clone();
|
let before = self.editor.buffer.clone();
|
||||||
@@ -1415,7 +1416,11 @@ impl ShedVi {
|
|||||||
|
|
||||||
self.editor.exec_cmd(cmd.clone())?;
|
self.editor.exec_cmd(cmd.clone())?;
|
||||||
|
|
||||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some() {
|
if self.mode.report_mode() == ModeReport::Visual
|
||||||
|
&& cmd
|
||||||
|
.verb()
|
||||||
|
.is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank)
|
||||||
|
{
|
||||||
self.editor.stop_selecting();
|
self.editor.stop_selecting();
|
||||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||||
self.swap_mode(&mut mode);
|
self.swap_mode(&mut mode);
|
||||||
|
|||||||
@@ -307,6 +307,9 @@ impl Verb {
|
|||||||
| Self::JoinLines
|
| Self::JoinLines
|
||||||
| Self::InsertChar(_)
|
| Self::InsertChar(_)
|
||||||
| Self::Insert(_)
|
| Self::Insert(_)
|
||||||
|
| Self::Dedent
|
||||||
|
| Self::Indent
|
||||||
|
| Self::Equalize
|
||||||
| Self::Rot13
|
| Self::Rot13
|
||||||
| Self::EndOfFile
|
| Self::EndOfFile
|
||||||
| Self::IncrementNumber(_)
|
| Self::IncrementNumber(_)
|
||||||
|
|||||||
635
src/shopt.rs
635
src/shopt.rs
@@ -2,6 +2,35 @@ use std::{fmt::Display, str::FromStr};
|
|||||||
|
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
|
|
||||||
|
/// Escapes a string for embedding inside single quotes.
|
||||||
|
/// Only escapes unescaped `\` and `'` characters.
|
||||||
|
pub fn escape_for_single_quote(s: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(s.len());
|
||||||
|
let mut chars = s.chars().peekable();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch == '\\' {
|
||||||
|
match chars.peek() {
|
||||||
|
Some(&'\\') | Some(&'\'') => {
|
||||||
|
// Already escaped — pass through both characters
|
||||||
|
result.push(ch);
|
||||||
|
result.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Lone backslash — escape it
|
||||||
|
result.push('\\');
|
||||||
|
result.push('\\');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ch == '\'' {
|
||||||
|
result.push('\\');
|
||||||
|
result.push('\'');
|
||||||
|
} else {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum ShedBellStyle {
|
pub enum ShedBellStyle {
|
||||||
Audible,
|
Audible,
|
||||||
@@ -24,34 +53,97 @@ impl FromStr for ShedBellStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, Debug)]
|
/// Generates a shopt group struct with `set`, `get`, `Display`, and `Default` impls.
|
||||||
pub enum ShedEditMode {
|
///
|
||||||
#[default]
|
/// Doc comments on each field become the description shown by `shopt get`.
|
||||||
Vi,
|
/// Every field type must implement `FromStr + Display`.
|
||||||
Emacs,
|
///
|
||||||
}
|
/// Optional per-field validation: `#[validate(|val| expr)]` runs after parsing
|
||||||
|
/// and must return `Result<(), String>` where the error string is the message.
|
||||||
|
macro_rules! shopt_group {
|
||||||
|
(
|
||||||
|
$(#[$struct_meta:meta])*
|
||||||
|
pub struct $name:ident ($group_name:literal) {
|
||||||
|
$(
|
||||||
|
$(#[doc = $desc:literal])*
|
||||||
|
$(#[validate($validator:expr)])?
|
||||||
|
$field:ident : $ty:ty = $default:expr
|
||||||
|
),* $(,)?
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
$(#[$struct_meta])*
|
||||||
|
pub struct $name {
|
||||||
|
$(pub $field: $ty,)*
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for ShedEditMode {
|
impl Default for $name {
|
||||||
type Err = ShErr;
|
fn default() -> Self {
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
Self {
|
||||||
match s.to_ascii_lowercase().as_str() {
|
$($field: $default,)*
|
||||||
"vi" => Ok(Self::Vi),
|
}
|
||||||
"emacs" => Ok(Self::Emacs),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> {
|
||||||
|
match opt {
|
||||||
|
$(
|
||||||
|
stringify!($field) => {
|
||||||
|
let parsed = val.parse::<$ty>().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
|
ShErrKind::SyntaxErr,
|
||||||
|
format!("shopt: invalid value '{}' for {}.{}", val, $group_name, opt),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
$(
|
||||||
|
let validate: fn(&$ty) -> Result<(), String> = $validator;
|
||||||
|
validate(&parsed).map_err(|msg| {
|
||||||
|
ShErr::simple(ShErrKind::SyntaxErr, format!("shopt: {msg}"))
|
||||||
|
})?;
|
||||||
|
)?
|
||||||
|
self.$field = parsed;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
_ => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::SyntaxErr,
|
||||||
|
format!("shopt: unexpected '{}' option '{opt}'", $group_name),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, query: &str) -> ShResult<Option<String>> {
|
||||||
|
if query.is_empty() {
|
||||||
|
return Ok(Some(format!("{self}")));
|
||||||
|
}
|
||||||
|
match query {
|
||||||
|
$(
|
||||||
|
stringify!($field) => {
|
||||||
|
let desc = concat!($($desc, "\n",)*);
|
||||||
|
let output = format!("{}{}", desc, self.$field);
|
||||||
|
Ok(Some(output))
|
||||||
|
}
|
||||||
|
)*
|
||||||
_ => Err(ShErr::simple(
|
_ => Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("Invalid edit mode '{s}'"),
|
format!("shopt: unexpected '{}' option '{query}'", $group_name),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ShedEditMode {
|
impl Display for $name {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
let output = [
|
||||||
ShedEditMode::Vi => write!(f, "vi"),
|
$(format!("{}.{}='{}'", $group_name, stringify!($field),
|
||||||
ShedEditMode::Emacs => write!(f, "emacs"),
|
$crate::shopt::escape_for_single_quote(&self.$field.to_string())),)*
|
||||||
|
];
|
||||||
|
writeln!(f, "{}", output.join("\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -82,8 +174,8 @@ impl ShOpts {
|
|||||||
|
|
||||||
pub fn display_opts(&mut self) -> ShResult<String> {
|
pub fn display_opts(&mut self) -> ShResult<String> {
|
||||||
let output = [
|
let output = [
|
||||||
format!("core:\n{}", self.query("core")?.unwrap_or_default()),
|
self.query("core")?.unwrap_or_default().to_string(),
|
||||||
format!("prompt:\n{}", self.query("prompt")?.unwrap_or_default()),
|
self.query("prompt")?.unwrap_or_default().to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Ok(output.join("\n"))
|
Ok(output.join("\n"))
|
||||||
@@ -135,459 +227,78 @@ impl ShOpts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
shopt_group! {
|
||||||
pub struct ShOptCore {
|
#[derive(Clone, Debug)]
|
||||||
pub dotglob: bool,
|
pub struct ShOptCore ("core") {
|
||||||
pub autocd: bool,
|
/// Include hidden files in glob patterns
|
||||||
pub hist_ignore_dupes: bool,
|
dotglob: bool = false,
|
||||||
pub max_hist: isize,
|
|
||||||
pub interactive_comments: bool,
|
|
||||||
pub auto_hist: bool,
|
|
||||||
pub bell_enabled: bool,
|
|
||||||
pub max_recurse_depth: usize,
|
|
||||||
pub xpg_echo: bool,
|
|
||||||
pub noclobber: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShOptCore {
|
/// Allow navigation to directories by passing the directory as a command directly
|
||||||
pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> {
|
autocd: bool = false,
|
||||||
match opt {
|
|
||||||
"dotglob" => {
|
/// Ignore consecutive duplicate command history entries
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
hist_ignore_dupes: bool = true,
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
/// Maximum number of entries in the command history file (-1 for unlimited)
|
||||||
"shopt: expected 'true' or 'false' for dotglob value",
|
#[validate(|v: &isize| if *v < -1 {
|
||||||
));
|
Err("expected a non-negative integer or -1 for max_hist value".into())
|
||||||
};
|
} else {
|
||||||
self.dotglob = val;
|
|
||||||
}
|
|
||||||
"autocd" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for autocd value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.autocd = val;
|
|
||||||
}
|
|
||||||
"hist_ignore_dupes" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for hist_ignore_dupes value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.hist_ignore_dupes = val;
|
|
||||||
}
|
|
||||||
"max_hist" => {
|
|
||||||
let Ok(val) = val.parse::<isize>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected an integer for max_hist value (-1 for unlimited)",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
if val < -1 {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected a non-negative integer or -1 for max_hist value",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.max_hist = val;
|
|
||||||
}
|
|
||||||
"interactive_comments" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for interactive_comments value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.interactive_comments = val;
|
|
||||||
}
|
|
||||||
"auto_hist" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for auto_hist value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.auto_hist = val;
|
|
||||||
}
|
|
||||||
"bell_enabled" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for bell_enabled value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.bell_enabled = val;
|
|
||||||
}
|
|
||||||
"max_recurse_depth" => {
|
|
||||||
let Ok(val) = val.parse::<usize>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected a positive integer for max_recurse_depth value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.max_recurse_depth = val;
|
|
||||||
}
|
|
||||||
"xpg_echo" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for xpg_echo value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.xpg_echo = val;
|
|
||||||
}
|
|
||||||
"noclobber" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for noclobber value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.noclobber = val;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
format!("shopt: Unexpected 'core' option '{opt}'"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
})]
|
||||||
pub fn get(&self, query: &str) -> ShResult<Option<String>> {
|
max_hist: isize = 10_000,
|
||||||
if query.is_empty() {
|
|
||||||
return Ok(Some(format!("{self}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
match query {
|
/// Whether or not to allow comments in interactive mode
|
||||||
"dotglob" => {
|
interactive_comments: bool = true,
|
||||||
let mut output = String::from("Include hidden files in glob patterns\n");
|
|
||||||
output.push_str(&format!("{}", self.dotglob));
|
/// Whether or not to automatically save commands to the command history file
|
||||||
Ok(Some(output))
|
auto_hist: bool = true,
|
||||||
}
|
|
||||||
"autocd" => {
|
/// Whether or not to allow shed to trigger the terminal bell
|
||||||
let mut output = String::from(
|
bell_enabled: bool = true,
|
||||||
"Allow navigation to directories by passing the directory as a command directly\n",
|
|
||||||
);
|
/// Maximum limit of recursive shell function calls
|
||||||
output.push_str(&format!("{}", self.autocd));
|
max_recurse_depth: usize = 1000,
|
||||||
Ok(Some(output))
|
|
||||||
}
|
/// Whether echo expands escape sequences by default
|
||||||
"hist_ignore_dupes" => {
|
xpg_echo: bool = false,
|
||||||
let mut output = String::from("Ignore consecutive duplicate command history entries\n");
|
|
||||||
output.push_str(&format!("{}", self.hist_ignore_dupes));
|
/// Prevent > from overwriting existing files (use >| to override)
|
||||||
Ok(Some(output))
|
noclobber: bool = false,
|
||||||
}
|
|
||||||
"max_hist" => {
|
|
||||||
let mut output = String::from(
|
|
||||||
"Maximum number of entries in the command history file (-1 for unlimited)\n",
|
|
||||||
);
|
|
||||||
output.push_str(&format!("{}", self.max_hist));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"interactive_comments" => {
|
|
||||||
let mut output = String::from("Whether or not to allow comments in interactive mode\n");
|
|
||||||
output.push_str(&format!("{}", self.interactive_comments));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"auto_hist" => {
|
|
||||||
let mut output = String::from(
|
|
||||||
"Whether or not to automatically save commands to the command history file\n",
|
|
||||||
);
|
|
||||||
output.push_str(&format!("{}", self.auto_hist));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"bell_enabled" => {
|
|
||||||
let mut output = String::from("Whether or not to allow shed to trigger the terminal bell");
|
|
||||||
output.push_str(&format!("{}", self.bell_enabled));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"max_recurse_depth" => {
|
|
||||||
let mut output = String::from("Maximum limit of recursive shell function calls\n");
|
|
||||||
output.push_str(&format!("{}", self.max_recurse_depth));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"xpg_echo" => {
|
|
||||||
let mut output = String::from("Whether echo expands escape sequences by default\n");
|
|
||||||
output.push_str(&format!("{}", self.xpg_echo));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"noclobber" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("Prevent > from overwriting existing files (use >| to override)\n");
|
|
||||||
output.push_str(&format!("{}", self.noclobber));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
_ => Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
format!("shopt: Unexpected 'core' option '{query}'"),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ShOptCore {
|
shopt_group! {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
#[derive(Clone, Debug)]
|
||||||
let mut output = vec![];
|
pub struct ShOptPrompt ("prompt") {
|
||||||
output.push(format!("dotglob = {}", self.dotglob));
|
/// Maximum number of path segments used in the '\W' prompt escape sequence
|
||||||
output.push(format!("autocd = {}", self.autocd));
|
trunc_prompt_path: usize = 4,
|
||||||
output.push(format!("hist_ignore_dupes = {}", self.hist_ignore_dupes));
|
|
||||||
output.push(format!("max_hist = {}", self.max_hist));
|
|
||||||
output.push(format!(
|
|
||||||
"interactive_comments = {}",
|
|
||||||
self.interactive_comments
|
|
||||||
));
|
|
||||||
output.push(format!("auto_hist = {}", self.auto_hist));
|
|
||||||
output.push(format!("bell_enabled = {}", self.bell_enabled));
|
|
||||||
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
|
|
||||||
output.push(format!("xpg_echo = {}", self.xpg_echo));
|
|
||||||
output.push(format!("noclobber = {}", self.noclobber));
|
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
/// Maximum number of completion candidates displayed upon pressing tab
|
||||||
|
comp_limit: usize = 100,
|
||||||
|
|
||||||
writeln!(f, "{final_output}")
|
/// Whether to enable or disable syntax highlighting on the prompt
|
||||||
}
|
highlight: bool = true,
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ShOptCore {
|
/// Whether to automatically indent new lines in multiline commands
|
||||||
fn default() -> Self {
|
auto_indent: bool = true,
|
||||||
ShOptCore {
|
|
||||||
dotglob: false,
|
|
||||||
autocd: false,
|
|
||||||
hist_ignore_dupes: true,
|
|
||||||
max_hist: 10_000,
|
|
||||||
interactive_comments: true,
|
|
||||||
auto_hist: true,
|
|
||||||
bell_enabled: true,
|
|
||||||
max_recurse_depth: 1000,
|
|
||||||
xpg_echo: false,
|
|
||||||
noclobber: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
/// Whether to automatically insert a newline when the input is incomplete
|
||||||
pub struct ShOptPrompt {
|
linebreak_on_incomplete: bool = true,
|
||||||
pub trunc_prompt_path: usize,
|
|
||||||
pub edit_mode: ShedEditMode,
|
|
||||||
pub comp_limit: usize,
|
|
||||||
pub highlight: bool,
|
|
||||||
pub auto_indent: bool,
|
|
||||||
pub linebreak_on_incomplete: bool,
|
|
||||||
pub leader: String,
|
|
||||||
pub line_numbers: bool,
|
|
||||||
pub screensaver_cmd: String,
|
|
||||||
pub screensaver_idle_time: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShOptPrompt {
|
/// The leader key sequence used in keymap bindings
|
||||||
pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> {
|
leader: String = " ".to_string(),
|
||||||
match opt {
|
|
||||||
"trunc_prompt_path" => {
|
|
||||||
let Ok(val) = val.parse::<usize>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected a positive integer for trunc_prompt_path value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.trunc_prompt_path = val;
|
|
||||||
}
|
|
||||||
"edit_mode" => {
|
|
||||||
let Ok(val) = val.parse::<ShedEditMode>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'vi' or 'emacs' for edit_mode value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.edit_mode = val;
|
|
||||||
}
|
|
||||||
"comp_limit" => {
|
|
||||||
let Ok(val) = val.parse::<usize>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected a positive integer for comp_limit value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.comp_limit = val;
|
|
||||||
}
|
|
||||||
"highlight" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for highlight value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.highlight = val;
|
|
||||||
}
|
|
||||||
"auto_indent" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for auto_indent value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.auto_indent = val;
|
|
||||||
}
|
|
||||||
"linebreak_on_incomplete" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for linebreak_on_incomplete value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.linebreak_on_incomplete = val;
|
|
||||||
}
|
|
||||||
"leader" => {
|
|
||||||
self.leader = val.to_string();
|
|
||||||
}
|
|
||||||
"line_numbers" => {
|
|
||||||
let Ok(val) = val.parse::<bool>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected 'true' or 'false' for line_numbers value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.line_numbers = val;
|
|
||||||
}
|
|
||||||
"screensaver_cmd" => {
|
|
||||||
self.screensaver_cmd = val.to_string();
|
|
||||||
}
|
|
||||||
"screensaver_idle_time" => {
|
|
||||||
let Ok(val) = val.parse::<usize>() else {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
"shopt: expected a positive integer for screensaver_idle_time value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
self.screensaver_idle_time = val;
|
|
||||||
}
|
|
||||||
"custom" => {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn get(&self, query: &str) -> ShResult<Option<String>> {
|
|
||||||
if query.is_empty() {
|
|
||||||
return Ok(Some(format!("{self}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
match query {
|
/// Whether to display line numbers in multiline input
|
||||||
"trunc_prompt_path" => {
|
line_numbers: bool = true,
|
||||||
let mut output = String::from(
|
|
||||||
"Maximum number of path segments used in the '\\W' prompt escape sequence\n",
|
|
||||||
);
|
|
||||||
output.push_str(&format!("{}", self.trunc_prompt_path));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"edit_mode" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("The style of editor shortcuts used in the line-editing of the prompt\n");
|
|
||||||
output.push_str(&format!("{}", self.edit_mode));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"comp_limit" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("Maximum number of completion candidates displayed upon pressing tab\n");
|
|
||||||
output.push_str(&format!("{}", self.comp_limit));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"highlight" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("Whether to enable or disable syntax highlighting on the prompt\n");
|
|
||||||
output.push_str(&format!("{}", self.highlight));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"auto_indent" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("Whether to automatically indent new lines in multiline commands\n");
|
|
||||||
output.push_str(&format!("{}", self.auto_indent));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"linebreak_on_incomplete" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("Whether to automatically insert a newline when the input is incomplete\n");
|
|
||||||
output.push_str(&format!("{}", self.linebreak_on_incomplete));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"leader" => {
|
|
||||||
let mut output = String::from("The leader key sequence used in keymap bindings\n");
|
|
||||||
output.push_str(&self.leader);
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"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))
|
|
||||||
}
|
|
||||||
"screensaver_cmd" => {
|
|
||||||
let mut output = String::from("Command to execute as a screensaver after idle timeout\n");
|
|
||||||
output.push_str(&self.screensaver_cmd);
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
"screensaver_idle_time" => {
|
|
||||||
let mut output =
|
|
||||||
String::from("Idle time in seconds before running screensaver_cmd (0 = disabled)\n");
|
|
||||||
output.push_str(&format!("{}", self.screensaver_idle_time));
|
|
||||||
Ok(Some(output))
|
|
||||||
}
|
|
||||||
_ => Err(ShErr::simple(
|
|
||||||
ShErrKind::SyntaxErr,
|
|
||||||
format!("shopt: Unexpected 'prompt' option '{query}'"),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ShOptPrompt {
|
/// Command to execute as a screensaver after idle timeout
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
screensaver_cmd: String = String::new(),
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path));
|
/// Idle time in seconds before running screensaver_cmd (0 = disabled)
|
||||||
output.push(format!("edit_mode = {}", self.edit_mode));
|
screensaver_idle_time: usize = 0,
|
||||||
output.push(format!("comp_limit = {}", self.comp_limit));
|
|
||||||
output.push(format!("highlight = {}", self.highlight));
|
|
||||||
output.push(format!("auto_indent = {}", self.auto_indent));
|
|
||||||
output.push(format!(
|
|
||||||
"linebreak_on_incomplete = {}",
|
|
||||||
self.linebreak_on_incomplete
|
|
||||||
));
|
|
||||||
output.push(format!("leader = {}", self.leader));
|
|
||||||
output.push(format!("line_numbers = {}", self.line_numbers));
|
|
||||||
output.push(format!("screensaver_cmd = {}", self.screensaver_cmd));
|
|
||||||
output.push(format!(
|
|
||||||
"screensaver_idle_time = {}",
|
|
||||||
self.screensaver_idle_time
|
|
||||||
));
|
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
/// Whether tab completion matching is case-insensitive
|
||||||
|
completion_ignore_case: bool = false,
|
||||||
writeln!(f, "{final_output}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ShOptPrompt {
|
|
||||||
fn default() -> Self {
|
|
||||||
ShOptPrompt {
|
|
||||||
trunc_prompt_path: 4,
|
|
||||||
edit_mode: ShedEditMode::Vi,
|
|
||||||
comp_limit: 100,
|
|
||||||
highlight: true,
|
|
||||||
auto_indent: true,
|
|
||||||
linebreak_on_incomplete: true,
|
|
||||||
leader: "\\".to_string(),
|
|
||||||
line_numbers: true,
|
|
||||||
screensaver_cmd: String::new(),
|
|
||||||
screensaver_idle_time: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,12 +365,6 @@ mod tests {
|
|||||||
fn set_and_get_prompt_opts() {
|
fn set_and_get_prompt_opts() {
|
||||||
let mut opts = ShOpts::default();
|
let mut opts = ShOpts::default();
|
||||||
|
|
||||||
opts.set("prompt.edit_mode", "emacs").unwrap();
|
|
||||||
assert!(matches!(opts.prompt.edit_mode, ShedEditMode::Emacs));
|
|
||||||
|
|
||||||
opts.set("prompt.edit_mode", "vi").unwrap();
|
|
||||||
assert!(matches!(opts.prompt.edit_mode, ShedEditMode::Vi));
|
|
||||||
|
|
||||||
opts.set("prompt.comp_limit", "50").unwrap();
|
opts.set("prompt.comp_limit", "50").unwrap();
|
||||||
assert_eq!(opts.prompt.comp_limit, 50);
|
assert_eq!(opts.prompt.comp_limit, 50);
|
||||||
|
|
||||||
@@ -704,7 +409,6 @@ mod tests {
|
|||||||
assert!(opts.set("core.dotglob", "notabool").is_err());
|
assert!(opts.set("core.dotglob", "notabool").is_err());
|
||||||
assert!(opts.set("core.max_hist", "notanint").is_err());
|
assert!(opts.set("core.max_hist", "notanint").is_err());
|
||||||
assert!(opts.set("core.max_recurse_depth", "-5").is_err());
|
assert!(opts.set("core.max_recurse_depth", "-5").is_err());
|
||||||
assert!(opts.set("prompt.edit_mode", "notepad").is_err());
|
|
||||||
assert!(opts.set("prompt.comp_limit", "abc").is_err());
|
assert!(opts.set("prompt.comp_limit", "abc").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,7 +422,6 @@ mod tests {
|
|||||||
assert!(core_output.contains("bell_enabled"));
|
assert!(core_output.contains("bell_enabled"));
|
||||||
|
|
||||||
let prompt_output = opts.get("prompt").unwrap().unwrap();
|
let prompt_output = opts.get("prompt").unwrap().unwrap();
|
||||||
assert!(prompt_output.contains("edit_mode"));
|
|
||||||
assert!(prompt_output.contains("comp_limit"));
|
assert!(prompt_output.contains("comp_limit"));
|
||||||
assert!(prompt_output.contains("highlight"));
|
assert!(prompt_output.contains("highlight"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
readline::{
|
readline::{
|
||||||
complete::{BashCompSpec, CompSpec},
|
complete::{BashCompSpec, Candidate, CompSpec},
|
||||||
keys::KeyEvent,
|
keys::KeyEvent,
|
||||||
markers,
|
markers,
|
||||||
},
|
},
|
||||||
@@ -1001,6 +1001,13 @@ impl From<Vec<String>> for Var {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Candidate>> for Var {
|
||||||
|
fn from(value: Vec<Candidate>) -> Self {
|
||||||
|
let as_strs = value.into_iter().map(|c| c.0).collect::<Vec<_>>();
|
||||||
|
Self::new(VarKind::Arr(as_strs.into()), VarFlags::NONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&[String]> for Var {
|
impl From<&[String]> for Var {
|
||||||
fn from(value: &[String]) -> Self {
|
fn from(value: &[String]) -> Self {
|
||||||
let mut new = VecDeque::new();
|
let mut new = VecDeque::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user