tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

impl App {
    /// One row per record in `tab`, in registry order.
    ///
    /// The Views tab reads the same [`Self::view_rows`] the `v` modal does — one
    /// interpretation of a view, so the page cannot disagree with the modal about
    /// a name, a scope or the active marker.
    pub fn library_entries(
        &self,
        tab: crate::ui::library::LibraryTab,
    ) -> Vec<crate::ui::library::LibraryEntry> {
        use crate::ui::library::{LibraryEntry, LibraryTab};
        match tab {
            LibraryTab::Views => self
                .view_rows()
                .into_iter()
                .map(|row| LibraryEntry {
                    title: row.name,
                    subtitle: row.scope,
                    active: row.active,
                })
                .collect(),
            LibraryTab::Templates => self
                .store
                .config
                .issue_templates
                .iter()
                .map(|(id, template)| LibraryEntry {
                    title: id.to_string(),
                    subtitle: template_subtitle(template),
                    active: false,
                })
                .collect(),
        }
    }

    /// The selected record's detail pane: the facts the record actually holds,
    /// read from the registry rather than re-derived from a rendering.
    pub fn library_detail(
        &self,
        tab: crate::ui::library::LibraryTab,
        index: usize,
    ) -> Option<crate::ui::library::LibraryDetail> {
        use crate::ui::library::{LibraryDetail, LibraryTab};
        match tab {
            LibraryTab::Views => {
                let view = self.store.config.views.iter().nth(index)?;
                let projects = Self::view_projects();
                let resolution = view.scope.resolution(&projects);
                let mut lines = vec![
                    ("name".to_string(), view.name.clone()),
                    ("id".to_string(), view.id.as_str().to_string()),
                    (
                        "scope".to_string(),
                        crate::ui::views::scope_summary(&view.scope, resolution),
                    ),
                    (
                        "active on the board".to_string(),
                        if self.active_view_id() == Some(&view.id) {
                            "yes".to_string()
                        } else {
                            "no".to_string()
                        },
                    ),
                ];
                lines.extend(plan_lines(&view.plan));
                Some(LibraryDetail {
                    heading: "View".to_string(),
                    lines,
                })
            }
            LibraryTab::Templates => {
                let (id, template) = self.store.config.issue_templates.iter().nth(index)?;
                let mut lines = vec![
                    ("id".to_string(), id.to_string()),
                    (
                        "name".to_string(),
                        template
                            .name
                            .clone()
                            .unwrap_or_else(|| "(unnamed)".to_string()),
                    ),
                    ("scope".to_string(), template.scope.to_string()),
                ];
                for (key, value) in template_default_lines(&template.defaults) {
                    lines.push((format!("default {key}"), value));
                }
                for prompt in &template.prompts {
                    lines.push((
                        format!("prompt {}", prompt.key),
                        format!(
                            "{}{}",
                            prompt.label.clone().unwrap_or_else(|| prompt.key.clone()),
                            if prompt.required { " (required)" } else { "" }
                        ),
                    ));
                }
                for reference in &template.references {
                    lines.push((
                        format!("reference {:?}", reference.kind),
                        format!(
                            "{}{}",
                            reference.value,
                            if reference.required {
                                " (required)"
                            } else {
                                ""
                            }
                        ),
                    ));
                }
                Some(LibraryDetail {
                    heading: "Template".to_string(),
                    lines,
                })
            }
        }
    }

    /// The stable id of the view under the Library cursor, `None` on the
    /// Templates tab or with nothing selected.
    pub(in crate::app) fn library_selected_view_id(
        &self,
    ) -> Option<crate::views::registry::ViewId> {
        use crate::ui::library::LibraryTab;
        let state = self.library.as_ref()?;
        if state.tab != LibraryTab::Views {
            return None;
        }
        self.store
            .config
            .views
            .iter()
            .nth(state.selected)
            .map(|view| view.id.clone())
    }

    /// Switch the Library page's tab, resetting the cursor to the first row: the
    /// two registries hold different records, so a carried-over index would point
    /// at a row the user never chose.
    pub(in crate::app) fn library_switch_tab(&mut self, forward: bool) {
        let Some(state) = self.library.as_mut() else {
            return;
        };
        state.tab = if forward {
            state.tab.next()
        } else {
            state.tab.prev()
        };
        state.selected = 0;
    }

    /// The F11 Library page's keys (tix #1593).
    ///
    /// `Tab`/`Shift+Tab` (and `h`/`l`) switch registry, `j`/`k` and the arrows
    /// walk rows, `Enter` applies the selected view — the page's single mutating
    /// action, and read-only by C-005 — and `q`/`esc` return to the board.
    ///
    /// The cursor is clamped to the registry on every key, because the registry
    /// can change under the page: `tix view delete` in another terminal, or a
    /// reload that drops a record this session did not touch.
    pub(in crate::app) fn handle_library_key(&mut self, key: KeyEvent) -> Result<Action> {
        use crate::ui::library::LibraryTab;
        if !matches!(self.mode, Mode::Library) {
            return Ok(Action::None);
        }
        let tab = self.library_state().tab;
        let rows = self.library_entries(tab).len();
        if let Some(state) = self.library.as_mut() {
            state.clamp_selected(rows);
        }
        match key.code {
            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
                // The field's contract is "`Some` exactly while `mode` is
                // `Mode::Library`", so the page's own close clears it rather than
                // leaving it for the next mode switch.
                self.library = None;
                self.mode = Mode::Board;
            }
            // The configured `next`/`previous` keys canonicalize to Tab/BackTab in
            // this context (see `KeyBindings::canonical_event`), so the handler
            // reads the canonical event and not the letter.
            KeyCode::Tab => self.library_switch_tab(true),
            KeyCode::BackTab => self.library_switch_tab(false),
            KeyCode::Up | KeyCode::Char('k') => {
                if let Some(state) = self.library.as_mut() {
                    state.move_selected(-1, rows);
                }
            }
            KeyCode::Down | KeyCode::Char('j') => {
                if let Some(state) = self.library.as_mut() {
                    state.move_selected(1, rows);
                }
            }
            KeyCode::Enter => match self.library_selected_view_id() {
                // The page's one mutation: applying a view is read-only on the
                // issue and the configuration (C-005), so it is the reason `Enter`
                // is bound here at all.
                Some(id) => return self.apply_view(id.as_str()),
                None => {
                    self.status_msg = Some(match tab {
                        LibraryTab::Views => "No view selected".to_string(),
                        LibraryTab::Templates => "Templates are read here; create one with \
                             `tix create --template <ID>`"
                            .to_string(),
                    });
                }
            },
            KeyCode::Char('?') => self.open_help(Mode::Library),
            _ => {}
        }
        Ok(Action::None)
    }
}