Skip to main content

ite_cli/
tree.rs

1//! Source-neutral tree data consumed by the application and renderer.
2
3use std::ffi::OsString;
4
5use tui_treelistview::{TreeChildren, TreeModel, TreeRevision};
6
7pub type NodeId = usize;
8
9/// Values used when the focused node is accepted or passed to a shell binding.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ActionValues {
12    /// Text written to stdout when the node is accepted.
13    pub output: OsString,
14    /// Text written to stdout by the alternate accept action.
15    pub alternate_output: OsString,
16    /// Value exported to shell bindings as `$path`.
17    pub path: OsString,
18    /// Value exported to shell bindings as `$relpath`.
19    pub relpath: OsString,
20}
21
22impl ActionValues {
23    pub fn new(
24        output: impl Into<OsString>,
25        path: impl Into<OsString>,
26        relpath: impl Into<OsString>,
27    ) -> Self {
28        let output = output.into();
29        Self {
30            alternate_output: output.clone(),
31            output,
32            path: path.into(),
33            relpath: relpath.into(),
34        }
35    }
36
37    pub fn with_alternate_output(mut self, output: impl Into<OsString>) -> Self {
38        self.alternate_output = output.into();
39        self
40    }
41}
42
43#[derive(Debug)]
44pub struct Node {
45    pub name: String,
46    /// Optional secondary text rendered after the name.
47    pub detail: Option<String>,
48    pub parent: Option<NodeId>,
49    pub children: Vec<NodeId>,
50    /// Whether this node represents a container, including an empty one.
51    pub is_container: bool,
52    /// 0 for roots.
53    pub depth: usize,
54    pub action: ActionValues,
55}
56
57#[derive(Debug, Default)]
58pub struct Tree {
59    pub(crate) nodes: Vec<Node>,
60    pub(crate) roots: Vec<NodeId>,
61    view_root: Option<NodeId>,
62    revision: TreeRevision,
63}
64
65impl Tree {
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    pub fn push(
71        &mut self,
72        parent: Option<NodeId>,
73        name: impl Into<String>,
74        is_container: bool,
75        action: ActionValues,
76    ) -> NodeId {
77        self.push_with_detail(parent, name, None, is_container, action)
78    }
79
80    pub fn push_with_detail(
81        &mut self,
82        parent: Option<NodeId>,
83        name: impl Into<String>,
84        detail: Option<String>,
85        is_container: bool,
86        action: ActionValues,
87    ) -> NodeId {
88        let id = self.nodes.len();
89        let depth = parent.map_or(0, |id| self.nodes[id].depth + 1);
90        self.nodes.push(Node {
91            name: name.into(),
92            detail,
93            parent,
94            children: Vec::new(),
95            is_container,
96            depth,
97            action,
98        });
99        match parent {
100            Some(parent) => self.nodes[parent].children.push(id),
101            None => self.roots.push(id),
102        }
103        id
104    }
105
106    pub fn node(&self, id: NodeId) -> &Node {
107        &self.nodes[id]
108    }
109
110    pub fn len(&self) -> usize {
111        self.nodes.len()
112    }
113
114    pub fn is_empty(&self) -> bool {
115        self.nodes.is_empty()
116    }
117
118    pub fn root_ids(&self) -> &[NodeId] {
119        &self.roots
120    }
121
122    /// The temporary root used by the tree view, if it has been narrowed.
123    pub(crate) fn view_root(&self) -> Option<NodeId> {
124        self.view_root
125    }
126
127    /// Narrow the tree model to one subtree, or restore the original forest.
128    pub(crate) fn set_view_root(&mut self, root: Option<NodeId>) {
129        if self.view_root != root {
130            self.view_root = root;
131            self.revision.advance();
132        }
133    }
134
135    /// The node's parent in the current view. A temporary root has no parent.
136    pub(crate) fn view_parent(&self, id: NodeId) -> Option<NodeId> {
137        if self.view_root == Some(id) {
138            None
139        } else {
140            self.nodes[id].parent
141        }
142    }
143
144    /// Whether a node belongs to the subtree exposed by the current view.
145    pub(crate) fn is_in_view(&self, id: NodeId) -> bool {
146        let Some(root) = self.view_root else {
147            return true;
148        };
149        let mut cursor = Some(id);
150        while let Some(current) = cursor {
151            if current == root {
152                return true;
153            }
154            cursor = self.nodes[current].parent;
155        }
156        false
157    }
158
159    /// True when the node cannot be expanded.
160    pub fn is_leaf(&self, id: NodeId) -> bool {
161        self.nodes[id].children.is_empty()
162    }
163
164    /// All expandable nodes as `(id, parent)` pairs, in tree order.
165    pub fn branches(&self) -> impl Iterator<Item = (NodeId, Option<NodeId>)> + '_ {
166        self.nodes
167            .iter()
168            .enumerate()
169            .filter(|(id, _)| !self.is_leaf(*id))
170            .map(|(id, node)| (id, node.parent))
171    }
172}
173
174impl TreeModel for Tree {
175    type Id = NodeId;
176
177    fn roots(&self) -> impl Iterator<Item = NodeId> + '_ {
178        self.roots
179            .iter()
180            .copied()
181            .filter(|_| self.view_root.is_none())
182            .chain(self.view_root)
183    }
184
185    fn children(&self, id: NodeId) -> TreeChildren<'_, NodeId> {
186        TreeChildren::loaded(&self.nodes[id].children)
187    }
188
189    fn revision(&self) -> TreeRevision {
190        self.revision
191    }
192
193    fn size_hint(&self) -> usize {
194        self.nodes.len()
195    }
196}