use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shell {
Bash,
Zsh,
Pwsh,
Clink,
Nu,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PerShellString {
All(String),
ByShell {
default: Option<String>,
bash: Option<String>,
zsh: Option<String>,
pwsh: Option<String>,
nu: Option<String>,
},
}
impl PerShellString {
pub fn for_shell(&self, shell: Shell) -> Option<&str> {
match self {
PerShellString::All(s) => Some(s.as_str()),
PerShellString::ByShell { default, bash, zsh, pwsh, nu } => {
let specific = match shell {
Shell::Bash => bash.as_deref(),
Shell::Zsh => zsh.as_deref(),
Shell::Pwsh => pwsh.as_deref(),
Shell::Nu => nu.as_deref(),
Shell::Clink => None, };
specific.or(default.as_deref())
}
}
}
pub fn all_values(&self) -> Vec<&str> {
match self {
PerShellString::All(s) => vec![s.as_str()],
PerShellString::ByShell { default, bash, zsh, pwsh, nu } => {
[default, bash, zsh, pwsh, nu]
.iter()
.filter_map(|v| v.as_deref())
.collect()
}
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PerShellCmds {
All(Vec<String>),
ByShell {
default: Option<Vec<String>>,
bash: Option<Vec<String>>,
zsh: Option<Vec<String>>,
pwsh: Option<Vec<String>>,
nu: Option<Vec<String>>,
},
}
impl PerShellCmds {
pub fn for_shell(&self, shell: Shell) -> Option<&[String]> {
match self {
PerShellCmds::All(v) => Some(v.as_slice()),
PerShellCmds::ByShell { default, bash, zsh, pwsh, nu } => {
let specific: Option<&Vec<String>> = match shell {
Shell::Bash => bash.as_ref(),
Shell::Zsh => zsh.as_ref(),
Shell::Pwsh => pwsh.as_ref(),
Shell::Nu => nu.as_ref(),
Shell::Clink => None,
};
specific.or(default.as_ref()).map(Vec::as_slice)
}
}
}
pub fn all_values(&self) -> Vec<&[String]> {
match self {
PerShellCmds::All(v) => vec![v.as_slice()],
PerShellCmds::ByShell { default, bash, zsh, pwsh, nu } => {
[default, bash, zsh, pwsh, nu]
.iter()
.filter_map(|v| v.as_deref())
.collect()
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum TriggerKey {
#[default]
Space,
Tab,
AltSpace,
ShiftSpace,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct PerShellKey {
pub default: Option<TriggerKey>,
pub bash: Option<TriggerKey>,
pub zsh: Option<TriggerKey>,
pub pwsh: Option<TriggerKey>,
pub nu: Option<TriggerKey>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct KeybindConfig {
#[serde(default)]
pub trigger: PerShellKey,
#[serde(default)]
pub self_insert: PerShellKey,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Abbr {
pub key: String,
pub expand: PerShellString,
pub when_command_exists: Option<PerShellCmds>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct PrecacheConfig {
#[serde(default)]
pub path_only: bool,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Config {
pub version: u32,
#[serde(default)]
pub keybind: KeybindConfig,
#[serde(default)]
pub precache: PrecacheConfig,
#[serde(default)]
pub abbr: Vec<Abbr>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExpandResult {
Expanded { text: String, cursor_offset: Option<usize> },
PassThrough(String),
}
pub const CURSOR_PLACEHOLDER: &str = "{}";
#[cfg(test)]
mod tests {
use super::*;
use super::Shell;
#[test]
fn per_shell_string_all_always_returns_value() {
let v = PerShellString::All("lsd".into());
assert_eq!(v.for_shell(Shell::Bash), Some("lsd"));
assert_eq!(v.for_shell(Shell::Pwsh), Some("lsd"));
assert_eq!(v.for_shell(Shell::Nu), Some("lsd"));
}
#[test]
fn per_shell_string_for_shell_returns_shell_specific() {
let v = PerShellString::ByShell {
default: Some("7z".into()),
pwsh: Some("7z.exe".into()),
bash: None, zsh: None, nu: None,
};
assert_eq!(v.for_shell(Shell::Pwsh), Some("7z.exe"));
assert_eq!(v.for_shell(Shell::Bash), Some("7z")); assert_eq!(v.for_shell(Shell::Nu), Some("7z"));
}
#[test]
fn per_shell_string_none_when_no_entry() {
let v = PerShellString::ByShell {
default: None,
pwsh: Some("7z.exe".into()),
bash: None, zsh: None, nu: None,
};
assert_eq!(v.for_shell(Shell::Bash), None); assert_eq!(v.for_shell(Shell::Pwsh), Some("7z.exe"));
}
#[test]
fn per_shell_string_clink_uses_default() {
let v = PerShellString::ByShell {
default: Some("cmd".into()),
bash: None, zsh: None, pwsh: None, nu: None,
};
assert_eq!(v.for_shell(Shell::Clink), Some("cmd"));
}
#[test]
fn per_shell_cmds_all_always_returns_value() {
let v = PerShellCmds::All(vec!["lsd".into()]);
assert_eq!(v.for_shell(Shell::Bash), Some(["lsd".to_string()].as_slice()));
assert_eq!(v.for_shell(Shell::Pwsh), Some(["lsd".to_string()].as_slice()));
}
#[test]
fn per_shell_cmds_for_shell_returns_shell_specific() {
let v = PerShellCmds::ByShell {
default: Some(vec!["7z".into()]),
pwsh: Some(vec!["7z.exe".into()]),
bash: None, zsh: None, nu: None,
};
assert_eq!(v.for_shell(Shell::Pwsh), Some(["7z.exe".to_string()].as_slice()));
assert_eq!(v.for_shell(Shell::Bash), Some(["7z".to_string()].as_slice()));
}
#[test]
fn per_shell_cmds_none_when_no_entry() {
let v = PerShellCmds::ByShell {
default: None,
pwsh: Some(vec!["7z.exe".into()]),
bash: None, zsh: None, nu: None,
};
assert_eq!(v.for_shell(Shell::Bash), None);
assert_eq!(v.for_shell(Shell::Pwsh), Some(["7z.exe".to_string()].as_slice()));
}
#[test]
fn abbr_fields() {
let a = Abbr {
key: "gcm".into(),
expand: PerShellString::All("git commit -m".into()),
when_command_exists: None,
};
assert_eq!(a.key, "gcm");
assert_eq!(a.expand, PerShellString::All("git commit -m".into()));
assert!(a.when_command_exists.is_none());
}
#[test]
fn abbr_with_when_command_exists() {
let a = Abbr {
key: "ls".into(),
expand: PerShellString::All("lsd".into()),
when_command_exists: Some(PerShellCmds::All(vec!["lsd".into()])),
};
match a.when_command_exists.unwrap() {
PerShellCmds::All(v) => assert_eq!(v, vec!["lsd".to_string()]),
_ => panic!("expected All"),
}
}
#[test]
fn config_fields() {
let c = Config {
version: 1,
keybind: KeybindConfig::default(),
precache: PrecacheConfig::default(),
abbr: vec![],
};
assert_eq!(c.version, 1);
assert_eq!(c.keybind, KeybindConfig::default());
assert!(c.abbr.is_empty());
}
#[test]
fn keybind_config_fields() {
let k = KeybindConfig {
trigger: PerShellKey {
default: Some(TriggerKey::Space),
bash: Some(TriggerKey::AltSpace),
zsh: Some(TriggerKey::Space),
pwsh: Some(TriggerKey::Tab),
nu: None,
},
self_insert: PerShellKey::default(),
};
assert_eq!(k.trigger.default, Some(TriggerKey::Space));
assert_eq!(k.trigger.bash, Some(TriggerKey::AltSpace));
assert_eq!(k.trigger.zsh, Some(TriggerKey::Space));
assert_eq!(k.trigger.pwsh, Some(TriggerKey::Tab));
assert_eq!(k.trigger.nu, None);
assert_eq!(k.self_insert, PerShellKey::default());
}
#[test]
fn parse_config_accepts_self_insert_shift_space() {
let toml = r#"
version = 1
[keybind.self_insert]
pwsh = "shift-space"
"#;
let config: Config = toml::from_str(toml).expect("should parse");
assert_eq!(
config.keybind.self_insert.pwsh,
Some(TriggerKey::ShiftSpace),
"self_insert.pwsh should deserialize to ShiftSpace"
);
}
#[test]
fn parse_config_keybind_entirely_absent() {
let toml = "version = 1\n";
let config: Config = toml::from_str(toml).expect("should parse");
assert_eq!(config.keybind.trigger, PerShellKey::default());
assert_eq!(config.keybind.self_insert, PerShellKey::default());
}
#[test]
fn expand_result_variants() {
let expanded = ExpandResult::Expanded { text: "git commit -m".into(), cursor_offset: None };
let pass = ExpandResult::PassThrough("unknown".into());
assert_eq!(expanded, ExpandResult::Expanded { text: "git commit -m".into(), cursor_offset: None });
assert_eq!(pass, ExpandResult::PassThrough("unknown".into()));
}
}