qqq-cli 0.11.0

Local-first task queue for coding agent sessions
use super::render::{PopupKind, PopupRow};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use unicode_width::UnicodeWidthStr;

const TEMPLATES: [(&str, &str); 6] = [
    ("Unarchived tasks", "not archived"),
    ("Active tasks", "not archived and status ~= \"completed\""),
    ("By tag", "not archived and has_tag(\"backend\")"),
    ("High priority", "not archived and priority >= 5"),
    (
        "Recent tasks",
        "not archived\nand datetime(created_time) >= datetime(\"now\", \"-7 days\")",
    ),
    ("All tasks", "true"),
];

pub(super) enum Action {
    Use(&'static str),
    Cancel,
}

pub(super) struct Picker {
    selected: usize,
}

impl Picker {
    pub fn new() -> Self {
        Self { selected: 0 }
    }

    pub fn key(&mut self, key: KeyEvent) -> Option<Action> {
        let key = super::escape_alias(key);
        if key.code == KeyCode::Esc
            || (key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c'))
        {
            return Some(Action::Cancel);
        }
        if key
            .modifiers
            .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER)
        {
            return None;
        }
        match key.code {
            KeyCode::Up => self.selected = self.selected.saturating_sub(1),
            KeyCode::Down => self.selected = (self.selected + 1).min(TEMPLATES.len() - 1),
            KeyCode::Home => self.selected = 0,
            KeyCode::End => self.selected = TEMPLATES.len() - 1,
            KeyCode::Enter => return Some(Action::Use(TEMPLATES[self.selected].1)),
            _ => (),
        }
        None
    }

    pub fn rows(&self, width: usize, height: usize) -> Vec<PopupRow> {
        if height == 0 {
            return Vec::new();
        }
        let heading = usize::from(height >= 3);
        let footer = usize::from(height >= 2);
        let available = height - heading - footer;
        let visible = (available / 2).max(1).min(TEMPLATES.len());
        let start = self
            .selected
            .saturating_sub(visible - 1)
            .min(TEMPLATES.len() - visible);
        let mut rows = Vec::new();
        if heading > 0 {
            rows.push(PopupRow::new("View templates", PopupKind::Heading));
        }
        rows.extend(TEMPLATES.iter().enumerate().skip(start).take(visible).map(
            |(index, (name, _))| {
                PopupRow::new(
                    *name,
                    if index == self.selected {
                        PopupKind::SelectedChoice
                    } else {
                        PopupKind::Choice
                    },
                )
            },
        ));
        let preview_height = height.saturating_sub(rows.len() + footer);
        rows.extend(
            TEMPLATES[self.selected]
                .1
                .lines()
                .flat_map(|line| super::wrap_modal(line, width))
                .take(preview_height)
                .map(|line| PopupRow::new(line, PopupKind::Body)),
        );
        if footer > 0 {
            let hint = ["↑↓ Pick Enter Use Esc Back", "↑↓ Enter Esc", "↑↓ ↵ Esc"]
                .into_iter()
                .find(|text| text.width() <= width)
                .unwrap_or("↑↓ ↵ Esc");
            rows.push(PopupRow::new(hint, PopupKind::Hint));
        }
        rows
    }
}