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
19pub fn prompt_line(label: &str, edit: &LineEdit, width: usize) -> String {
24 use unicode_width::UnicodeWidthStr as _;
25 let i = byte_at(&edit.buf, edit.cursor);
26 let before = format!("{label}{}", &edit.buf[..i]);
27 let after = &edit.buf[i..];
28 let room = width.max(8).saturating_sub(1 + after.width().min(4));
29 if before.width() <= room {
30 return format!("{before}\u{2581}{after}");
31 }
32 let mut cut = before.as_str();
33 while 1 + cut.width() > room {
34 let mut chars = cut.chars();
35 chars.next();
36 cut = chars.as_str();
37 }
38 format!("<{cut}\u{2581}{after}")
39}
40
41fn word_start(buf: &str, cursor: usize) -> usize {
43 let chars: Vec<char> = buf.chars().collect();
44 let mut c = cursor.min(chars.len());
45 while c > 0 && chars[c - 1].is_whitespace() {
46 c -= 1;
47 }
48 while c > 0 && !chars[c - 1].is_whitespace() {
49 c -= 1;
50 }
51 c
52}
53
54fn word_end(buf: &str, cursor: usize) -> usize {
56 let chars: Vec<char> = buf.chars().collect();
57 let mut c = cursor.min(chars.len());
58 while c < chars.len() && chars[c].is_whitespace() {
59 c += 1;
60 }
61 while c < chars.len() && !chars[c].is_whitespace() {
62 c += 1;
63 }
64 c
65}
66
67#[derive(Clone, Copy, PartialEq, Eq, Debug)]
69pub enum Edit {
70 Edited,
72 Cancel,
74 Submit,
76 Complete,
78 History(bool),
80 Ignored,
82}
83
84#[derive(Clone, Debug, Default)]
86pub struct LineEdit {
87 pub buf: String,
88 pub cursor: usize,
89 hist_pos: Option<usize>,
91 stash: String,
94}
95
96impl LineEdit {
97 pub fn new(prefill: String) -> LineEdit {
99 let cursor = prefill.chars().count();
100 LineEdit {
101 buf: prefill,
102 cursor,
103 hist_pos: None,
104 stash: String::new(),
105 }
106 }
107
108 pub fn set(&mut self, text: &str) {
110 self.buf = text.to_string();
111 self.cursor = self.buf.chars().count();
112 }
113
114 pub fn key(&mut self, key: KeyEvent) -> Edit {
116 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
117 let alt = key.modifiers.contains(KeyModifiers::ALT);
118 let buf = &mut self.buf;
119 let cursor = &mut self.cursor;
120 match key.code {
121 KeyCode::Esc => return Edit::Cancel,
122 KeyCode::Char('g') if ctrl => return Edit::Cancel,
123 KeyCode::Char('b') if alt => *cursor = word_start(buf, *cursor),
126 KeyCode::Char('f') if alt => *cursor = word_end(buf, *cursor),
127 KeyCode::Char('d') if alt => {
128 let end = word_end(buf, *cursor);
129 let (from, to) = (byte_at(buf, *cursor), byte_at(buf, end));
130 buf.replace_range(from..to, "");
131 }
132 KeyCode::Enter => return Edit::Submit,
133 KeyCode::Tab => return Edit::Complete,
134 KeyCode::Up => return Edit::History(true),
135 KeyCode::Down => return Edit::History(false),
136 KeyCode::Left => *cursor = cursor.saturating_sub(1),
137 KeyCode::Right => *cursor = (*cursor + 1).min(buf.chars().count()),
138 KeyCode::Home => *cursor = 0,
139 KeyCode::End => *cursor = buf.chars().count(),
140 KeyCode::Char('a') if ctrl => *cursor = 0,
141 KeyCode::Char('e') if ctrl => *cursor = buf.chars().count(),
142 KeyCode::Backspace => {
143 if *cursor > 0 {
144 buf.remove(byte_at(buf, *cursor - 1));
145 *cursor -= 1;
146 }
147 }
148 KeyCode::Delete => {
149 if *cursor < buf.chars().count() {
150 buf.remove(byte_at(buf, *cursor));
151 }
152 }
153 KeyCode::Char('d') if ctrl => {
154 if *cursor < buf.chars().count() {
155 buf.remove(byte_at(buf, *cursor));
156 }
157 }
158 KeyCode::Char('u') if ctrl => {
159 let i = byte_at(buf, *cursor);
161 buf.replace_range(..i, "");
162 *cursor = 0;
163 }
164 KeyCode::Char('k') if ctrl => {
165 let i = byte_at(buf, *cursor);
166 buf.truncate(i);
167 }
168 KeyCode::Char('w') if ctrl => {
169 let c = word_start(buf, *cursor);
171 let (start, end) = (byte_at(buf, c), byte_at(buf, *cursor));
172 buf.replace_range(start..end, "");
173 *cursor = c;
174 }
175 KeyCode::Char(c) if !ctrl && !alt => {
177 buf.insert(byte_at(buf, *cursor), c);
178 *cursor += 1;
179 }
180 _ => return Edit::Ignored,
181 }
182 Edit::Edited
183 }
184
185 pub fn history_step(&mut self, bucket: &[String], older: bool) {
189 if bucket.is_empty() {
190 return;
191 }
192 let next = match (self.hist_pos, older) {
193 (None, true) => Some(0),
194 (None, false) => return,
195 (Some(p), true) => Some((p + 1).min(bucket.len() - 1)),
196 (Some(0), false) => None,
197 (Some(p), false) => Some(p - 1),
198 };
199 match next {
200 Some(p) => {
201 if self.hist_pos.is_none() {
202 self.stash = self.buf.clone();
203 }
204 self.buf = bucket[p].clone();
205 }
206 None => self.buf = self.stash.clone(),
207 }
208 self.hist_pos = next;
209 self.cursor = self.buf.chars().count();
210 }
211}
212
213pub const KNOWN_BUCKETS: &[&str] = &[
216 "mailbox", "pattern", "address", "command", "other", "file", "notmuch",
217];
218
219#[derive(Default, Debug)]
221pub struct History {
222 buckets: HashMap<&'static str, Vec<String>>,
223}
224
225impl History {
226 pub fn get(&self, bucket: &str) -> &[String] {
227 self.buckets.get(bucket).map(Vec::as_slice).unwrap_or(&[])
228 }
229
230 pub fn push(&mut self, bucket: &'static str, entry: &str) {
232 let entry = entry.trim();
233 if entry.is_empty() {
234 return;
235 }
236 let list = self.buckets.entry(bucket).or_default();
237 list.retain(|e| e != entry);
238 list.insert(0, entry.to_string());
239 list.truncate(100);
240 }
241
242 pub fn load(&mut self, path: &Path) {
246 let Ok(text) = std::fs::read_to_string(path) else {
247 return;
248 };
249 for line in text.lines() {
250 if let Some((bucket, entry)) = line.split_once('\t')
251 && !entry.is_empty()
252 && let Some(known) = KNOWN_BUCKETS.iter().find(|b| **b == bucket)
253 {
254 let list = self.buckets.entry(known).or_default();
255 if list.len() < 100 && !list.iter().any(|e| e == entry) {
258 list.push(entry.to_string());
259 }
260 }
261 }
262 }
263
264 pub fn save(&self, path: &Path, cap: usize) {
268 let mut out = String::new();
269 for bucket in KNOWN_BUCKETS {
270 let Some(entries) = self.buckets.get(bucket) else {
271 continue;
272 };
273 for entry in entries.iter().take(cap) {
274 if !entry.contains(['\t', '\n']) {
277 out += &format!("{bucket}\t{entry}\n");
278 }
279 }
280 }
281 let _ = rmut_core::scratch::save_private(path, out.as_bytes());
282 }
283}
284
285#[derive(Clone, Debug)]
290pub struct Complete {
291 start: usize,
292 tail: String,
293 candidates: Vec<String>,
294 index: usize,
295 expect: String,
296}
297
298impl Complete {
299 pub fn token(buf: &str, cursor: usize, address_list: bool) -> (usize, String) {
304 let head = &buf[..byte_at(buf, cursor)];
305 let after_comma = if address_list {
306 head.rfind(',').map(|i| i + 1).unwrap_or(0)
307 } else {
308 0
309 };
310 let start =
311 after_comma + head[after_comma..].len() - head[after_comma..].trim_start().len();
312 (start, head[start..].trim().to_string())
313 }
314
315 fn fill(&self, buf: &str) -> (String, usize) {
317 let head = format!("{}{}", &buf[..self.start], self.candidates[self.index]);
318 let cursor = head.chars().count();
319 (head + &self.tail, cursor)
320 }
321
322 pub fn cycle(&mut self, buf: &str) -> Option<((String, usize), String)> {
326 if self.expect != buf || self.candidates.len() < 2 {
327 return None;
328 }
329 self.index = (self.index + 1) % self.candidates.len();
330 let next = self.fill(buf);
331 self.expect = next.0.clone();
332 let note = format!("match {}/{}", self.index + 1, self.candidates.len());
333 Some((next, note))
334 }
335
336 pub fn first(
342 buf: &str,
343 cursor: usize,
344 start: usize,
345 candidates: Vec<String>,
346 ) -> (Complete, (String, usize), Option<String>) {
347 let mut state = Complete {
348 start,
349 tail: buf[byte_at(buf, cursor)..].to_string(),
350 candidates,
351 index: 0,
352 expect: String::new(),
353 };
354 let next = state.fill(buf);
355 state.expect = next.0.clone();
356 let note = (state.candidates.len() > 1)
357 .then(|| format!("match 1/{} (Tab cycles)", state.candidates.len()));
358 (state, next, note)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 fn press(edit: &mut LineEdit, code: KeyCode) -> Edit {
367 edit.key(KeyEvent::new(code, KeyModifiers::NONE))
368 }
369
370 fn ctrl(edit: &mut LineEdit, c: char) -> Edit {
371 edit.key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL))
372 }
373
374 #[test]
375 fn a_long_prompt_scrolls_to_keep_the_cursor_in_view() {
376 let mut e = LineEdit::new("short".into());
377 assert_eq!(prompt_line("To: ", &e, 40), "To: short\u{2581}");
378 e.set(&"x".repeat(50));
379 let line = prompt_line("To: ", &e, 20);
380 assert!(line.starts_with('<'), "{line}");
381 assert!(line.ends_with('\u{2581}'), "the cursor is in view: {line}");
382 assert_eq!(line.chars().count(), 20);
383 e.cursor = 0;
385 assert!(prompt_line("To: ", &e, 20).starts_with("To: \u{2581}xxx"));
386 }
387
388 #[test]
389 fn ctrl_g_cancels_and_alt_moves_by_words() {
390 let alt =
391 |e: &mut LineEdit, c: char| e.key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::ALT));
392 let mut e = LineEdit::new("one two three".into());
393 assert_eq!(ctrl(&mut e, 'g'), Edit::Cancel);
394 alt(&mut e, 'b');
395 assert_eq!(e.cursor, 9);
396 alt(&mut e, 'b');
397 assert_eq!(e.cursor, 4);
398 alt(&mut e, 'f');
399 assert_eq!(e.cursor, 7);
400 alt(&mut e, 'd');
401 assert_eq!(e.buf, "one two");
402 e.cursor = 0;
403 alt(&mut e, 'd');
404 assert_eq!(e.buf, " two");
405 assert_eq!(alt(&mut e, 'x'), Edit::Ignored);
407 assert_eq!(e.buf, " two");
408 }
409
410 #[test]
411 fn editing_keys_move_insert_and_kill() {
412 let mut e = LineEdit::new("ab".into());
413 assert_eq!(press(&mut e, KeyCode::Char('c')), Edit::Edited);
414 assert_eq!(e.buf, "abc");
415 press(&mut e, KeyCode::Left);
416 press(&mut e, KeyCode::Char('x'));
417 assert_eq!(e.buf, "abxc");
418 press(&mut e, KeyCode::Backspace);
419 assert_eq!(e.buf, "abc");
420 ctrl(&mut e, 'a');
421 press(&mut e, KeyCode::Delete);
422 assert_eq!(e.buf, "bc");
423 ctrl(&mut e, 'e');
424 ctrl(&mut e, 'u');
425 assert_eq!(e.buf, "");
426 e.set("two words here");
427 ctrl(&mut e, 'w');
428 assert_eq!(e.buf, "two words ");
429 press(&mut e, KeyCode::Home);
430 ctrl(&mut e, 'k');
431 assert_eq!(e.buf, "");
432 assert_eq!(press(&mut e, KeyCode::Enter), Edit::Submit);
433 assert_eq!(press(&mut e, KeyCode::Esc), Edit::Cancel);
434 assert_eq!(press(&mut e, KeyCode::Tab), Edit::Complete);
435 assert_eq!(press(&mut e, KeyCode::Up), Edit::History(true));
436 assert_eq!(press(&mut e, KeyCode::PageUp), Edit::Ignored);
437 }
438
439 #[test]
440 fn cursor_is_in_chars_not_bytes() {
441 let mut e = LineEdit::new("héllo".into());
442 press(&mut e, KeyCode::Left);
443 press(&mut e, KeyCode::Left);
444 press(&mut e, KeyCode::Backspace);
445 assert_eq!(e.buf, "hélo");
446 assert_eq!(byte_at("héllo", 2), 3);
447 }
448
449 #[test]
450 fn history_steps_and_restores_the_stash() {
451 let bucket = vec!["newest".to_string(), "older".to_string()];
452 let mut e = LineEdit::new("typing".into());
453 e.history_step(&bucket, true);
454 assert_eq!(e.buf, "newest");
455 e.history_step(&bucket, true);
456 assert_eq!(e.buf, "older");
457 e.history_step(&bucket, true);
458 assert_eq!(e.buf, "older", "stays at the oldest");
459 e.history_step(&bucket, false);
460 e.history_step(&bucket, false);
461 assert_eq!(e.buf, "typing", "back past the newest restores the line");
462 e.history_step(&[], true);
463 assert_eq!(e.buf, "typing");
464 }
465
466 #[test]
467 fn history_dedupes_and_round_trips_through_a_file() {
468 let mut h = History::default();
469 h.push("pattern", "~f jane");
470 h.push("pattern", "~N");
471 h.push("pattern", "~f jane");
472 h.push("pattern", " ");
473 assert_eq!(h.get("pattern"), ["~f jane", "~N"]);
474 let dir = tempfile::tempdir().unwrap();
475 let path = dir.path().join("sub").join("history");
476 h.push("command", "set beep");
477 h.save(&path, 1);
478 let mut back = History::default();
479 back.load(&path);
480 assert_eq!(back.get("pattern"), ["~f jane"], "capped at 1");
481 assert_eq!(back.get("command"), ["set beep"]);
482 assert!(back.get("bogus").is_empty());
483 }
484
485 #[test]
486 fn completion_tokens_and_cycling() {
487 assert_eq!(Complete::token("jane, bo", 8, true), (6, "bo".into()));
488 assert_eq!(Complete::token(" =arch", 7, false), (2, "=arch".into()));
489 let (mut c, line, note) = Complete::first(
490 "jane, bo",
491 8,
492 6,
493 vec!["bob@example.com".into(), "bonnie@example.com".into()],
494 );
495 assert_eq!(line, ("jane, bob@example.com".to_string(), 21));
496 assert_eq!(note.as_deref(), Some("match 1/2 (Tab cycles)"));
497 let (line, note) = c.cycle(&line.0).unwrap();
498 assert_eq!(line.0, "jane, bonnie@example.com");
499 assert_eq!(note, "match 2/2");
500 assert!(c.cycle("edited since").is_none());
501 let (mut one, line, note) = Complete::first("x", 1, 0, vec!["xy".into()]);
502 assert_eq!((line.0.as_str(), note), ("xy", None));
503 assert!(one.cycle("xy").is_none());
504 }
505
506 #[test]
507 fn completion_works_on_the_token_before_the_cursor() {
508 let buf = "ja, carol@example.com";
511 let (start, word) = Complete::token(buf, 2, true);
512 assert_eq!((start, word.as_str()), (0, "ja"));
513 let (_, (line, cursor), _) =
514 Complete::first(buf, 2, start, vec!["jane@example.com".into()]);
515 assert_eq!(line, "jane@example.com, carol@example.com");
516 assert_eq!(cursor, 16);
517 }
518}