Skip to main content

rich_interact/components/
views.rs

1//! Tables and trees to pick from (#491): a [`Select`] whose items are a
2//! table's rows or a tree's nodes, so they filter, preview and take
3//! [`Actions`] the same way, with each action told what it is done to (a
4//! [`TargetKind::Row`] or a [`TargetKind::Node`]).
5//!
6//! Since 0.0.14 a table copies its focused row or cell as text, CSV or
7//! JSON (#434), and a tree filters as a tree, keeping each match's
8//! ancestors (#428), shows the focused node's breadcrumbs and copies its
9//! path (#464), all through the terminal [clipboard](crate::clipboard).
10//!
11//! Since 0.0.7 a table wider than its list scrolls across (#717), with
12//! leading columns frozen if asked.
13
14use std::cell::Cell;
15use std::sync::Arc;
16use std::time::Duration;
17
18use rich::cells::cell_len;
19use rich::{Segment, Style};
20
21use crate::clipboard::{self, CopyFormat};
22use crate::component::{Component, Context, Flow, View};
23use crate::components::{PreviewLayout, Select, Theme, fit, pad, plain, shown, text};
24use crate::event::{Event, Key, KeyCode, MouseKind};
25use crate::item::{Actions, Item, TargetKind};
26use crate::keymap::{Keymap, keys};
27use crate::policy::{LineIo, NotInteractive};
28
29/// The builder methods both views pass on to their [`Select`].
30macro_rules! select_builders {
31    () => {
32        /// Show at most `rows` rows at once (default 10).
33        pub fn height(mut self, rows: usize) -> Self {
34            self.select = self.select.height(rows);
35            self
36        }
37
38        pub fn preview(mut self, layout: PreviewLayout) -> Self {
39            self.select = self.select.preview(layout);
40            self
41        }
42
43        pub fn theme(mut self, theme: Theme) -> Self {
44            self.select = self.select.theme(theme);
45            self
46        }
47
48        /// Start with this filter text.
49        pub fn query(mut self, query: impl Into<String>) -> Self {
50            self.select = self.select.query(query);
51            self
52        }
53
54        /// The row returned without a terminal, and focused first.
55        pub fn default(mut self, index: usize) -> Self {
56            self.select = self.select.default(index);
57            self
58        }
59
60        /// Actions offered on every row, beside each row's own.
61        pub fn actions(mut self, actions: Actions) -> Self {
62            self.select = self.select.actions(actions);
63            self
64        }
65
66        /// The key that opens the action menu (default Ctrl+K).
67        pub fn menu_key(mut self, key: Key) -> Self {
68            self.select = self.select.menu_key(key);
69            self
70        }
71
72        /// Report the mouse: see [`Select::with_mouse`].
73        pub fn with_mouse(mut self, on: bool) -> Self {
74            self.select = self.select.with_mouse(on);
75            self
76        }
77
78        /// The id of the action that picked the row, if one did.
79        pub fn action(&self) -> Option<&str> {
80            self.select.action()
81        }
82
83        pub fn items(&self) -> &[Item<T>] {
84            self.select.items()
85        }
86
87        /// The underlying list.
88        pub fn select(&self) -> &Select<T> {
89            &self.select
90        }
91
92        /// An icon before each row's label, by item index: see
93        /// [`Select::set_icons`].
94        pub fn set_icons(&mut self, icons: Vec<Option<rich::Text>>) {
95            self.select.set_icons(icons);
96        }
97    };
98}
99
100/// The keys a [`TableSelect`] adds to its list's, in context `table`.
101pub fn table_keymap() -> Keymap {
102    Keymap::new("table")
103        .bind("copy-row", keys("ctrl+y"), "copy the row")
104        .bind("copy-cell", keys("alt+y"), "copy the focused cell")
105        .bind("next-column", keys("ctrl+right"), "focus the next column")
106        .bind(
107            "previous-column",
108            keys("ctrl+left"),
109            "focus the previous column",
110        )
111        .bind("copy-format", keys("alt+f"), "copy as text, CSV or JSON")
112        .bind("scroll-left", keys("left"), "scroll a column left")
113        .bind("scroll-right", keys("right"), "scroll a column right")
114        .bind(
115            "scroll-start",
116            keys("shift+home"),
117            "scroll to the first column",
118        )
119        .bind("scroll-end", keys("shift+end"), "scroll to the last column")
120}
121
122/// The `table` actions that scroll across, live only with
123/// [`TableSelect::scroll_x`].
124const SCROLLING: [&str; 4] = ["scroll-left", "scroll-right", "scroll-start", "scroll-end"];
125
126/// A wide table's horizontal scroll (#717): the first `frozen` cells of a
127/// label (the frozen columns) stay, and the rest shows from cell
128/// `frozen + offset`. The heading is cut the same way, so it stays over
129/// its column.
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub(crate) struct Pan {
132    frozen: usize,
133    offset: usize,
134    /// Whether columns are out of view to the left, and to the right.
135    left: bool,
136    right: bool,
137}
138
139impl Pan {
140    /// The cells of `line` in view.
141    pub(crate) fn window(&self, line: &[Segment]) -> Vec<Segment> {
142        if self.offset == 0 {
143            return line.to_vec();
144        }
145        let mut out = crate::kit::slice(line, 0, self.frozen);
146        out.extend(crate::kit::slice(
147            line,
148            self.frozen + self.offset,
149            usize::MAX,
150        ));
151        out
152    }
153
154    /// The heading line at most `width` wide: `‹` in its indent while
155    /// columns are out of view to the left, and `›` in its last cell while
156    /// they are to the right.
157    pub(crate) fn heading(&self, indent: &str, heading: &[Segment], width: usize) -> Vec<Segment> {
158        let indent = if self.left {
159            format!("‹{}", &indent[1..])
160        } else {
161            indent.to_string()
162        };
163        let mut line = vec![plain(indent)];
164        line.extend(self.window(heading));
165        if self.right && width > 1 {
166            let mut line = pad(line, width - 1);
167            line.push(plain("›"));
168            return line;
169        }
170        fit(line, width)
171    }
172}
173
174/// Pick a row of a table: columns aligned under their headings, rows
175/// filtered by any cell. Returns the row's value.
176///
177/// Ctrl+Y copies the focused row, and Alt+Y its focused cell, to the
178/// terminal's [clipboard](crate::clipboard) (#434): as text (cells
179/// separated by tabs), CSV or JSON (an object keyed by the headings),
180/// cycled with Alt+F or set with [`copy_format`](Self::copy_format).
181/// Ctrl+Right and Ctrl+Left move the focused column, which the headings
182/// underline.
183///
184/// With [`scroll_x`](Self::scroll_x), a table wider than its list scrolls
185/// across (#717): Left and Right move a column at a time, Shift+Home and
186/// Shift+End go to the ends, Shift with the wheel scrolls, and the view
187/// follows the focused column. `‹` and `›` at the ends of the headings say
188/// that columns are out of view on that side.
189/// [`freeze_columns`](Self::freeze_columns) keeps leading columns in place.
190#[must_use = "a component does nothing until it is run or mounted"]
191pub struct TableSelect<T> {
192    select: Select<T>,
193    /// The headings as drawn (controls shown as pictures).
194    headers: Vec<String>,
195    /// The headings and every row's cells as given: what copies carry.
196    raw_headers: Vec<String>,
197    cells: Vec<Vec<String>>,
198    widths: Vec<usize>,
199    /// The focused column, once moved to.
200    column: Option<usize>,
201    format: CopyFormat,
202    keymap: Keymap,
203    /// Scroll across when wider than the list (#717).
204    scroll: bool,
205    /// Leading columns that stay in place while the rest scroll.
206    freeze: usize,
207    /// The first column scrolled into view after the frozen ones.
208    left: usize,
209    /// The width of the list's labels at the last render.
210    span: Cell<usize>,
211}
212
213impl<T> TableSelect<T> {
214    /// A table of `rows`, each an item (its value, actions, preview) and
215    /// its cells, under `headers`.
216    pub fn new<H, S, R, V>(prompt: impl Into<String>, headers: H, rows: R) -> TableSelect<T>
217    where
218        H: IntoIterator<Item = S>,
219        S: Into<String>,
220        R: IntoIterator<Item = (V, Vec<String>)>,
221        V: Into<Item<T>>,
222    {
223        // Drawn sanitised; copied and given to actions as they are.
224        let raw_headers: Vec<String> = headers.into_iter().map(Into::into).collect();
225        let headers: Vec<String> = raw_headers.iter().map(|h| shown(h)).collect();
226        let rows: Vec<(Item<T>, Vec<String>)> = rows
227            .into_iter()
228            .map(|(item, cells)| (item.into(), cells))
229            .collect();
230        let columns = rows
231            .iter()
232            .map(|(_, cells)| cells.len())
233            .chain([headers.len()])
234            .max()
235            .unwrap_or(0);
236        let mut widths = vec![0; columns];
237        for (column, cell) in headers.iter().enumerate() {
238            widths[column] = widths[column].max(cell_len(cell));
239        }
240        for (_, cells) in &rows {
241            for (column, cell) in cells.iter().enumerate() {
242                widths[column] = widths[column].max(cell_len(&shown(cell)));
243            }
244        }
245        let line = |cells: &[String]| -> String {
246            let mut out = String::new();
247            for (column, width) in widths.iter().enumerate() {
248                let cell = cells.get(column).map_or("", String::as_str);
249                out.push_str(cell);
250                if column + 1 < widths.len() {
251                    out.push_str(&" ".repeat(width - cell_len(cell) + 2));
252                }
253            }
254            out.trim_end().to_string()
255        };
256        let mut values = Vec::with_capacity(rows.len());
257        let mut table = Vec::with_capacity(rows.len());
258        let items: Vec<Item<T>> = rows
259            .into_iter()
260            .map(|(mut item, cells)| {
261                let drawn: Vec<String> = cells.iter().map(|cell| shown(cell)).collect();
262                item.label = line(&drawn);
263                values.push(cells.join("\t"));
264                table.push(cells);
265                item
266            })
267            .collect();
268        let mut select = Select::new(prompt, items);
269        select.set_kind(TargetKind::Row);
270        select.set_values(values);
271        let mut view = TableSelect {
272            select,
273            headers,
274            raw_headers,
275            cells: table,
276            widths,
277            column: None,
278            format: CopyFormat::Text,
279            keymap: table_keymap(),
280            scroll: false,
281            freeze: 0,
282            left: 0,
283            span: Cell::new(usize::MAX),
284        };
285        view.heading();
286        view
287    }
288
289    select_builders!();
290
291    /// How Ctrl+Y and Alt+Y copy (default: text).
292    pub fn copy_format(mut self, format: CopyFormat) -> Self {
293        self.format = format;
294        self
295    }
296
297    /// The focused column, once one has been moved to.
298    pub fn column(&self) -> Option<usize> {
299        self.column
300    }
301
302    /// Scroll across when the table is wider than its list (#717), rather
303    /// than cutting it at the right edge: see the [type](Self)'s keys.
304    pub fn scroll_x(mut self, on: bool) -> Self {
305        self.scroll = on;
306        self.select.set_hints(on.then(|| "←→ scroll".to_string()));
307        self
308    }
309
310    /// Keep the first `columns` columns in place while the rest scroll
311    /// across; turns [`scroll_x`](Self::scroll_x) on.
312    pub fn freeze_columns(mut self, columns: usize) -> Self {
313        self.freeze = columns;
314        self.scroll_x(true)
315    }
316
317    /// The first column in view after the frozen ones, as of the last
318    /// render.
319    pub fn first_column(&self) -> usize {
320        self.left
321            .clamp(self.frozen_columns(), self.last_left(self.span.get()))
322    }
323
324    /// Where each column starts in a row's label.
325    fn starts(&self) -> Vec<usize> {
326        let mut at = 0;
327        self.widths
328            .iter()
329            .map(|width| {
330                let start = at;
331                at += width + 2;
332                start
333            })
334            .collect()
335    }
336
337    /// How wide a row's label is at most.
338    fn total(&self) -> usize {
339        let starts = self.starts();
340        match (starts.last(), self.widths.last()) {
341            (Some(start), Some(width)) => start + width,
342            _ => 0,
343        }
344    }
345
346    /// The frozen columns there are.
347    fn frozen_columns(&self) -> usize {
348        self.freeze.min(self.widths.len())
349    }
350
351    /// The cells the frozen columns take, the gap after them included.
352    fn frozen_cells(&self) -> usize {
353        match self.frozen_columns() {
354            0 => 0,
355            n if n == self.widths.len() => self.total(),
356            n => self.starts()[n],
357        }
358    }
359
360    /// The furthest the view scrolls in labels `span` wide: the first
361    /// column from which the rest of the table fits.
362    fn last_left(&self, span: usize) -> usize {
363        let first = self.frozen_columns();
364        let (starts, total, frozen) = (self.starts(), self.total(), self.frozen_cells());
365        (first..self.widths.len())
366            .find(|&left| total - (starts[left] - frozen) <= span)
367            .unwrap_or_else(|| self.widths.len().saturating_sub(1).max(first))
368    }
369
370    /// What of each label shows in labels `span` wide.
371    fn pan(&self, span: usize) -> Option<Pan> {
372        if !self.scroll {
373            return None;
374        }
375        let first = self.frozen_columns();
376        let left = self.left.clamp(first, self.last_left(span));
377        let frozen = self.frozen_cells();
378        let offset = match self.starts().get(left) {
379            Some(start) if left > first => start - frozen,
380            _ => 0,
381        };
382        Some(Pan {
383            frozen,
384            offset,
385            left: offset > 0,
386            right: self.total() - offset > span,
387        })
388    }
389
390    /// Scroll so that `column` is in view in labels `span` wide.
391    fn follow(&mut self, column: usize, span: usize) {
392        let first = self.frozen_columns();
393        if column < first {
394            return;
395        }
396        let (starts, frozen) = (self.starts(), self.frozen_cells());
397        let mut left = self.left.clamp(first, self.last_left(span));
398        if column < left {
399            left = column;
400        }
401        while left < column && starts[column] + self.widths[column] - (starts[left] - frozen) > span
402        {
403            left += 1;
404        }
405        self.left = left;
406    }
407
408    /// Scroll across for `action` in labels `span` wide.
409    fn scroll_by(&mut self, action: &str, span: usize) {
410        let first = self.frozen_columns();
411        let last = self.last_left(span);
412        let left = self.left.clamp(first, last);
413        self.left = match action {
414            "scroll-left" => left.saturating_sub(1).max(first),
415            "scroll-right" => (left + 1).min(last),
416            "scroll-start" => first,
417            _ => last,
418        };
419    }
420
421    /// Make `keys` do `action` (see [`table_keymap`]) on this table only.
422    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
423        self.keymap.rebind(action, keys);
424        self
425    }
426
427    /// The row of headings, the focused column underlined.
428    fn heading(&mut self) {
429        let style = Theme::default().prompt;
430        let Some(focused_column) = self.column else {
431            // One segment, as before columns could be focused.
432            let mut heading = String::new();
433            for (column, width) in self.widths.iter().enumerate() {
434                let cell = self.headers.get(column).map_or("", String::as_str);
435                heading.push_str(cell);
436                if column + 1 < self.widths.len() {
437                    heading.push_str(&" ".repeat(width - cell_len(cell) + 2));
438                }
439            }
440            self.select.heading = Some(vec![text(heading.trim_end(), &style)]);
441            return;
442        };
443        let focused = style.combine(&Style::parse("underline").expect("a style"));
444        let mut line: Vec<Segment> = Vec::new();
445        let last = self.widths.len().saturating_sub(1);
446        for (column, width) in self.widths.iter().enumerate() {
447            let cell = self.headers.get(column).map_or("", String::as_str);
448            let style = if column == focused_column {
449                &focused
450            } else {
451                &style
452            };
453            if !cell.is_empty() {
454                line.push(text(cell, style));
455            }
456            if column < last {
457                line.push(plain(" ".repeat(width - cell_len(cell) + 2)));
458            }
459        }
460        // As before a column was focused: no padding after the last heading.
461        while line
462            .last()
463            .is_some_and(|segment| segment.style.is_none() && segment.text.trim().is_empty())
464        {
465            line.pop();
466        }
467        self.select.heading = Some(line);
468    }
469
470    /// Copy the focused row, or its focused cell.
471    fn copy(&mut self, cell: bool) {
472        let Some(index) = self.select.focused() else {
473            return;
474        };
475        let row = &self.cells[index];
476        let (what, copied) = if cell {
477            let column = self.column.unwrap_or(0);
478            let value = row.get(column).map_or("", String::as_str);
479            ("cell", self.format.cell(value))
480        } else {
481            ("row", self.format.row(&self.raw_headers, row))
482        };
483        let result = clipboard::copy(copied);
484        let what = format!("{what} as {}", self.format.name());
485        self.select
486            .set_status(Some(clipboard::report(&what, &result)));
487    }
488
489    /// Carry out a table key; `false` when `key` is not one. `span` is
490    /// how wide the list's labels are.
491    fn table_key(&mut self, key: Key, span: usize) -> bool {
492        match self.keymap.action(key) {
493            Some("copy-row") => self.copy(false),
494            Some("copy-cell") => self.copy(true),
495            Some(step @ ("next-column" | "previous-column")) => {
496                let columns = self.widths.len().max(1);
497                let at = self.column.unwrap_or(0);
498                let column = if step == "next-column" {
499                    (at + 1).min(columns - 1)
500                } else {
501                    at.saturating_sub(1)
502                };
503                self.column = Some(column);
504                if self.scroll {
505                    self.follow(column, span);
506                }
507                self.heading();
508            }
509            Some(action) if self.scroll && SCROLLING.contains(&action) => {
510                let action = action.to_string();
511                self.scroll_by(&action, span);
512            }
513            Some("copy-format") => {
514                self.format = self.format.next();
515                self.select
516                    .set_status(Some(format!("copy as {}", self.format.name())));
517            }
518            _ => return false,
519        }
520        true
521    }
522}
523
524impl<T: Clone> Component for TableSelect<T> {
525    type Output = T;
526
527    fn keymap(&self) -> Keymap {
528        let mut keymap = if self.select.menu_open() {
529            Keymap::default()
530        } else if self.scroll {
531            self.keymap.clone()
532        } else {
533            let mut keymap = Keymap::new("table");
534            for binding in self.keymap.bindings() {
535                if !SCROLLING.contains(&binding.action.as_str()) {
536                    keymap.add(binding);
537                }
538            }
539            keymap
540        };
541        keymap.extend(self.select.visible_keymap());
542        keymap
543    }
544
545    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<T> {
546        // The labels follow the pointer's two cells.
547        let span = self.select.list_width(context.width).saturating_sub(2);
548        self.span.set(span);
549        if let Some(key) = event.key()
550            && !self.select.menu_open()
551            && self.table_key(key, span)
552        {
553            return Flow::Continue;
554        }
555        // Shift with the wheel scrolls across.
556        if let Event::Mouse(mouse) = event {
557            let across = match mouse.kind {
558                MouseKind::ScrollUp => Some("scroll-left"),
559                MouseKind::ScrollDown => Some("scroll-right"),
560                _ => None,
561            };
562            if let Some(action) =
563                across.filter(|_| self.scroll && mouse.modifiers.shift && !self.select.menu_open())
564            {
565                self.scroll_by(action, span);
566                return Flow::Continue;
567            }
568        }
569        self.select.handle(event, context)
570    }
571
572    fn render(&self, context: &Context<'_>) -> View {
573        let span = self.select.list_width(context.width).saturating_sub(2);
574        self.span.set(span);
575        self.select.pan.set(self.pan(span));
576        self.select.render(context)
577    }
578
579    fn tick(&self) -> Option<Duration> {
580        self.select.tick()
581    }
582
583    fn mouse(&self) -> bool {
584        Component::mouse(&self.select)
585    }
586
587    fn default_value(&self) -> Option<T> {
588        self.select.default_value()
589    }
590
591    fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<T>, NotInteractive> {
592        self.select.prompt(io)
593    }
594}
595
596/// The keys a [`TreeSelect`] adds to its list's, in context `tree`.
597pub fn tree_keymap() -> Keymap {
598    Keymap::new("tree")
599        .bind("expand", keys("right"), "expand")
600        .bind("collapse", keys("left"), "collapse, or go to the parent")
601        .bind("copy-path", keys("ctrl+y"), "copy the path")
602}
603
604/// Pick a node of a tree. Nodes are given in order, each with its depth
605/// (0 for a root); Right expands the focused node and Left collapses it,
606/// or moves to its parent.
607///
608/// Typing searches every node, collapsed or not, and filters as a tree
609/// (#428): each match keeps its ancestors, dimmed, so you see where it is,
610/// and the cursor goes to the best match. With
611/// [`breadcrumbs`](Self::breadcrumbs) on, a line under the question shows
612/// the path to the focused node (#464). Ctrl+Y copies that path (the
613/// node's [`ActionTarget::value`](crate::ActionTarget::value)) to the
614/// terminal's [clipboard](crate::clipboard).
615#[must_use = "a component does nothing until it is run or mounted"]
616pub struct TreeSelect<T> {
617    select: Select<T>,
618    parents: Arc<[Option<usize>]>,
619    /// Whether each node is the last of its parent's children.
620    last: Vec<bool>,
621    parent_of_any: Vec<bool>,
622    collapsed: Vec<bool>,
623    /// Each node's name in the breadcrumbs, by index, where it is not its
624    /// label.
625    crumbs: Vec<String>,
626    breadcrumbs: bool,
627    /// The width the breadcrumbs were last laid out at.
628    width: usize,
629    /// Whether the guides are drawn for a filtered list.
630    filtered: bool,
631    keymap: Keymap,
632}
633
634impl<T> TreeSelect<T> {
635    /// A tree of `nodes`, in order, each with its depth.
636    ///
637    /// Building it is linear in the number of nodes, and so is what it
638    /// keeps: each node's path (the labels joined by `/`) is worked out
639    /// when it is asked for, and its guides when its row is drawn.
640    pub fn new<I, V>(prompt: impl Into<String>, nodes: I) -> TreeSelect<T>
641    where
642        I: IntoIterator<Item = (usize, V)>,
643        V: Into<Item<T>>,
644    {
645        let mut items: Vec<Item<T>> = Vec::new();
646        let mut parents: Vec<Option<usize>> = Vec::new();
647        // The open path: the last node seen at each depth down to the one
648        // before.
649        let mut open: Vec<usize> = Vec::new();
650        for (depth, node) in nodes {
651            // No deeper than one below the node before.
652            let depth = depth.min(open.len());
653            open.truncate(depth);
654            let index = items.len();
655            parents.push(open.last().copied());
656            open.push(index);
657            let mut item: Item<T> = node.into();
658            item.label = shown(&item.label);
659            items.push(item);
660        }
661        let count = items.len();
662        let mut parent_of_any = vec![false; count];
663        for parent in parents.iter().flatten() {
664            parent_of_any[*parent] = true;
665        }
666        let last = last_children(&parents, None);
667        let parents: Arc<[Option<usize>]> = parents.into();
668        let mut select = Select::new(prompt, items);
669        select.set_kind(TargetKind::Node);
670        select.hints = Some("←→ fold".into());
671        select.set_tree(Some(parents.to_vec()));
672        let mut tree = TreeSelect {
673            select,
674            parents,
675            last,
676            parent_of_any,
677            collapsed: vec![false; count],
678            crumbs: Vec::new(),
679            breadcrumbs: false,
680            width: usize::MAX,
681            filtered: false,
682            keymap: tree_keymap(),
683        };
684        tree.update();
685        tree
686    }
687
688    /// Show the path to the focused node on a line under the question
689    /// (#464): `root › parent › node`, cut from the left when it is too
690    /// wide.
691    pub fn breadcrumbs(mut self, on: bool) -> Self {
692        self.breadcrumbs = on;
693        self.crumb_line();
694        self
695    }
696
697    /// Each node's name in the breadcrumbs, by index, in place of its
698    /// label: a key rather than `key: value`, say.
699    pub fn crumbs(mut self, crumbs: Vec<String>) -> Self {
700        let count = self.parents.len();
701        self.crumbs = crumbs
702            .into_iter()
703            .take(count)
704            .map(|crumb| shown(&crumb))
705            .collect();
706        self.crumb_line();
707        self
708    }
709
710    /// What Ctrl+Y copies and actions see as each node's value, by index,
711    /// in place of the labels joined by `/`: a JSON path, say. Nodes past
712    /// the end of `paths` keep the joined labels.
713    pub fn paths(mut self, paths: Vec<String>) -> Self {
714        self.select.set_values(paths);
715        self
716    }
717
718    /// Like [`paths`](Self::paths), with each node's value worked out from
719    /// its index when it is asked for (the focused node's, on Ctrl+Y or an
720    /// action), so a large tree keeps no string per node.
721    pub fn paths_with(mut self, path: impl Fn(usize) -> String + Send + Sync + 'static) -> Self {
722        self.select.set_values(Vec::new());
723        self.select.value_of = Some(Arc::new(path));
724        self
725    }
726
727    /// Collapse every node at `depth` or deeper that has children, and
728    /// expand the rest: `fold_below(1)` shows the roots' children only.
729    pub fn fold_below(mut self, depth: usize) -> Self {
730        // Parents come before their children, so one pass finds each
731        // node's depth.
732        let mut depths = vec![0usize; self.parents.len()];
733        for index in 0..self.parents.len() {
734            depths[index] = self.parents[index].map_or(0, |parent| depths[parent] + 1);
735            self.collapsed[index] = depths[index] >= depth && self.parent_of_any[index];
736        }
737        self.update();
738        self
739    }
740
741    /// Make `keys` do `action` (see [`tree_keymap`]) on this tree only.
742    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
743        self.keymap.rebind(action, keys);
744        self
745    }
746
747    /// Each node's parent, by index.
748    pub fn parents(&self) -> &[Option<usize>] {
749        &self.parents
750    }
751
752    /// The focused node's index.
753    pub fn focused(&self) -> Option<usize> {
754        self.select.focused()
755    }
756
757    /// The names on the path to node `index`, from the root.
758    pub fn path_to(&self, index: usize) -> Vec<&str> {
759        let mut path: Vec<&str> = crate::kit::ancestors(&self.parents, index)
760            .map(|ancestor| self.crumb(ancestor))
761            .collect();
762        path.reverse();
763        path.push(self.crumb(index));
764        path
765    }
766
767    /// Node `index`'s name in the breadcrumbs.
768    fn crumb(&self, index: usize) -> &str {
769        match self.crumbs.get(index) {
770            Some(crumb) => crumb,
771            None => &self.select.items()[index].label,
772        }
773    }
774
775    /// Show `status` in place of the footer until the next key.
776    pub fn set_status(&mut self, status: Option<String>) {
777        self.select.set_status(status);
778    }
779
780    /// The footer's extra key hints (default `←→ fold`).
781    pub fn set_hints(&mut self, hints: Option<String>) {
782        self.select.set_hints(hints);
783    }
784
785    /// The breadcrumbs of the focused node, at the last width seen.
786    fn crumb_line(&mut self) {
787        if !self.breadcrumbs {
788            return;
789        }
790        let path = self.select.focused().map(|index| self.path_to(index));
791        let line = breadcrumb_line(path.as_deref().unwrap_or(&[]), self.width);
792        self.select.heading = Some(line);
793    }
794
795    /// Ctrl+Y: copy the focused node's path.
796    fn copy_path(&mut self) {
797        let Some(index) = self.select.focused() else {
798            return;
799        };
800        let path = self.select.target(index).value;
801        let result = clipboard::copy(path);
802        self.select
803            .set_status(Some(clipboard::report("path", &result)));
804    }
805
806    /// Start with every node that has children collapsed (`true`) or
807    /// expanded.
808    pub fn collapsed(mut self, collapsed: bool) -> Self {
809        for (index, parent) in self.parent_of_any.iter().enumerate() {
810            self.collapsed[index] = collapsed && *parent;
811        }
812        self.update();
813        self
814    }
815
816    select_builders!();
817
818    /// Whether node `index` is collapsed.
819    pub fn is_collapsed(&self, index: usize) -> bool {
820        self.collapsed.get(index).copied().unwrap_or(false)
821    }
822
823    /// Collapse (`true`) or expand node `index`.
824    pub fn set_collapsed(&mut self, index: usize, collapsed: bool) {
825        if self.parent_of_any.get(index).copied().unwrap_or(false) {
826            self.collapsed[index] = collapsed;
827            self.update();
828        }
829    }
830
831    /// The guides and fold markers, and which nodes a collapse hides.
832    fn update(&mut self) {
833        // Parents come before their children: a node is hidden when its
834        // parent is collapsed or hidden.
835        let mut hidden = vec![false; self.parents.len()];
836        for index in 0..self.parents.len() {
837            hidden[index] =
838                self.parents[index].is_some_and(|parent| self.collapsed[parent] || hidden[parent]);
839        }
840        self.select.guides = Some(self.guides(None));
841        self.select.hidden = hidden;
842        self.select.refilter();
843        self.filtered = false;
844        self.refresh_guides();
845        self.crumb_line();
846    }
847
848    /// What the guides are drawn from. With `shown`, only the nodes listed
849    /// count: a filtered tree draws the branches between what it lists,
850    /// and a node whose children are listed is open.
851    fn guides(&self, shown: Option<&[bool]>) -> Guides {
852        let count = self.parents.len();
853        let (last, open) = match shown {
854            None => (self.last.clone(), None),
855            Some(shown) => {
856                let mut open = vec![false; count];
857                for (index, parent) in self.parents.iter().enumerate() {
858                    if let Some(parent) = parent.filter(|_| shown[index]) {
859                        open[parent] = true;
860                    }
861                }
862                (last_children(&self.parents, Some(shown)), Some(open))
863            }
864        };
865        let markers = (0..count)
866            .map(|index| {
867                let collapsed = match &open {
868                    Some(open) => !open[index],
869                    None => self.collapsed[index],
870                };
871                match (self.parent_of_any[index], collapsed) {
872                    (true, true) => Marker::Folded,
873                    (true, false) => Marker::Open,
874                    _ => Marker::Leaf,
875                }
876            })
877            .collect();
878        Guides {
879            parents: Arc::clone(&self.parents),
880            last,
881            markers,
882        }
883    }
884
885    /// Redraw the guides for what the filter lists, once a query is typed,
886    /// and back when it is cleared.
887    fn refresh_guides(&mut self) {
888        let filtering = !self.select.query_text().is_empty();
889        if !filtering && !self.filtered {
890            return;
891        }
892        let guides = if filtering {
893            let mut shown = vec![false; self.parents.len()];
894            for (index, _) in self.select.filter().matches() {
895                shown[*index] = true;
896            }
897            self.guides(Some(&shown))
898        } else {
899            self.guides(None)
900        };
901        self.select.guides = Some(guides);
902        self.filtered = filtering;
903    }
904
905    /// Right and Left: fold the focused node, or go to its parent.
906    fn fold(&mut self, key: Key) -> bool {
907        if !self.select.query_text().is_empty() || key.modifiers != Default::default() {
908            return false;
909        }
910        let Some(index) = self.select.focused() else {
911            return false;
912        };
913        match key.code {
914            KeyCode::Right if self.collapsed[index] => {
915                self.collapsed[index] = false;
916                self.update();
917            }
918            KeyCode::Left if self.parent_of_any[index] && !self.collapsed[index] => {
919                self.collapsed[index] = true;
920                self.update();
921            }
922            KeyCode::Left => {
923                if let Some(parent) = self.parents[index] {
924                    self.select.focus_item(parent);
925                }
926            }
927            KeyCode::Right => {}
928            _ => return false,
929        }
930        true
931    }
932}
933
934/// Whether each node is the last of its parent's children, in one pass
935/// from the end. With `shown`, only the nodes listed count.
936fn last_children(parents: &[Option<usize>], shown: Option<&[bool]>) -> Vec<bool> {
937    let count = parents.len();
938    let mut last = vec![true; count];
939    // Whether a later child was seen, by parent (the roots at `count`).
940    let mut seen = vec![false; count + 1];
941    for index in (0..count).rev() {
942        if shown.is_some_and(|shown| !shown[index]) {
943            continue;
944        }
945        let slot = parents[index].unwrap_or(count);
946        last[index] = !seen[slot];
947        seen[slot] = true;
948    }
949    last
950}
951
952/// A tree node's fold marker.
953#[derive(Clone, Copy, Debug, PartialEq, Eq)]
954enum Marker {
955    /// No children.
956    Leaf,
957    Folded,
958    Open,
959}
960
961/// What a [`TreeSelect`]'s guides are drawn from. Its [`Select`] draws a
962/// row's guides when it draws the row, so a large tree keeps no string per
963/// node and the work is the rows on screen times their depth.
964pub(crate) struct Guides {
965    parents: Arc<[Option<usize>]>,
966    /// Whether each node is drawn as the last of its parent's children.
967    last: Vec<bool>,
968    markers: Vec<Marker>,
969}
970
971impl Guides {
972    /// Node `index`'s guides and fold marker: `│   ├── ▸ `.
973    pub(crate) fn prefix(&self, index: usize) -> String {
974        if index >= self.parents.len() {
975            return String::new();
976        }
977        let up: Vec<usize> = crate::kit::ancestors(&self.parents, index).collect();
978        let mut guides = String::new();
979        // Each ancestor below the root draws a bar if more of its siblings
980        // follow.
981        for &ancestor in up.iter().rev().skip(1) {
982            guides.push_str(if self.last[ancestor] {
983                "    "
984            } else {
985                "│   "
986            });
987        }
988        if self.parents[index].is_some() {
989            guides.push_str(if self.last[index] {
990                "└── "
991            } else {
992                "├── "
993            });
994        }
995        guides.push_str(match self.markers[index] {
996            Marker::Folded => "▸ ",
997            Marker::Open => "▾ ",
998            Marker::Leaf => "",
999        });
1000        guides
1001    }
1002
1003    /// Node `index`'s path: the names `name` gives it and its ancestors,
1004    /// from the root, joined by `/`.
1005    pub(crate) fn path<'a>(&self, index: usize, name: impl Fn(usize) -> &'a str) -> String {
1006        let mut path: Vec<&str> = crate::kit::ancestors(&self.parents, index)
1007            .map(&name)
1008            .collect();
1009        path.reverse();
1010        path.push(name(index));
1011        path.join("/")
1012    }
1013}
1014
1015impl<T: Clone> Component for TreeSelect<T> {
1016    type Output = T;
1017
1018    fn keymap(&self) -> Keymap {
1019        let mut keymap = if self.select.menu_open() {
1020            Keymap::default()
1021        } else {
1022            self.keymap.clone()
1023        };
1024        keymap.extend(self.select.visible_keymap());
1025        keymap
1026    }
1027
1028    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<T> {
1029        self.width = context.width;
1030        let flow = match event.key() {
1031            Some(key) if !self.select.menu_open() && self.keymap.is(key, "copy-path") => {
1032                // A key clears the status first, as the list does.
1033                self.select.set_status(None);
1034                self.copy_path();
1035                Flow::Continue
1036            }
1037            Some(key) if !self.select.menu_open() && self.fold(key) => {
1038                self.select.set_status(None);
1039                Flow::Continue
1040            }
1041            _ => self.select.handle(event, context),
1042        };
1043        self.refresh_guides();
1044        self.crumb_line();
1045        flow
1046    }
1047
1048    fn render(&self, context: &Context<'_>) -> View {
1049        self.select.render(context)
1050    }
1051
1052    fn tick(&self) -> Option<Duration> {
1053        self.select.tick()
1054    }
1055
1056    fn mouse(&self) -> bool {
1057        Component::mouse(&self.select)
1058    }
1059
1060    fn default_value(&self) -> Option<T> {
1061        self.select.default_value()
1062    }
1063
1064    fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<T>, NotInteractive> {
1065        self.select.prompt(io)
1066    }
1067}
1068
1069/// The path to a node as one line, `root › parent › node`, cut from the
1070/// left (`… › parent › node`) to fit `width`.
1071///
1072/// A private stand-in: once the shared breadcrumbs component lands
1073/// (0.0.14 workstream 2, #482), the tree and the explorers draw theirs
1074/// with it instead.
1075fn breadcrumb_line(path: &[&str], width: usize) -> Vec<Segment> {
1076    let theme = Theme::default();
1077    let separator = " › ";
1078    let room = width.saturating_sub(2);
1079    let mut first = 0;
1080    let total = |from: usize| -> usize {
1081        let names: usize = path[from..].iter().map(|name| cell_len(name)).sum();
1082        let gaps = path.len().saturating_sub(from + 1) * cell_len(separator);
1083        names
1084            + gaps
1085            + if from > 0 {
1086                cell_len("…") + cell_len(separator)
1087            } else {
1088                0
1089            }
1090    };
1091    while first + 1 < path.len() && total(first) > room {
1092        first += 1;
1093    }
1094    let mut line = Vec::new();
1095    if first > 0 {
1096        line.push(text("…", &theme.hint));
1097        line.push(text(separator, &theme.hint));
1098    }
1099    for (offset, name) in path[first..].iter().enumerate() {
1100        if offset > 0 {
1101            line.push(text(separator, &theme.hint));
1102        }
1103        let last = first + offset + 1 == path.len();
1104        line.push(if last {
1105            text(*name, &theme.focused)
1106        } else {
1107            text(*name, &theme.hint)
1108        });
1109    }
1110    fit(line, room)
1111}