use anyhow::Result;
use anyhow::anyhow;
use std::collections::BTreeSet;
pub fn parse_comma_set(input: &str) -> BTreeSet<String> {
input
.split(|c: char| c == ',' || c.is_whitespace())
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().to_lowercase())
.collect()
}
pub fn bool_from_env(var: &str, default: bool) -> bool {
match std::env::var(var) {
Ok(v) => match v.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => true,
"0" | "false" | "no" | "off" => false,
_ => default,
},
Err(_) => default,
}
}
pub fn usize_from_env(var: &str, default: usize) -> usize {
std::env::var(var)
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(default)
}
pub fn set_from_env(var: &str) -> Option<BTreeSet<String>> {
std::env::var(var)
.ok()
.map(|s| parse_comma_set(&s))
.filter(|s| !s.is_empty())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Argv {
pub raw: String,
pub program: String,
pub args: Vec<String>,
}
pub fn editor_argv() -> Result<Argv> {
let visual = std::env::var("VISUAL").ok();
let editor = std::env::var("EDITOR").ok();
let raw = visual
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.or_else(|| editor.as_deref().map(str::trim).filter(|s| !s.is_empty()))
.unwrap_or("vi")
.to_string();
let parts =
shlex::split(&raw).ok_or_else(|| anyhow!("Invalid $VISUAL/$EDITOR value: {raw}"))?;
let (program, args) = parts
.split_first()
.ok_or_else(|| anyhow!("Empty $VISUAL/$EDITOR value"))?;
Ok(Argv {
raw,
program: program.clone(),
args: args.to_vec(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_comma_set_basic() {
let set = parse_comma_set("foo,bar,baz");
assert_eq!(set.len(), 3);
assert!(set.contains("foo"));
assert!(set.contains("bar"));
assert!(set.contains("baz"));
}
#[test]
fn parse_comma_set_with_spaces() {
let set = parse_comma_set("foo, bar , baz");
assert_eq!(set.len(), 3);
assert!(set.contains("foo"));
assert!(set.contains("bar"));
assert!(set.contains("baz"));
}
#[test]
fn parse_comma_set_whitespace_separated() {
let set = parse_comma_set("foo bar baz");
assert_eq!(set.len(), 3);
}
#[test]
fn parse_comma_set_mixed_separators() {
let set = parse_comma_set("foo, bar baz");
assert_eq!(set.len(), 3);
}
#[test]
fn parse_comma_set_lowercases() {
let set = parse_comma_set("FOO, Bar, BAZ");
assert!(set.contains("foo"));
assert!(set.contains("bar"));
assert!(set.contains("baz"));
assert!(!set.contains("FOO"));
}
#[test]
fn parse_comma_set_empty() {
let set = parse_comma_set("");
assert!(set.is_empty());
}
#[test]
fn parse_comma_set_only_separators() {
let set = parse_comma_set(", , , ");
assert!(set.is_empty());
}
#[test]
fn parse_comma_set_duplicates_deduplicated() {
let set = parse_comma_set("foo, FOO, Foo");
assert_eq!(set.len(), 1);
assert!(set.contains("foo"));
}
#[test]
fn bool_from_env_returns_default_when_unset() {
let result = bool_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__", true);
assert!(result);
let result = bool_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__", false);
assert!(!result);
}
#[test]
fn usize_from_env_returns_default_when_unset() {
let result = usize_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__", 42);
assert_eq!(result, 42);
}
#[test]
fn set_from_env_returns_none_when_unset() {
let result = set_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__");
assert!(result.is_none());
}
fn argv_from(visual: Option<&str>, editor: Option<&str>) -> super::Result<Argv> {
let raw = visual
.map(str::trim)
.filter(|s| !s.is_empty())
.or_else(|| editor.map(str::trim).filter(|s| !s.is_empty()))
.unwrap_or("vi")
.to_string();
let parts = shlex::split(&raw).ok_or_else(|| anyhow::anyhow!("Invalid value: {raw}"))?;
let (program, args) = parts
.split_first()
.ok_or_else(|| anyhow::anyhow!("Empty value"))?;
Ok(Argv {
raw,
program: program.clone(),
args: args.to_vec(),
})
}
#[test]
fn test_editor_code_wait() {
let argv = argv_from(None, Some("code --wait")).unwrap();
assert_eq!(argv.program, "code");
assert_eq!(argv.args, vec!["--wait"]);
}
#[test]
fn test_editor_visual_precedence() {
let argv = argv_from(Some("nvim"), Some("vim")).unwrap();
assert_eq!(argv.program, "nvim");
}
#[test]
fn test_editor_whitespace_fallback() {
let argv = argv_from(Some(" "), Some(" ")).unwrap();
assert_eq!(argv.program, "vi");
}
#[test]
fn test_editor_quoted_args() {
let argv = argv_from(None, Some(r#"nvim -c "set number""#)).unwrap();
assert_eq!(argv.program, "nvim");
assert_eq!(argv.args, vec!["-c", "set number"]);
}
#[test]
fn test_editor_multiple_args() {
let argv = argv_from(None, Some("code --wait --new-window")).unwrap();
assert_eq!(argv.program, "code");
assert_eq!(argv.args, vec!["--wait", "--new-window"]);
}
#[test]
fn test_editor_default_vi() {
let argv = argv_from(None, None).unwrap();
assert_eq!(argv.program, "vi");
assert!(argv.args.is_empty());
}
}