1use std::ops::Range;
49
50use rich::cells::{cell_len, split_graphemes};
51use rich::{Segment, Style};
52
53pub use crate::components::Theme;
54use crate::event::{Event, Key, KeyCode, Mouse, MouseKind};
55use crate::fuzzy::rank;
56use crate::item::Action;
57
58pub fn text(text: impl Into<String>, style: &Style) -> Segment {
63 Segment::new(text.into(), Some(style.clone()))
64}
65
66pub fn icon(text: &rich::Text) -> Vec<Segment> {
71 let console = rich::Console::builder().width(4096).build();
72 let options = console.options().update_width(4096);
73 let mut text = text.clone();
74 text.set_no_wrap(Some(true));
75 console
76 .render_lines(&text, &options, false)
77 .into_iter()
78 .next()
79 .unwrap_or_default()
80 .into_iter()
81 .filter(|segment| !segment.text.is_empty())
82 .collect()
83}
84
85pub fn plain(text: impl Into<String>) -> Segment {
87 Segment::new(text.into(), None)
88}
89
90pub fn width(line: &[Segment]) -> usize {
92 line.iter().map(Segment::cell_length).sum()
93}
94
95pub fn fit(line: Vec<Segment>, columns: usize) -> Vec<Segment> {
97 if width(&line) > columns {
98 Segment::adjust_line_length(&line, columns, None)
99 } else {
100 line
101 }
102}
103
104pub fn pad(line: Vec<Segment>, columns: usize) -> Vec<Segment> {
106 Segment::adjust_line_length(&line, columns, None)
107}
108
109pub fn highlight(
112 label: &str,
113 positions: &[usize],
114 base: Option<&Style>,
115 matched: &Style,
116) -> Vec<Segment> {
117 let hit = match base {
118 Some(base) => base.combine(matched),
119 None => matched.clone(),
120 };
121 let mut segments: Vec<Segment> = Vec::new();
122 let mut run = String::new();
123 let mut run_hit = false;
124 let mut next = positions.iter().peekable();
125 for (index, c) in label.chars().enumerate() {
126 let is_hit = next.peek() == Some(&&index);
127 if is_hit {
128 next.next();
129 }
130 if is_hit != run_hit && !run.is_empty() {
131 let style = if run_hit {
132 Some(hit.clone())
133 } else {
134 base.cloned()
135 };
136 segments.push(Segment::new(std::mem::take(&mut run), style));
137 }
138 run_hit = is_hit;
139 run.push(c);
140 }
141 if !run.is_empty() {
142 let style = if run_hit { Some(hit) } else { base.cloned() };
143 segments.push(Segment::new(run, style));
144 }
145 segments
146}
147
148pub fn slice(line: &[Segment], from: usize, to: usize) -> Vec<Segment> {
152 let mut out: Vec<Segment> = Vec::new();
153 let mut at = 0;
154 for segment in line.iter().filter(|segment| !segment.control) {
155 if at >= to {
156 break;
157 }
158 let length = segment.cell_length();
159 if at >= from && at + length <= to {
160 out.push(segment.clone());
161 at += length;
162 continue;
163 }
164 let (spans, _) = split_graphemes(&segment.text);
165 let mut piece = String::new();
166 for (start, end, cells) in spans {
167 let (left, right) = (at, at + cells);
168 if left >= from && right <= to {
169 piece.push_str(&segment.text[start..end]);
170 } else if right > from && left < to {
171 let covered = right.min(to) - left.max(from);
172 piece.push_str(&" ".repeat(covered));
173 }
174 at = right;
175 }
176 if !piece.is_empty() {
177 out.push(Segment::new(piece, segment.style.clone()));
178 }
179 }
180 out
181}
182
183pub fn overlay(base: &[Segment], x: usize, top: &[Segment]) -> Vec<Segment> {
186 let end = x + width(top);
187 let mut line = pad(slice(base, 0, x), x);
188 line.extend(top.iter().cloned());
189 let base_width = width(base);
190 if base_width > end {
191 line.extend(slice(base, end, base_width));
192 }
193 line
194}
195
196pub fn restyle(line: &[Segment], style: &Style) -> Vec<Segment> {
198 line.iter()
199 .map(|segment| {
200 if segment.control {
201 return segment.clone();
202 }
203 let styled = match &segment.style {
204 Some(own) => own.combine(style),
205 None => style.clone(),
206 };
207 Segment::new(segment.text.clone(), Some(styled))
208 })
209 .collect()
210}
211
212pub fn frame(
216 lines: Vec<Vec<Segment>>,
217 inner: usize,
218 title: Option<&str>,
219 style: &Style,
220) -> Vec<Vec<Segment>> {
221 let mut top = vec![text("╭", style)];
222 match title {
223 Some(title) if inner >= 4 => {
224 let title = fit(vec![text(format!(" {title} "), style)], inner - 1);
225 let used = width(&title) + 1;
226 top.push(text("─", style));
227 top.extend(title);
228 top.push(text("─".repeat(inner.saturating_sub(used)), style));
229 }
230 _ => top.push(text("─".repeat(inner), style)),
231 }
232 top.push(text("╮", style));
233 let mut boxed = vec![top];
234 for line in lines {
235 let mut row = vec![text("│", style)];
236 row.extend(pad(line, inner));
237 row.push(text("│", style));
238 boxed.push(row);
239 }
240 boxed.push(vec![
241 text("╰", style),
242 text("─".repeat(inner), style),
243 text("╯", style),
244 ]);
245 boxed
246}
247
248pub fn place(
252 base: &mut Vec<Vec<Segment>>,
253 x: usize,
254 y: usize,
255 top: &[Vec<Segment>],
256 backdrop: Option<&Style>,
257) {
258 if let Some(style) = backdrop {
259 for line in base.iter_mut() {
260 *line = restyle(line, style);
261 }
262 }
263 if base.len() < y + top.len() {
264 base.resize_with(y + top.len(), Vec::new);
265 }
266 for (row, line) in top.iter().enumerate() {
267 let under = &mut base[y + row];
268 *under = overlay(under, x, line);
269 }
270}
271
272pub fn pasted(text: &str, newline: &str) -> String {
276 let mut out = String::with_capacity(text.len());
277 for c in text.chars() {
278 match c {
279 '\n' | '\r' => out.push_str(newline),
280 '\t' => out.push(' '),
281 '\0'..='\u{1f}' | '\u{7f}'..='\u{9f}' => {}
282 _ => out.push(c),
283 }
284 }
285 out
286}
287
288pub fn shown(text: &str) -> String {
291 text.chars()
292 .map(|c| crate::paint::visible(c).unwrap_or(c))
293 .collect()
294}
295
296pub fn question(theme: &Theme, prompt: &str) -> Vec<Segment> {
298 vec![
299 text(theme.question.clone(), &theme.question_style),
300 plain(" "),
301 text(prompt, &theme.prompt),
302 plain(" "),
303 text("›", &theme.hint),
304 plain(" "),
305 ]
306}
307
308#[derive(Clone, Debug, Default, PartialEq, Eq)]
319pub struct ListState {
320 len: usize,
321 cursor: usize,
322 offset: usize,
323 selected: Vec<bool>,
324}
325
326impl ListState {
327 pub fn new() -> ListState {
328 ListState::default()
329 }
330
331 pub fn len(&self) -> usize {
333 self.len
334 }
335
336 pub fn is_empty(&self) -> bool {
337 self.len == 0
338 }
339
340 pub fn set_len(&mut self, len: usize) {
344 self.len = len;
345 }
346
347 pub fn cursor(&self) -> usize {
349 self.cursor
350 }
351
352 pub fn set_cursor(&mut self, row: usize) {
355 self.cursor = row;
356 }
357
358 pub fn offset(&self) -> usize {
360 self.offset
361 }
362
363 pub fn set_offset(&mut self, offset: usize) {
364 self.offset = offset;
365 }
366
367 pub fn reset(&mut self, row: usize, page: usize) {
370 self.cursor = row;
371 self.offset = 0;
372 self.follow(page);
373 }
374
375 pub fn follow(&mut self, page: usize) {
377 if self.cursor < self.offset {
378 self.offset = self.cursor;
379 } else if self.cursor >= self.offset + page {
380 self.offset = self.cursor + 1 - page;
381 }
382 }
383
384 pub fn step(&mut self, delta: isize, page: usize) {
387 if self.len == 0 {
388 return;
389 }
390 let last = self.len - 1;
391 self.cursor = self.cursor.saturating_add_signed(delta).min(last);
392 self.follow(page);
393 }
394
395 pub fn select_row(&mut self, row: usize, page: usize) {
397 if self.len == 0 {
398 return;
399 }
400 self.cursor = row.min(self.len - 1);
401 self.follow(page);
402 }
403
404 pub fn visible(&self, page: usize) -> Range<usize> {
406 let end = (self.offset + page).min(self.len);
407 self.offset.min(end)..end
408 }
409
410 pub fn clear_selection(&mut self, count: usize) {
412 self.selected = vec![false; count];
413 }
414
415 pub fn is_selected(&self, index: usize) -> bool {
416 self.selected.get(index).copied().unwrap_or(false)
417 }
418
419 pub fn set_selected(&mut self, index: usize, on: bool) {
421 if index >= self.selected.len() {
422 self.selected.resize(index + 1, false);
423 }
424 self.selected[index] = on;
425 }
426
427 pub fn toggle(&mut self, index: usize) {
429 let on = !self.is_selected(index);
430 self.set_selected(index, on);
431 }
432
433 pub fn selected(&self) -> Vec<usize> {
435 (0..self.selected.len())
436 .filter(|&index| self.selected[index])
437 .collect()
438 }
439
440 pub fn selected_count(&self) -> usize {
442 self.selected.iter().filter(|on| **on).count()
443 }
444}
445
446#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
452pub struct ScrollState {
453 len: usize,
454 offset: usize,
455}
456
457impl ScrollState {
458 pub fn new(len: usize) -> ScrollState {
459 ScrollState { len, offset: 0 }
460 }
461
462 pub fn len(&self) -> usize {
463 self.len
464 }
465
466 pub fn is_empty(&self) -> bool {
467 self.len == 0
468 }
469
470 pub fn set_len(&mut self, len: usize) {
472 self.len = len;
473 self.offset = self.offset.min(len.saturating_sub(1));
474 }
475
476 pub fn offset(&self) -> usize {
478 self.offset
479 }
480
481 fn last_offset(&self, page: usize) -> usize {
482 self.len.saturating_sub(page)
483 }
484
485 pub fn scroll(&mut self, delta: isize, page: usize) -> bool {
488 let target = self
489 .offset
490 .saturating_add_signed(delta)
491 .min(self.last_offset(page));
492 let changed = target != self.offset;
493 self.offset = target;
494 changed
495 }
496
497 pub fn scroll_to(&mut self, line: usize, page: usize) -> bool {
499 let target = line.min(self.last_offset(page));
500 let changed = target != self.offset;
501 self.offset = target;
502 changed
503 }
504
505 pub fn show(&mut self, line: usize, page: usize) -> bool {
507 if line < self.offset {
508 self.scroll_to(line, page)
509 } else if line >= self.offset + page {
510 self.scroll_to(line + 1 - page, page)
511 } else {
512 false
513 }
514 }
515
516 pub fn visible(&self, page: usize) -> Range<usize> {
518 let end = (self.offset + page).min(self.len);
519 self.offset.min(end)..end
520 }
521
522 pub fn keymap(context: &str) -> crate::keymap::Keymap {
525 use crate::keymap::keys;
526 crate::keymap::Keymap::new(context)
527 .bind("scroll-up", keys("up k"), "scroll up")
528 .bind("scroll-down", keys("down j"), "scroll down")
529 .bind("page-up", keys("pageup b"), "page up")
530 .bind("page-down", keys("pagedown space"), "page down")
531 .bind("top", keys("home g"), "to the top")
532 .bind("bottom", keys("end G"), "to the bottom")
533 }
534
535 pub fn act(&mut self, action: &str, page: usize) -> bool {
540 let page_step = page.max(1) as isize;
541 match action {
542 "scroll-up" => self.scroll(-1, page),
543 "scroll-down" => self.scroll(1, page),
544 "page-up" => self.scroll(-page_step, page),
545 "page-down" => self.scroll(page_step, page),
546 "top" => self.scroll_to(0, page),
547 "bottom" => self.scroll_to(usize::MAX, page),
548 _ => return false,
549 };
550 true
551 }
552
553 pub fn handle(&mut self, event: &Event, page: usize) -> bool {
559 let page_step = page.max(1) as isize;
560 match event {
561 Event::Key(key) if !key.modifiers.ctrl && !key.modifiers.alt => {
562 match key.code {
563 KeyCode::Up | KeyCode::Char('k') => self.scroll(-1, page),
564 KeyCode::Down | KeyCode::Char('j') => self.scroll(1, page),
565 KeyCode::PageUp | KeyCode::Char('b') => self.scroll(-page_step, page),
566 KeyCode::PageDown | KeyCode::Char(' ') => self.scroll(page_step, page),
567 KeyCode::Home | KeyCode::Char('g') => self.scroll_to(0, page),
568 KeyCode::End | KeyCode::Char('G') => self.scroll_to(usize::MAX, page),
569 _ => return false,
570 };
571 true
572 }
573 Event::Mouse(mouse) => match mouse.kind {
574 MouseKind::ScrollUp => {
575 self.scroll(-3, page);
576 true
577 }
578 MouseKind::ScrollDown => {
579 self.scroll(3, page);
580 true
581 }
582 _ => false,
583 },
584 _ => false,
585 }
586 }
587}
588
589#[derive(Clone, Debug, Default, PartialEq, Eq)]
600pub struct FilterState {
601 query: String,
602 candidates: Vec<String>,
603 hidden: Vec<bool>,
604 limits: Vec<usize>,
605 matches: Vec<(usize, Vec<usize>)>,
606 parents: Option<Vec<Option<usize>>>,
609 context: Vec<bool>,
611 best: Option<usize>,
613}
614
615impl FilterState {
616 pub fn new<I, S>(candidates: I) -> FilterState
618 where
619 I: IntoIterator<Item = S>,
620 S: Into<String>,
621 {
622 let mut filter = FilterState::default();
623 filter.set_candidates(candidates);
624 filter
625 }
626
627 pub fn set_candidates<I, S>(&mut self, candidates: I)
629 where
630 I: IntoIterator<Item = S>,
631 S: Into<String>,
632 {
633 self.candidates = candidates.into_iter().map(Into::into).collect();
634 self.refilter();
635 }
636
637 pub fn candidates(&self) -> &[String] {
638 &self.candidates
639 }
640
641 pub fn set_hidden(&mut self, hidden: Vec<bool>) {
644 self.hidden = hidden;
645 }
646
647 pub fn set_tree(&mut self, parents: Option<Vec<Option<usize>>>) {
656 self.parents = parents;
657 }
658
659 pub fn set_highlight_limits(&mut self, limits: Vec<usize>) {
664 self.limits = limits;
665 }
666
667 pub fn query(&self) -> &str {
669 &self.query
670 }
671
672 pub fn query_mut(&mut self) -> &mut String {
675 &mut self.query
676 }
677
678 pub fn is_filtering(&self) -> bool {
679 !self.query.is_empty()
680 }
681
682 pub fn set_query(&mut self, query: impl Into<String>) {
684 self.query = query.into();
685 self.refilter();
686 }
687
688 pub fn push(&mut self, c: char) {
690 self.query.push(c);
691 self.refilter();
692 }
693
694 pub fn push_str(&mut self, text: &str) {
696 self.query.push_str(text);
697 self.refilter();
698 }
699
700 pub fn pop(&mut self) -> bool {
702 let popped = self.query.pop().is_some();
703 if popped {
704 self.refilter();
705 }
706 popped
707 }
708
709 pub fn clear(&mut self) {
711 self.query.clear();
712 self.refilter();
713 }
714
715 pub fn refilter(&mut self) {
717 let filtering = !self.query.is_empty();
718 let hidden = &self.hidden;
719 let limits = &self.limits;
720 let candidates = &self.candidates;
721 let ranked: Vec<(usize, Vec<usize>)> =
722 rank(&self.query, candidates.iter().map(String::as_str))
723 .into_iter()
724 .filter(|(index, _)| filtering || !hidden.get(*index).copied().unwrap_or(false))
725 .map(|(index, found)| {
726 let mut positions = found.positions;
727 if let Some(limit) = limits.get(index) {
728 positions.retain(|position| position < limit);
729 }
730 (index, positions)
731 })
732 .collect();
733 match &self.parents {
734 Some(parents) if filtering => {
735 let best = ranked.first().map(|(index, _)| *index);
736 let (matches, context) = keep_ancestors(ranked, parents);
737 self.best = best.and_then(|best| matches.iter().position(|(i, _)| *i == best));
738 self.matches = matches;
739 self.context = context;
740 }
741 _ => {
742 self.best = (!ranked.is_empty()).then_some(0);
743 self.context = vec![false; ranked.len()];
744 self.matches = ranked;
745 }
746 }
747 }
748
749 pub fn is_context(&self, position: usize) -> bool {
752 self.context.get(position).copied().unwrap_or(false)
753 }
754
755 pub fn best(&self) -> Option<usize> {
758 self.best
759 }
760
761 pub fn matches(&self) -> &[(usize, Vec<usize>)] {
764 &self.matches
765 }
766
767 pub fn len(&self) -> usize {
769 self.matches.len()
770 }
771
772 pub fn is_empty(&self) -> bool {
773 self.matches.is_empty()
774 }
775
776 pub fn index(&self, position: usize) -> Option<usize> {
778 self.matches.get(position).map(|(index, _)| *index)
779 }
780
781 pub fn position_of(&self, index: usize) -> Option<usize> {
783 self.matches.iter().position(|(i, _)| *i == index)
784 }
785}
786
787pub fn ancestors(parents: &[Option<usize>], index: usize) -> impl Iterator<Item = usize> + '_ {
797 let mut at = parents.get(index).copied().flatten();
798 let mut left = parents.len();
800 std::iter::from_fn(move || {
801 let current = at.filter(|_| left > 0)?;
802 left -= 1;
803 at = parents.get(current).copied().flatten();
804 Some(current)
805 })
806}
807
808pub fn keep_ancestors(
828 matches: Vec<(usize, Vec<usize>)>,
829 parents: &[Option<usize>],
830) -> (Vec<(usize, Vec<usize>)>, Vec<bool>) {
831 let count = parents
832 .len()
833 .max(matches.iter().map(|(i, _)| i + 1).max().unwrap_or(0));
834 let mut found: Vec<Option<Vec<usize>>> = vec![None; count];
835 let mut kept = vec![false; count];
836 for (index, positions) in matches {
837 for ancestor in ancestors(parents, index) {
839 if kept.get(ancestor).is_none_or(|&kept| kept) {
840 break;
841 }
842 kept[ancestor] = true;
843 }
844 kept[index] = true;
845 found[index] = Some(positions);
846 }
847 let mut list = Vec::new();
848 let mut context = Vec::new();
849 for (index, positions) in found.into_iter().enumerate() {
850 if !kept[index] {
851 continue;
852 }
853 context.push(positions.is_none());
854 list.push((index, positions.unwrap_or_default()));
855 }
856 (list, context)
857}
858
859#[derive(Clone, Debug, Default, PartialEq, Eq)]
871pub struct TextBuffer {
872 value: String,
873 caret: usize,
874 undo: Vec<(String, usize)>,
875}
876
877impl TextBuffer {
878 pub fn new() -> TextBuffer {
879 TextBuffer::default()
880 }
881
882 pub fn with_text(text: impl Into<String>) -> TextBuffer {
884 let mut buffer = TextBuffer::new();
885 buffer.set_text(text);
886 buffer
887 }
888
889 pub fn text(&self) -> &str {
890 &self.value
891 }
892
893 pub fn is_empty(&self) -> bool {
894 self.value.is_empty()
895 }
896
897 pub fn len(&self) -> usize {
899 self.value.chars().count()
900 }
901
902 pub fn set_text(&mut self, text: impl Into<String>) {
904 self.value = text.into();
905 self.caret = self.len();
906 }
907
908 pub fn caret(&self) -> usize {
910 self.caret
911 }
912
913 pub fn set_caret(&mut self, caret: usize) {
916 self.caret = caret.min(self.len());
917 self.snap(false);
918 }
919
920 fn snap(&mut self, forward: bool) {
925 let stops = self.stops();
926 self.caret = if forward {
927 stops.iter().copied().find(|&stop| stop >= self.caret)
928 } else {
929 stops.iter().rev().copied().find(|&stop| stop <= self.caret)
930 }
931 .unwrap_or(self.caret);
932 }
933
934 pub fn byte(&self, caret: usize) -> usize {
936 self.value
937 .char_indices()
938 .nth(caret)
939 .map_or(self.value.len(), |(index, _)| index)
940 }
941
942 pub fn before_caret(&self) -> &str {
944 &self.value[..self.byte(self.caret)]
945 }
946
947 pub fn stops(&self) -> Vec<usize> {
950 let (spans, _) = split_graphemes(&self.value);
951 let mut stops = Vec::with_capacity(spans.len() + 1);
952 let (mut chars, mut byte) = (0, 0);
953 for (start, _, _) in spans {
954 chars += self.value[byte..start].chars().count();
955 byte = start;
956 stops.push(chars);
957 }
958 stops.push(self.value.chars().count());
959 stops
960 }
961
962 pub fn previous_stop(&self) -> usize {
964 self.stops()
965 .iter()
966 .rev()
967 .copied()
968 .find(|&stop| stop < self.caret)
969 .unwrap_or(0)
970 }
971
972 pub fn next_stop(&self) -> usize {
974 self.stops()
975 .iter()
976 .copied()
977 .find(|&stop| stop > self.caret)
978 .unwrap_or(self.caret)
979 }
980
981 pub fn insert(&mut self, text: &str) {
983 let at = self.byte(self.caret);
984 self.value.insert_str(at, text);
985 self.caret += text.chars().count();
986 self.snap(true);
987 }
988
989 pub fn left(&mut self) {
991 self.caret = self.previous_stop();
992 }
993
994 pub fn right(&mut self) {
996 self.caret = self.next_stop().min(self.len());
997 }
998
999 pub fn home(&mut self) {
1000 self.caret = 0;
1001 }
1002
1003 pub fn end(&mut self) {
1004 self.caret = self.len();
1005 }
1006
1007 pub fn backspace(&mut self) -> bool {
1009 if self.caret == 0 {
1010 return false;
1011 }
1012 let previous = self.previous_stop();
1013 let (from, to) = (self.byte(previous), self.byte(self.caret));
1014 self.value.replace_range(from..to, "");
1015 self.caret = previous;
1016 self.snap(false);
1017 true
1018 }
1019
1020 pub fn delete(&mut self) -> bool {
1022 if self.caret >= self.len() {
1023 return false;
1024 }
1025 let (from, to) = (self.byte(self.caret), self.byte(self.next_stop()));
1026 self.value.replace_range(from..to, "");
1027 self.snap(false);
1028 true
1029 }
1030
1031 pub fn delete_to_start(&mut self) {
1033 let at = self.byte(self.caret);
1034 self.value.replace_range(..at, "");
1035 self.caret = 0;
1036 }
1037
1038 pub fn delete_word(&mut self) {
1040 let chars: Vec<char> = self.value.chars().collect();
1041 let mut start = self.caret;
1042 while start > 0 && chars[start - 1] == ' ' {
1043 start -= 1;
1044 }
1045 while start > 0 && chars[start - 1] != ' ' {
1046 start -= 1;
1047 }
1048 let (from, to) = (self.byte(start), self.byte(self.caret));
1049 self.value.replace_range(from..to, "");
1050 self.caret = start;
1051 self.snap(false);
1052 }
1053
1054 pub fn checkpoint(&mut self) {
1056 let point = (self.value.clone(), self.caret);
1057 if self.undo.last() != Some(&point) {
1058 self.undo.push(point);
1059 }
1060 }
1061
1062 pub fn undo(&mut self) -> bool {
1064 match self.undo.pop() {
1065 Some((value, caret)) => {
1066 self.value = value;
1067 self.caret = caret;
1068 true
1069 }
1070 None => false,
1071 }
1072 }
1073
1074 pub fn masked(text: &str, mask: Option<char>) -> String {
1076 match mask {
1077 Some(mask) => std::iter::repeat_n(mask, text.chars().count()).collect(),
1078 None => text.to_string(),
1079 }
1080 }
1081
1082 pub fn window(&self, available: usize, mask: Option<char>) -> (String, usize) {
1086 let shown = Self::masked(&self.value, mask);
1087 let caret = shown
1090 .char_indices()
1091 .nth(self.caret)
1092 .map_or(shown.len(), |(index, _)| index);
1093 let (spans, _) = split_graphemes(&shown);
1097 let before: usize = spans
1098 .iter()
1099 .filter(|(start, _, _)| *start < caret)
1100 .map(|(_, _, cells)| cells)
1101 .sum();
1102 let skip = (before + 1).saturating_sub(available.max(1));
1104 if skip == 0 {
1105 return (shown, before);
1106 }
1107 let mut dropped = 0;
1108 let mut from = shown.len();
1109 for (start, _, cells) in spans {
1110 if dropped >= skip || start >= caret {
1111 from = start;
1112 break;
1113 }
1114 dropped += cells;
1115 }
1116 (shown[from..].to_string(), before.saturating_sub(dropped))
1117 }
1118}
1119
1120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1130#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
1131pub struct Divider {
1132 position: Option<usize>,
1133 dragging: bool,
1134 pub min_before: usize,
1136 pub min_after: usize,
1138 pub thickness: usize,
1140}
1141
1142impl Default for Divider {
1143 fn default() -> Self {
1144 Divider::new(1)
1145 }
1146}
1147
1148impl Divider {
1149 pub fn new(thickness: usize) -> Divider {
1151 Divider {
1152 position: None,
1153 dragging: false,
1154 min_before: 0,
1155 min_after: 0,
1156 thickness,
1157 }
1158 }
1159
1160 pub fn min(mut self, cells: usize) -> Divider {
1162 self.min_before = cells;
1163 self.min_after = cells;
1164 self
1165 }
1166
1167 pub fn at(mut self, position: usize) -> Divider {
1169 self.position = Some(position);
1170 self
1171 }
1172
1173 pub fn position(&self) -> Option<usize> {
1175 self.position
1176 }
1177
1178 pub fn set_position(&mut self, position: Option<usize>) {
1179 self.position = position;
1180 }
1181
1182 pub fn dragging(&self) -> bool {
1184 self.dragging
1185 }
1186
1187 pub fn clamp(&self, position: usize, total: usize) -> usize {
1190 let most = total
1191 .saturating_sub(self.min_after + self.thickness)
1192 .max(self.min_before.min(total));
1193 position.clamp(self.min_before.min(most), most)
1194 }
1195
1196 pub fn resolve(&self, total: usize, default: usize) -> usize {
1199 match self.position {
1200 Some(position) => self.clamp(position, total),
1201 None => default,
1202 }
1203 }
1204
1205 pub fn hit(&self, at: usize, first: usize) -> bool {
1208 (first..first + self.thickness.max(1)).contains(&at)
1209 }
1210
1211 pub fn press(&mut self, at: usize, first: usize) -> bool {
1214 self.dragging = self.hit(at, first);
1215 self.dragging
1216 }
1217
1218 pub fn drag(&mut self, at: usize, total: usize) -> bool {
1221 if !self.dragging {
1222 return false;
1223 }
1224 self.position = Some(self.clamp(at, total));
1225 true
1226 }
1227
1228 pub fn release(&mut self) {
1230 self.dragging = false;
1231 }
1232
1233 pub fn handle_mouse(&mut self, mouse: &Mouse, at: usize, first: usize, total: usize) -> bool {
1237 match mouse.kind {
1238 MouseKind::Down(crate::event::Button::Left) => self.press(at, first),
1239 MouseKind::Drag(crate::event::Button::Left) => self.drag(at, total),
1240 MouseKind::Up(_) if self.dragging => {
1241 self.release();
1242 true
1243 }
1244 _ => false,
1245 }
1246 }
1247
1248 pub fn nudge(&mut self, delta: isize, first: usize, total: usize) {
1250 self.position = Some(self.clamp(first.saturating_add_signed(delta), total));
1251 }
1252}
1253
1254const MENU_HINTS: &str = " ↑↓ move · enter run · esc close";
1259
1260#[derive(Clone, Debug, PartialEq, Eq)]
1262pub enum MenuReply {
1263 Stay,
1265 Close,
1267 Run(Action),
1269}
1270
1271#[derive(Clone, Debug)]
1276pub struct ActionMenu {
1277 pub actions: Vec<Action>,
1278 pub focus: usize,
1279 pub key: Key,
1281}
1282
1283impl ActionMenu {
1284 pub fn new(actions: Vec<Action>, key: Key) -> ActionMenu {
1285 ActionMenu {
1286 actions,
1287 focus: 0,
1288 key,
1289 }
1290 }
1291
1292 pub fn handle(&mut self, event: &Event, first: usize) -> MenuReply {
1295 let count = self.actions.len();
1296 if let Some(mouse) = event.mouse() {
1297 if mouse.is_click() {
1298 let row = mouse.row as usize;
1299 if row >= first && row < first + count {
1300 return MenuReply::Run(self.actions[row - first].clone());
1301 }
1302 return MenuReply::Close;
1303 }
1304 return MenuReply::Stay;
1305 }
1306 let Some(key) = event.key() else {
1307 return MenuReply::Stay;
1308 };
1309 match key.code {
1310 KeyCode::Escape => return MenuReply::Close,
1311 _ if key.matches(&self.key) => return MenuReply::Close,
1312 KeyCode::Up => self.focus = (self.focus + count - 1) % count.max(1),
1313 KeyCode::Down | KeyCode::Tab => self.focus = (self.focus + 1) % count.max(1),
1314 KeyCode::Enter => return MenuReply::Run(self.actions[self.focus].clone()),
1315 _ => {
1316 if let Some(at) = key.pick(self.actions.iter().map(|a| a.key.as_slice())) {
1317 return MenuReply::Run(self.actions[at].clone());
1318 }
1319 }
1320 }
1321 MenuReply::Stay
1322 }
1323
1324 pub fn size(&self) -> (usize, usize) {
1327 let label_width = self
1328 .actions
1329 .iter()
1330 .map(|action| cell_len(&action.label))
1331 .max()
1332 .unwrap_or(0);
1333 let key_width = self
1334 .actions
1335 .iter()
1336 .filter_map(|action| action.key.map(|key| cell_len(&key.to_string()) + 2))
1337 .max()
1338 .unwrap_or(0);
1339 let hints = cell_len(MENU_HINTS);
1340 (
1341 (4 + label_width + key_width).max(hints) + 2,
1342 self.actions.len() + 1,
1343 )
1344 }
1345
1346 pub fn render(&self, theme: &Theme, width: usize) -> Vec<Vec<Segment>> {
1348 let label_width = self
1349 .actions
1350 .iter()
1351 .map(|action| cell_len(&action.label))
1352 .max()
1353 .unwrap_or(0);
1354 let mut lines = Vec::new();
1355 for (index, action) in self.actions.iter().enumerate() {
1356 let focused = index == self.focus;
1357 let mut line = if focused {
1358 vec![text(format!(" {} ", theme.pointer), &theme.pointer_style)]
1359 } else {
1360 vec![plain(" ")]
1361 };
1362 let padding = label_width - cell_len(&action.label);
1363 line.push(if focused {
1364 text(action.label.clone(), &theme.focused)
1365 } else {
1366 plain(action.label.clone())
1367 });
1368 if let Some(key) = action.key {
1369 line.push(text(format!("{} {key}", " ".repeat(padding)), &theme.hint));
1370 }
1371 lines.push(fit(line, width));
1372 }
1373 lines.push(fit(vec![text(MENU_HINTS.to_string(), &theme.hint)], width));
1374 lines
1375 }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380 use super::*;
1381
1382 #[test]
1383 fn highlights_matched_characters() {
1384 let matched = Style::parse("bold").unwrap();
1385 let segments = highlight("main.rs", &[0, 1, 5], None, &matched);
1386 let texts: Vec<(&str, bool)> = segments
1387 .iter()
1388 .map(|s| (s.text.as_str(), s.style.is_some()))
1389 .collect();
1390 assert_eq!(
1391 texts,
1392 [("ma", true), ("in.", false), ("r", true), ("s", false)]
1393 );
1394 }
1395
1396 #[test]
1397 fn pastes_drop_terminal_controls() {
1398 assert_eq!(
1399 pasted("X\x1bcY\u{9b}2JZ\x07\x08W\tV\r\nU", " "),
1400 "XcY2JZW V U"
1401 );
1402 assert_eq!(pasted("a\nb", ""), "ab");
1403 }
1404
1405 #[test]
1406 fn fits_and_pads_lines() {
1407 let line = vec![plain("hello "), plain("world")];
1408 assert_eq!(width(&fit(line.clone(), 7)), 7);
1409 assert_eq!(width(&fit(line.clone(), 20)), 11);
1410 assert_eq!(width(&pad(line, 20)), 20);
1411 }
1412
1413 #[test]
1414 fn slices_and_overlays_by_cell() {
1415 let base = vec![plain("ab"), plain("日本"), plain("cd")];
1416 let cut: String = slice(&base, 3, 7).iter().map(|s| s.text.as_str()).collect();
1417 assert_eq!(cut, " 本c", "half a wide character is a space");
1418 let over = overlay(&base, 1, &[plain("XY")]);
1419 let text: String = over.iter().map(|s| s.text.as_str()).collect();
1420 assert_eq!(text, "aXY 本cd");
1421 let past = overlay(&[plain("ab")], 4, &[plain("Z")]);
1422 assert_eq!(width(&past), 5);
1423 }
1424
1425 #[test]
1426 fn a_list_follows_its_cursor() {
1427 let mut list = ListState::new();
1428 list.set_len(20);
1429 list.step(7, 5);
1430 assert_eq!((list.cursor(), list.offset()), (7, 3));
1431 assert_eq!(list.visible(5), 3..8);
1432 list.step(-100, 5);
1433 assert_eq!((list.cursor(), list.offset()), (0, 0));
1434 list.step(isize::MAX / 2, 5);
1435 assert_eq!(list.cursor(), 19);
1436 list.toggle(4);
1437 list.toggle(2);
1438 assert_eq!(list.selected(), [2, 4]);
1439 list.toggle(4);
1440 assert_eq!(list.selected_count(), 1);
1441 }
1442
1443 #[test]
1444 fn a_filter_ranks_and_hides() {
1445 let mut filter = FilterState::new(["alpha", "beta", "gamma"]);
1446 filter.set_hidden(vec![false, true, false]);
1447 filter.refilter();
1448 assert_eq!(filter.len(), 2, "hidden while nothing is typed");
1449 filter.set_query("ta");
1450 assert_eq!(filter.index(0), Some(1), "hidden ones still match a query");
1451 assert!(filter.pop());
1452 filter.clear();
1453 assert!(!filter.is_filtering());
1454 filter.set_highlight_limits(vec![2, 2, 2]);
1455 filter.set_query("ma");
1456 assert!(filter.matches()[0].1.iter().all(|p| *p < 2));
1457 }
1458
1459 #[test]
1460 fn a_text_buffer_edits_by_grapheme() {
1461 let mut buffer = TextBuffer::with_text("cafe\u{301} au lait");
1462 buffer.checkpoint();
1463 buffer.delete_word();
1464 assert_eq!(buffer.text(), "cafe\u{301} au ");
1465 buffer.home();
1466 buffer.right();
1467 buffer.right();
1468 buffer.right();
1469 buffer.right();
1470 assert_eq!(buffer.caret(), 5, "the accent moves with its letter");
1471 assert!(buffer.backspace());
1472 assert_eq!(buffer.text(), "caf au ");
1473 buffer.delete_to_start();
1474 assert_eq!(buffer.text(), " au ");
1475 assert!(buffer.undo());
1476 assert_eq!(buffer.text(), "cafe\u{301} au lait");
1477 let (shown, column) = TextBuffer::with_text("abcdefgh").window(4, Some('*'));
1478 assert_eq!((shown.as_str(), column), ("***", 3));
1479 }
1480
1481 #[test]
1482 fn a_divider_drags_within_its_minimums() {
1483 let mut divider = Divider::new(3).min(12);
1484 assert_eq!(divider.clamp(5, 80), 12);
1485 assert_eq!(divider.clamp(79, 80), 80 - 12 - 3);
1486 assert_eq!(divider.resolve(80, 36), 36);
1487 assert!(!divider.press(10, 36));
1488 assert!(divider.press(37, 36));
1489 assert!(divider.drag(50, 80));
1490 divider.release();
1491 assert!(!divider.drag(60, 80));
1492 assert_eq!(divider.resolve(80, 36), 50);
1493 divider.nudge(-100, 50, 80);
1494 assert_eq!(divider.resolve(80, 36), 12);
1495 }
1496}