torudo 0.10.0

A terminal-based todo.txt viewer and manager with TUI interface
pub struct HelpEntry {
    pub key: &'static str,
    pub desc: &'static str,
    pub indent: bool,
    pub todo_only: bool,
    pub requires_claude: bool,
    pub footer: Option<&'static str>,
}

impl HelpEntry {
    const fn is_visible(&self, is_todo: bool, has_claude: bool) -> bool {
        (!self.todo_only || is_todo) && (!self.requires_claude || has_claude)
    }
}

pub const HELP_ENTRIES: &[HelpEntry] = &[
    HelpEntry { key: "hjkl", desc: "Navigate between columns and todos", indent: false, todo_only: false, requires_claude: false, footer: Some("Nav") },
    HelpEntry { key: "x", desc: "Complete selected todo", indent: false, todo_only: true, requires_claude: false, footer: Some("Complete") },
    HelpEntry { key: "o", desc: "Open URLs in selected todo", indent: false, todo_only: false, requires_claude: false, footer: Some("Open URL") },
    HelpEntry { key: "r", desc: "Move selected todo to ref.txt", indent: false, todo_only: true, requires_claude: false, footer: Some("Ref") },
    HelpEntry { key: "m", desc: "Mode submenu", indent: false, todo_only: false, requires_claude: false, footer: Some("Mode") },
    HelpEntry { key: "mt", desc: "Switch to todo mode", indent: true, todo_only: false, requires_claude: false, footer: None },
    HelpEntry { key: "mr", desc: "Switch to ref mode", indent: true, todo_only: false, requires_claude: false, footer: None },
    HelpEntry { key: "c", desc: "Claude submenu (requires crmux or claude CLI)", indent: false, todo_only: true, requires_claude: true, footer: Some("Claude") },
    HelpEntry { key: "csp", desc: "Send plan prompt to project's idle crmux session (>= 0.10.0)", indent: true, todo_only: true, requires_claude: true, footer: None },
    HelpEntry { key: "csi", desc: "Send implement prompt to project's idle crmux session (>= 0.10.0)", indent: true, todo_only: true, requires_claude: true, footer: None },
    HelpEntry { key: "cgp", desc: "Get plans and import via crmux (>= 0.11.0)", indent: true, todo_only: true, requires_claude: true, footer: None },
    HelpEntry { key: "clp", desc: "Launch claude plan in tmux window (requires cwd in frontmatter)", indent: true, todo_only: true, requires_claude: true, footer: None },
    HelpEntry { key: "cli", desc: "Launch claude implement in tmux window (requires cwd in frontmatter)", indent: true, todo_only: true, requires_claude: true, footer: None },
    HelpEntry { key: "?", desc: "Toggle help", indent: false, todo_only: false, requires_claude: false, footer: Some("Help") },
    HelpEntry { key: "q", desc: "Quit", indent: false, todo_only: false, requires_claude: false, footer: Some("Quit") },
];

pub fn visible_entries(is_todo: bool, has_claude: bool) -> Vec<&'static HelpEntry> {
    HELP_ENTRIES
        .iter()
        .filter(|e| e.is_visible(is_todo, has_claude))
        .collect()
}

pub fn footer_entries(is_todo: bool, has_claude: bool) -> Vec<(&'static str, &'static str)> {
    HELP_ENTRIES
        .iter()
        .filter(|e| e.footer.is_some() && e.is_visible(is_todo, has_claude))
        .map(|e| (e.key, e.footer.unwrap()))
        .collect()
}

pub fn cli_help_text() -> String {
    let max_key_width = HELP_ENTRIES
        .iter()
        .map(|e| e.key.len() + if e.indent { 2 } else { 0 })
        .max()
        .unwrap_or(0);

    let lines: Vec<String> = HELP_ENTRIES
        .iter()
        .map(|e| {
            let prefix = if e.indent { "  " } else { "" };
            let padded_key = format!("{prefix}{}", e.key);
            format!("  {padded_key:<max_key_width$}  {}", e.desc)
        })
        .collect();

    format!("Keyboard Controls:\n{}", lines.join("\n"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cli_help_text_contains_all_keys() {
        let text = cli_help_text();
        for entry in HELP_ENTRIES {
            assert!(
                text.contains(entry.key),
                "Help text should contain key: {}",
                entry.key
            );
        }
    }
}