1use std::collections::HashMap;
5use std::path::PathBuf;
6
7pub fn default_path() -> Option<PathBuf> {
8 if let Ok(p) = std::env::var("RMUT_ALIASES") {
9 return Some(PathBuf::from(p));
10 }
11 std::env::var("HOME")
12 .ok()
13 .map(|h| PathBuf::from(h).join(".config/rmut/aliases"))
14}
15
16pub fn path_for(configured: Option<&str>) -> Option<PathBuf> {
20 match configured.map(str::trim).filter(|p| !p.is_empty()) {
21 Some(path) => Some(expand_home(path)),
22 None => default_path(),
23 }
24}
25
26fn expand_home(path: &str) -> PathBuf {
27 match path.strip_prefix("~/") {
28 Some(rest) => match std::env::var("HOME") {
29 Ok(home) => PathBuf::from(home).join(rest),
30 Err(_) => PathBuf::from(path),
31 },
32 None => PathBuf::from(path),
33 }
34}
35
36pub fn load(configured: Option<&str>) -> HashMap<String, String> {
38 path_for(configured)
39 .and_then(|p| std::fs::read_to_string(p).ok())
40 .map(|t| parse(&t))
41 .unwrap_or_default()
42}
43
44pub fn parse(text: &str) -> HashMap<String, String> {
45 let mut map = HashMap::new();
46 for line in text.lines() {
47 let line = line.trim();
48 if line.starts_with('#') {
49 continue;
50 }
51 if let Some(rest) = line.strip_prefix("alias ") {
52 let mut parts = rest.trim().splitn(2, char::is_whitespace);
53 if let (Some(nick), Some(expansion)) = (parts.next(), parts.next()) {
54 map.insert(nick.to_string(), expansion.trim().to_string());
55 }
56 }
57 }
58 map
59}
60
61pub fn append_to(configured: Option<&str>, nick: &str, expansion: &str) -> anyhow::Result<PathBuf> {
66 use anyhow::Context;
67 use std::io::Write;
68 let path = path_for(configured).context("no alias file path ($HOME unset)")?;
69 if let Some(parent) = path.parent() {
70 std::fs::create_dir_all(parent)?;
71 }
72 let mut file = std::fs::OpenOptions::new()
73 .create(true)
74 .append(true)
75 .open(&path)
76 .with_context(|| format!("opening {}", path.display()))?;
77 writeln!(file, "alias {nick} {expansion}")?;
78 Ok(path)
79}
80
81pub fn complete(
85 word: &str,
86 aliases: &HashMap<String, String>,
87 query_command: Option<&str>,
88) -> Vec<String> {
89 let lower = word.to_lowercase();
90 let mut out: Vec<String> = aliases
91 .iter()
92 .filter(|(nick, _)| nick.to_lowercase().starts_with(&lower))
93 .map(|(_, expansion)| expansion.clone())
94 .collect();
95 out.sort();
96 if let Some(command) = query_command {
97 out.extend(query(command, word));
98 }
99 out.dedup();
100 out
101}
102
103pub fn query(command: &str, word: &str) -> Vec<String> {
107 let quoted = format!("'{}'", word.replace('\'', r"'\''"));
108 let command = if command.contains("%s") {
109 command.replace("%s", "ed)
110 } else {
111 format!("{command} {quoted}")
112 };
113 let Ok(out) = std::process::Command::new("sh")
114 .arg("-c")
115 .arg(&command)
116 .output()
117 else {
118 return Vec::new();
119 };
120 String::from_utf8_lossy(&out.stdout)
121 .lines()
122 .skip(1)
123 .filter_map(|line| {
124 let mut fields = line.split('\t');
125 let addr = fields.next()?.trim();
126 if addr.is_empty() {
127 return None;
128 }
129 Some(
130 match fields.next().map(str::trim).filter(|n| !n.is_empty()) {
131 Some(name) => format!("{name} <{addr}>"),
132 None => addr.to_string(),
133 },
134 )
135 })
136 .collect()
137}
138
139pub fn expand(input: &str, aliases: &HashMap<String, String>) -> String {
142 input
143 .split(',')
144 .map(|token| {
145 let t = token.trim();
146 aliases.get(t).cloned().unwrap_or_else(|| t.to_string())
147 })
148 .filter(|s| !s.is_empty())
149 .collect::<Vec<_>>()
150 .join(", ")
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn complete_matches_nick_prefixes() {
159 let map = parse(
160 "alias petr Petr Novak <petr@example.com>\nalias pete pete@example.org\nalias jane jane@example.com\n",
161 );
162 assert_eq!(
163 complete("PE", &map, None),
164 vec![
165 "Petr Novak <petr@example.com>".to_string(),
166 "pete@example.org".into(),
167 ]
168 );
169 assert_eq!(complete("jane", &map, None).len(), 1);
170 assert!(complete("zz", &map, None).is_empty());
171 }
172
173 #[test]
174 fn query_parses_mutt_output() {
175 let cmd =
177 "printf 'Searching %s...\\nzdenka@example.com\\tZdenka Q\\tnote\\nbare@example.com\\n'";
178 assert_eq!(
179 query(cmd, "zd"),
180 vec![
181 "Zdenka Q <zdenka@example.com>".to_string(),
182 "bare@example.com".into(),
183 ]
184 );
185 assert_eq!(query("echo dummy; echo %s", "a'b"), vec!["a'b".to_string()]);
187 assert!(query("false", "x").is_empty());
188 let map = parse("alias zdeno zdeno@example.net\n");
190 let all = complete(
191 "zd",
192 &map,
193 Some("printf 'found\\nzdenka@example.com\\tZdenka Q\\n'"),
194 );
195 assert_eq!(
196 all,
197 vec![
198 "zdeno@example.net".to_string(),
199 "Zdenka Q <zdenka@example.com>".into(),
200 ]
201 );
202 }
203
204 #[test]
205 fn parse_and_expand() {
206 let map = parse(
207 "# my aliases\nalias jane Jane Doe <jane@example.com>\nalias team a@x, b@y\nnot an alias line\n",
208 );
209 assert_eq!(map.len(), 2);
210 assert_eq!(
211 expand("jane, chief@corp", &map),
212 "Jane Doe <jane@example.com>, chief@corp"
213 );
214 assert_eq!(expand("team", &map), "a@x, b@y");
215 assert_eq!(expand("nobody", &HashMap::new()), "nobody");
216 }
217}