1use std::collections::HashMap;
7use std::path::Path;
8
9use crate::key::{KeyCode, KeyEvent, KeyModifiers};
10
11pub fn byte_at(buf: &str, cursor: usize) -> usize {
13 buf.char_indices()
14 .nth(cursor)
15 .map(|(i, _)| i)
16 .unwrap_or(buf.len())
17}
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
21pub enum Edit {
22 Edited,
24 Cancel,
26 Submit,
28 Complete,
30 History(bool),
32 Ignored,
34}
35
36#[derive(Clone, Debug, Default)]
38pub struct LineEdit {
39 pub buf: String,
40 pub cursor: usize,
41 hist_pos: Option<usize>,
43 stash: String,
46}
47
48impl LineEdit {
49 pub fn new(prefill: String) -> LineEdit {
51 let cursor = prefill.chars().count();
52 LineEdit {
53 buf: prefill,
54 cursor,
55 hist_pos: None,
56 stash: String::new(),
57 }
58 }
59
60 pub fn set(&mut self, text: &str) {
62 self.buf = text.to_string();
63 self.cursor = self.buf.chars().count();
64 }
65
66 pub fn key(&mut self, key: KeyEvent) -> Edit {
68 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
69 let buf = &mut self.buf;
70 let cursor = &mut self.cursor;
71 match key.code {
72 KeyCode::Esc => return Edit::Cancel,
73 KeyCode::Enter => return Edit::Submit,
74 KeyCode::Tab => return Edit::Complete,
75 KeyCode::Up => return Edit::History(true),
76 KeyCode::Down => return Edit::History(false),
77 KeyCode::Left => *cursor = cursor.saturating_sub(1),
78 KeyCode::Right => *cursor = (*cursor + 1).min(buf.chars().count()),
79 KeyCode::Home => *cursor = 0,
80 KeyCode::End => *cursor = buf.chars().count(),
81 KeyCode::Char('a') if ctrl => *cursor = 0,
82 KeyCode::Char('e') if ctrl => *cursor = buf.chars().count(),
83 KeyCode::Backspace => {
84 if *cursor > 0 {
85 buf.remove(byte_at(buf, *cursor - 1));
86 *cursor -= 1;
87 }
88 }
89 KeyCode::Delete => {
90 if *cursor < buf.chars().count() {
91 buf.remove(byte_at(buf, *cursor));
92 }
93 }
94 KeyCode::Char('d') if ctrl => {
95 if *cursor < buf.chars().count() {
96 buf.remove(byte_at(buf, *cursor));
97 }
98 }
99 KeyCode::Char('u') if ctrl => {
100 let i = byte_at(buf, *cursor);
102 buf.replace_range(..i, "");
103 *cursor = 0;
104 }
105 KeyCode::Char('k') if ctrl => {
106 let i = byte_at(buf, *cursor);
107 buf.truncate(i);
108 }
109 KeyCode::Char('w') if ctrl => {
110 let chars: Vec<char> = buf.chars().collect();
112 let mut c = *cursor;
113 while c > 0 && chars[c - 1].is_whitespace() {
114 c -= 1;
115 }
116 while c > 0 && !chars[c - 1].is_whitespace() {
117 c -= 1;
118 }
119 let (start, end) = (byte_at(buf, c), byte_at(buf, *cursor));
120 buf.replace_range(start..end, "");
121 *cursor = c;
122 }
123 KeyCode::Char(c) if !ctrl => {
124 buf.insert(byte_at(buf, *cursor), c);
125 *cursor += 1;
126 }
127 _ => return Edit::Ignored,
128 }
129 Edit::Edited
130 }
131
132 pub fn history_step(&mut self, bucket: &[String], older: bool) {
136 if bucket.is_empty() {
137 return;
138 }
139 let next = match (self.hist_pos, older) {
140 (None, true) => Some(0),
141 (None, false) => return,
142 (Some(p), true) => Some((p + 1).min(bucket.len() - 1)),
143 (Some(0), false) => None,
144 (Some(p), false) => Some(p - 1),
145 };
146 match next {
147 Some(p) => {
148 if self.hist_pos.is_none() {
149 self.stash = self.buf.clone();
150 }
151 self.buf = bucket[p].clone();
152 }
153 None => self.buf = self.stash.clone(),
154 }
155 self.hist_pos = next;
156 self.cursor = self.buf.chars().count();
157 }
158}
159
160pub const KNOWN_BUCKETS: &[&str] = &[
163 "mailbox", "pattern", "address", "command", "other", "file", "notmuch",
164];
165
166#[derive(Default, Debug)]
168pub struct History {
169 buckets: HashMap<&'static str, Vec<String>>,
170}
171
172impl History {
173 pub fn get(&self, bucket: &str) -> &[String] {
174 self.buckets.get(bucket).map(Vec::as_slice).unwrap_or(&[])
175 }
176
177 pub fn push(&mut self, bucket: &'static str, entry: &str) {
179 let entry = entry.trim();
180 if entry.is_empty() {
181 return;
182 }
183 let list = self.buckets.entry(bucket).or_default();
184 list.retain(|e| e != entry);
185 list.insert(0, entry.to_string());
186 list.truncate(100);
187 }
188
189 pub fn load(&mut self, path: &Path) {
193 let Ok(text) = std::fs::read_to_string(path) else {
194 return;
195 };
196 for line in text.lines() {
197 if let Some((bucket, entry)) = line.split_once('\t')
198 && !entry.is_empty()
199 && let Some(known) = KNOWN_BUCKETS.iter().find(|b| **b == bucket)
200 {
201 self.buckets
202 .entry(known)
203 .or_default()
204 .push(entry.to_string());
205 }
206 }
207 }
208
209 pub fn save(&self, path: &Path, cap: usize) {
211 let mut out = String::new();
212 for (bucket, entries) in &self.buckets {
213 for entry in entries.iter().take(cap) {
214 if !entry.contains(['\t', '\n']) {
217 out += &format!("{bucket}\t{entry}\n");
218 }
219 }
220 }
221 if let Some(dir) = path.parent() {
222 let _ = std::fs::create_dir_all(dir);
223 }
224 let _ = std::fs::write(path, out);
225 }
226}
227
228#[derive(Clone, Debug)]
232pub struct Complete {
233 start: usize,
234 candidates: Vec<String>,
235 index: usize,
236 expect: String,
237}
238
239impl Complete {
240 pub fn token(buf: &str, address_list: bool) -> (usize, String) {
244 let after_comma = if address_list {
245 buf.rfind(',').map(|i| i + 1).unwrap_or(0)
246 } else {
247 0
248 };
249 let start = after_comma + buf[after_comma..].len() - buf[after_comma..].trim_start().len();
250 (start, buf[start..].trim().to_string())
251 }
252
253 pub fn cycle(&mut self, buf: &str) -> Option<(String, String)> {
257 if self.expect != buf || self.candidates.len() < 2 {
258 return None;
259 }
260 self.index = (self.index + 1) % self.candidates.len();
261 let next = format!("{}{}", &buf[..self.start], self.candidates[self.index]);
262 self.expect = next.clone();
263 let note = format!("match {}/{}", self.index + 1, self.candidates.len());
264 Some((next, note))
265 }
266
267 pub fn first(
271 buf: &str,
272 start: usize,
273 candidates: Vec<String>,
274 ) -> (Complete, String, Option<String>) {
275 let next = format!("{}{}", &buf[..start], candidates[0]);
276 let note =
277 (candidates.len() > 1).then(|| format!("match 1/{} (Tab cycles)", candidates.len()));
278 (
279 Complete {
280 start,
281 candidates,
282 index: 0,
283 expect: next.clone(),
284 },
285 next,
286 note,
287 )
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 fn press(edit: &mut LineEdit, code: KeyCode) -> Edit {
296 edit.key(KeyEvent::new(code, KeyModifiers::NONE))
297 }
298
299 fn ctrl(edit: &mut LineEdit, c: char) -> Edit {
300 edit.key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL))
301 }
302
303 #[test]
304 fn editing_keys_move_insert_and_kill() {
305 let mut e = LineEdit::new("ab".into());
306 assert_eq!(press(&mut e, KeyCode::Char('c')), Edit::Edited);
307 assert_eq!(e.buf, "abc");
308 press(&mut e, KeyCode::Left);
309 press(&mut e, KeyCode::Char('x'));
310 assert_eq!(e.buf, "abxc");
311 press(&mut e, KeyCode::Backspace);
312 assert_eq!(e.buf, "abc");
313 ctrl(&mut e, 'a');
314 press(&mut e, KeyCode::Delete);
315 assert_eq!(e.buf, "bc");
316 ctrl(&mut e, 'e');
317 ctrl(&mut e, 'u');
318 assert_eq!(e.buf, "");
319 e.set("two words here");
320 ctrl(&mut e, 'w');
321 assert_eq!(e.buf, "two words ");
322 press(&mut e, KeyCode::Home);
323 ctrl(&mut e, 'k');
324 assert_eq!(e.buf, "");
325 assert_eq!(press(&mut e, KeyCode::Enter), Edit::Submit);
326 assert_eq!(press(&mut e, KeyCode::Esc), Edit::Cancel);
327 assert_eq!(press(&mut e, KeyCode::Tab), Edit::Complete);
328 assert_eq!(press(&mut e, KeyCode::Up), Edit::History(true));
329 assert_eq!(press(&mut e, KeyCode::PageUp), Edit::Ignored);
330 }
331
332 #[test]
333 fn cursor_is_in_chars_not_bytes() {
334 let mut e = LineEdit::new("héllo".into());
335 press(&mut e, KeyCode::Left);
336 press(&mut e, KeyCode::Left);
337 press(&mut e, KeyCode::Backspace);
338 assert_eq!(e.buf, "hélo");
339 assert_eq!(byte_at("héllo", 2), 3);
340 }
341
342 #[test]
343 fn history_steps_and_restores_the_stash() {
344 let bucket = vec!["newest".to_string(), "older".to_string()];
345 let mut e = LineEdit::new("typing".into());
346 e.history_step(&bucket, true);
347 assert_eq!(e.buf, "newest");
348 e.history_step(&bucket, true);
349 assert_eq!(e.buf, "older");
350 e.history_step(&bucket, true);
351 assert_eq!(e.buf, "older", "stays at the oldest");
352 e.history_step(&bucket, false);
353 e.history_step(&bucket, false);
354 assert_eq!(e.buf, "typing", "back past the newest restores the line");
355 e.history_step(&[], true);
356 assert_eq!(e.buf, "typing");
357 }
358
359 #[test]
360 fn history_dedupes_and_round_trips_through_a_file() {
361 let mut h = History::default();
362 h.push("pattern", "~f jane");
363 h.push("pattern", "~N");
364 h.push("pattern", "~f jane");
365 h.push("pattern", " ");
366 assert_eq!(h.get("pattern"), ["~f jane", "~N"]);
367 let dir = tempfile::tempdir().unwrap();
368 let path = dir.path().join("sub").join("history");
369 h.push("command", "set beep");
370 h.save(&path, 1);
371 let mut back = History::default();
372 back.load(&path);
373 assert_eq!(back.get("pattern"), ["~f jane"], "capped at 1");
374 assert_eq!(back.get("command"), ["set beep"]);
375 assert!(back.get("bogus").is_empty());
376 }
377
378 #[test]
379 fn completion_tokens_and_cycling() {
380 assert_eq!(Complete::token("jane, bo", true), (6, "bo".into()));
381 assert_eq!(Complete::token(" =arch", false), (2, "=arch".into()));
382 let (mut c, line, note) = Complete::first(
383 "jane, bo",
384 6,
385 vec!["bob@example.com".into(), "bonnie@example.com".into()],
386 );
387 assert_eq!(line, "jane, bob@example.com");
388 assert_eq!(note.as_deref(), Some("match 1/2 (Tab cycles)"));
389 let (line, note) = c.cycle(&line).unwrap();
390 assert_eq!(line, "jane, bonnie@example.com");
391 assert_eq!(note, "match 2/2");
392 assert!(c.cycle("edited since").is_none());
393 let (mut one, line, note) = Complete::first("x", 0, vec!["xy".into()]);
394 assert_eq!((line.as_str(), note), ("xy", None));
395 assert!(one.cycle("xy").is_none());
396 }
397}