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>,
pub footer_key: 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"),
footer_key: None,
},
HelpEntry {
key: "Tab",
desc: "Next mode",
indent: false,
todo_only: false,
requires_claude: false,
footer: Some("Mode"),
footer_key: Some("Tab/S-Tab"),
},
HelpEntry {
key: "S-Tab",
desc: "Previous mode",
indent: false,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "x",
desc: "Complete selected todo",
indent: false,
todo_only: true,
requires_claude: false,
footer: Some("Done"),
footer_key: None,
},
HelpEntry {
key: "o",
desc: "Open URLs in selected todo",
indent: false,
todo_only: false,
requires_claude: false,
footer: Some("URL"),
footer_key: None,
},
HelpEntry {
key: "s",
desc: "Send to... submenu",
indent: false,
todo_only: false,
requires_claude: false,
footer: Some("Send"),
footer_key: None,
},
HelpEntry {
key: "st",
desc: "Send to todo.txt",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "sr",
desc: "Send to ref.txt",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "si",
desc: "Send to inbox.txt",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "ss",
desc: "Send to someday.txt",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "sw",
desc: "Send to waiting.txt",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "p",
desc: "Set/clear priority submenu",
indent: false,
todo_only: false,
requires_claude: false,
footer: Some("Priority"),
footer_key: None,
},
HelpEntry {
key: "pa",
desc: "Set priority (A)",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "pb",
desc: "Set priority (B)",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "pc",
desc: "Set priority (C)",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "pd",
desc: "Set priority (D)",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "pe",
desc: "Set priority (E)",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "px",
desc: "Clear priority",
indent: true,
todo_only: false,
requires_claude: false,
footer: None,
footer_key: None,
},
HelpEntry {
key: "c",
desc: "Claude submenu (requires crmux or claude CLI)",
indent: false,
todo_only: true,
requires_claude: true,
footer: Some("Claude"),
footer_key: None,
},
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,
footer_key: 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,
footer_key: None,
},
HelpEntry {
key: "cgp",
desc: "Get plans and import via crmux (>= 0.11.0)",
indent: true,
todo_only: true,
requires_claude: true,
footer: None,
footer_key: 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,
footer_key: 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,
footer_key: None,
},
HelpEntry {
key: "?",
desc: "Toggle help",
indent: false,
todo_only: false,
requires_claude: false,
footer: Some("Help"),
footer_key: None,
},
HelpEntry {
key: "q",
desc: "Quit",
indent: false,
todo_only: false,
requires_claude: false,
footer: Some("Quit"),
footer_key: None,
},
];
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.footer_key.unwrap_or(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
);
}
}
#[test]
fn test_footer_entries_contains_short_labels() {
let entries = footer_entries(true, true);
assert!(entries.contains(&("hjkl", "Nav")));
assert!(entries.contains(&("x", "Done")));
assert!(entries.contains(&("o", "URL")));
assert!(entries.contains(&("s", "Send")));
assert!(entries.contains(&("Tab/S-Tab", "Mode")));
assert!(entries.contains(&("c", "Claude")));
assert!(entries.contains(&("?", "Help")));
assert!(entries.contains(&("q", "Quit")));
}
#[test]
fn test_footer_entries_excludes_stab_as_separate_entry() {
let entries = footer_entries(true, true);
assert!(!entries.iter().any(|(k, _)| *k == "S-Tab"));
assert!(!entries.iter().any(|(k, _)| *k == "Tab"));
}
#[test]
fn test_help_entries_real_keys_preserved() {
assert!(HELP_ENTRIES.iter().any(|e| e.key == "Tab"));
assert!(HELP_ENTRIES.iter().any(|e| e.key == "S-Tab"));
}
#[test]
fn test_footer_entries_tab_mode_placed_right_after_hjkl() {
let entries = footer_entries(true, true);
let keys: Vec<&str> = entries.iter().map(|(k, _)| *k).collect();
let hjkl_pos = keys.iter().position(|k| *k == "hjkl").unwrap();
let tab_pos = keys.iter().position(|k| *k == "Tab/S-Tab").unwrap();
assert_eq!(
tab_pos,
hjkl_pos + 1,
"Tab/S-Tab should come right after hjkl, got keys: {keys:?}"
);
}
}