Skip to main content

tix/app/
library.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4impl App {
5    /// One row per record in `tab`, in registry order.
6    ///
7    /// The Views tab reads the same [`Self::view_rows`] the `v` modal does — one
8    /// interpretation of a view, so the page cannot disagree with the modal about
9    /// a name, a scope or the active marker.
10    pub fn library_entries(
11        &self,
12        tab: crate::ui::library::LibraryTab,
13    ) -> Vec<crate::ui::library::LibraryEntry> {
14        use crate::ui::library::{LibraryEntry, LibraryTab};
15        match tab {
16            LibraryTab::Views => self
17                .view_rows()
18                .into_iter()
19                .map(|row| LibraryEntry {
20                    title: row.name,
21                    subtitle: row.scope,
22                    active: row.active,
23                })
24                .collect(),
25            LibraryTab::Templates => self
26                .store
27                .config
28                .issue_templates
29                .iter()
30                .map(|(id, template)| LibraryEntry {
31                    title: id.to_string(),
32                    subtitle: template_subtitle(template),
33                    active: false,
34                })
35                .collect(),
36        }
37    }
38
39    /// The selected record's detail pane: the facts the record actually holds,
40    /// read from the registry rather than re-derived from a rendering.
41    pub fn library_detail(
42        &self,
43        tab: crate::ui::library::LibraryTab,
44        index: usize,
45    ) -> Option<crate::ui::library::LibraryDetail> {
46        use crate::ui::library::{LibraryDetail, LibraryTab};
47        match tab {
48            LibraryTab::Views => {
49                let view = self.store.config.views.iter().nth(index)?;
50                let projects = Self::view_projects();
51                let resolution = view.scope.resolution(&projects);
52                let mut lines = vec![
53                    ("name".to_string(), view.name.clone()),
54                    ("id".to_string(), view.id.as_str().to_string()),
55                    (
56                        "scope".to_string(),
57                        crate::ui::views::scope_summary(&view.scope, resolution),
58                    ),
59                    (
60                        "active on the board".to_string(),
61                        if self.active_view_id() == Some(&view.id) {
62                            "yes".to_string()
63                        } else {
64                            "no".to_string()
65                        },
66                    ),
67                ];
68                lines.extend(plan_lines(&view.plan));
69                Some(LibraryDetail {
70                    heading: "View".to_string(),
71                    lines,
72                })
73            }
74            LibraryTab::Templates => {
75                let (id, template) = self.store.config.issue_templates.iter().nth(index)?;
76                let mut lines = vec![
77                    ("id".to_string(), id.to_string()),
78                    (
79                        "name".to_string(),
80                        template
81                            .name
82                            .clone()
83                            .unwrap_or_else(|| "(unnamed)".to_string()),
84                    ),
85                    ("scope".to_string(), template.scope.to_string()),
86                ];
87                for (key, value) in template_default_lines(&template.defaults) {
88                    lines.push((format!("default {key}"), value));
89                }
90                for prompt in &template.prompts {
91                    lines.push((
92                        format!("prompt {}", prompt.key),
93                        format!(
94                            "{}{}",
95                            prompt.label.clone().unwrap_or_else(|| prompt.key.clone()),
96                            if prompt.required { " (required)" } else { "" }
97                        ),
98                    ));
99                }
100                for reference in &template.references {
101                    lines.push((
102                        format!("reference {:?}", reference.kind),
103                        format!(
104                            "{}{}",
105                            reference.value,
106                            if reference.required {
107                                " (required)"
108                            } else {
109                                ""
110                            }
111                        ),
112                    ));
113                }
114                Some(LibraryDetail {
115                    heading: "Template".to_string(),
116                    lines,
117                })
118            }
119        }
120    }
121
122    /// The stable id of the view under the Library cursor, `None` on the
123    /// Templates tab or with nothing selected.
124    pub(in crate::app) fn library_selected_view_id(
125        &self,
126    ) -> Option<crate::views::registry::ViewId> {
127        use crate::ui::library::LibraryTab;
128        let state = self.library.as_ref()?;
129        if state.tab != LibraryTab::Views {
130            return None;
131        }
132        self.store
133            .config
134            .views
135            .iter()
136            .nth(state.selected)
137            .map(|view| view.id.clone())
138    }
139
140    /// Switch the Library page's tab, resetting the cursor to the first row: the
141    /// two registries hold different records, so a carried-over index would point
142    /// at a row the user never chose.
143    pub(in crate::app) fn library_switch_tab(&mut self, forward: bool) {
144        let Some(state) = self.library.as_mut() else {
145            return;
146        };
147        state.tab = if forward {
148            state.tab.next()
149        } else {
150            state.tab.prev()
151        };
152        state.selected = 0;
153    }
154
155    /// The F11 Library page's keys (tix #1593).
156    ///
157    /// `Tab`/`Shift+Tab` (and `h`/`l`) switch registry, `j`/`k` and the arrows
158    /// walk rows, `Enter` applies the selected view — the page's single mutating
159    /// action, and read-only by C-005 — and `q`/`esc` return to the board.
160    ///
161    /// The cursor is clamped to the registry on every key, because the registry
162    /// can change under the page: `tix view delete` in another terminal, or a
163    /// reload that drops a record this session did not touch.
164    pub(in crate::app) fn handle_library_key(&mut self, key: KeyEvent) -> Result<Action> {
165        use crate::ui::library::LibraryTab;
166        if !matches!(self.mode, Mode::Library) {
167            return Ok(Action::None);
168        }
169        let tab = self.library_state().tab;
170        let rows = self.library_entries(tab).len();
171        if let Some(state) = self.library.as_mut() {
172            state.clamp_selected(rows);
173        }
174        match key.code {
175            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
176                // The field's contract is "`Some` exactly while `mode` is
177                // `Mode::Library`", so the page's own close clears it rather than
178                // leaving it for the next mode switch.
179                self.library = None;
180                self.mode = Mode::Board;
181            }
182            // The configured `next`/`previous` keys canonicalize to Tab/BackTab in
183            // this context (see `KeyBindings::canonical_event`), so the handler
184            // reads the canonical event and not the letter.
185            KeyCode::Tab => self.library_switch_tab(true),
186            KeyCode::BackTab => self.library_switch_tab(false),
187            KeyCode::Up | KeyCode::Char('k') => {
188                if let Some(state) = self.library.as_mut() {
189                    state.move_selected(-1, rows);
190                }
191            }
192            KeyCode::Down | KeyCode::Char('j') => {
193                if let Some(state) = self.library.as_mut() {
194                    state.move_selected(1, rows);
195                }
196            }
197            KeyCode::Enter => match self.library_selected_view_id() {
198                // The page's one mutation: applying a view is read-only on the
199                // issue and the configuration (C-005), so it is the reason `Enter`
200                // is bound here at all.
201                Some(id) => return self.apply_view(id.as_str()),
202                None => {
203                    self.status_msg = Some(match tab {
204                        LibraryTab::Views => "No view selected".to_string(),
205                        LibraryTab::Templates => "Templates are read here; create one with \
206                             `tix create --template <ID>`"
207                            .to_string(),
208                    });
209                }
210            },
211            KeyCode::Char('?') => self.open_help(Mode::Library),
212            _ => {}
213        }
214        Ok(Action::None)
215    }
216}