Skip to main content

gn_cli/commands/
shortcuts.rs

1use anyhow::Result;
2use clap::Args;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Args, Debug)]
9pub struct ShortcutsArgs {
10    /// Show all quickies, CLI abbreviations, and keyboard shortcuts
11    #[arg(short, long)]
12    pub list: bool,
13
14    /// Bind or customize a shortcut (format: KEY=COMMAND, e.g. "c=add -m")
15    #[arg(short, long)]
16    pub set: Option<String>,
17
18    /// Reset shortcuts and abbreviations to default configuration
19    #[arg(long)]
20    pub reset: bool,
21}
22
23#[derive(Serialize, Deserialize, Debug, Clone)]
24pub struct ShortcutConfig {
25    pub cli_aliases: BTreeMap<String, String>,
26    pub tui_keybindings: BTreeMap<String, String>,
27    pub custom_user_shortcuts: BTreeMap<String, String>,
28}
29
30impl Default for ShortcutConfig {
31    fn default() -> Self {
32        let mut cli_aliases = BTreeMap::new();
33        cli_aliases.insert("gn a".into(), "Add a new note to file and line range [alias: add]".into());
34        cli_aliases.insert("gn r".into(), "Reply to an existing note thread by ID or number [alias: reply]".into());
35        cli_aliases.insert("gn l / gn ls".into(), "List notes with numbered quick-indexes [alias: list]".into());
36        cli_aliases.insert("gn s".into(), "Interactive arrow-key note thread picker [alias: show]".into());
37        cli_aliases.insert("gn ok / gn close".into(), "Resolve or approve a note by index # or ID [alias: resolve]".into());
38        cli_aliases.insert("gn d".into(), "View git diff with inline notes attached to hunks [alias: diff]".into());
39        cli_aliases.insert("gn b".into(), "View git blame with inline notes annotations [alias: blame]".into());
40        cli_aliases.insert("gn push / gn pull".into(), "Sync notes bidirectionally with remote refs/notes/* [alias: sync]".into());
41        cli_aliases.insert("gn sum".into(), "AI summary of open discussion threads via Gemini [alias: summarize]".into());
42        cli_aliases.insert("gn doc".into(), "Check repo health, refspecs, and stale notes [alias: doctor]".into());
43        cli_aliases.insert("gn i".into(), "1-second setup: configure fetch refspec & auto-sync hooks [alias: init]".into());
44
45        let mut tui_keybindings = BTreeMap::new();
46        tui_keybindings.insert("↑ / ↓ or k / j".into(), "Navigate files and notes list".into());
47        tui_keybindings.insert("Enter / →".into(), "Expand file / focus diff & note thread panel".into());
48        tui_keybindings.insert("Esc / ←".into(), "Navigate back to previous pane".into());
49        tui_keybindings.insert("r".into(), "Open quick reply bar for highlighted thread".into());
50        tui_keybindings.insert("a / ok".into(), "Mark highlighted note as Approved / Resolved".into());
51        tui_keybindings.insert("q".into(), "Quit git-notes TUI".into());
52
53        let custom_user_shortcuts = BTreeMap::new();
54
55        Self {
56            cli_aliases,
57            tui_keybindings,
58            custom_user_shortcuts,
59        }
60    }
61}
62
63impl ShortcutConfig {
64    fn config_path() -> Option<PathBuf> {
65        dirs_fallback().map(|p| p.join("shortcuts.json"))
66    }
67
68    pub fn load() -> Self {
69        Self::config_path()
70            .and_then(|p| fs::read_to_string(p).ok())
71            .and_then(|s| serde_json::from_str(&s).ok())
72            .unwrap_or_default()
73    }
74
75    pub fn save(&self) -> Result<()> {
76        if let Some(path) = Self::config_path() {
77            if let Some(parent) = path.parent() {
78                let _ = fs::create_dir_all(parent);
79            }
80            let data = serde_json::to_string_pretty(self)?;
81            fs::write(path, data)?;
82        }
83        Ok(())
84    }
85}
86
87pub fn run(args: &ShortcutsArgs) -> Result<()> {
88    let mut config = ShortcutConfig::load();
89
90    if args.reset {
91        config = ShortcutConfig::default();
92        config.save()?;
93        println!("\x1b[32m✓ Reset all shortcuts and quickies to default configuration.\x1b[0m");
94        return Ok(());
95    }
96
97    if let Some(ref set_binding) = args.set {
98        if let Some((key, val)) = set_binding.split_once('=') {
99            config
100                .custom_user_shortcuts
101                .insert(key.trim().to_string(), val.trim().to_string());
102            config.save()?;
103            println!(
104                "\x1b[32m✓ Saved custom shortcut:\x1b[0m \x1b[36m{}\x1b[0m ➜ \x1b[1m{}\x1b[0m",
105                key.trim(),
106                val.trim()
107            );
108            return Ok(());
109        } else {
110            eprintln!("\x1b[31m✗ Invalid format. Use --set KEY=COMMAND (e.g., gn shortcuts --set c=\"add -m\")\x1b[0m");
111            return Ok(());
112        }
113    }
114
115    println!("\n\x1b[1;36m⚡ git-notes Quickies & Keyboard Shortcuts Cheat Sheet\x1b[0m\n");
116
117    println!("\x1b[1;33mCLI Quickies & Abbreviations (gn):\x1b[0m");
118    println!("┌───────────────────────┬────────────────────────────────────────────────────────────────┐");
119    println!("│ Quickie / Command     │ Action Description                                             │");
120    println!("├───────────────────────┼────────────────────────────────────────────────────────────────┤");
121    for (k, v) in &config.cli_aliases {
122        println!("│ \x1b[36m{:<21}\x1b[0m │ {:<62} │", k, v);
123    }
124    println!("└───────────────────────┴────────────────────────────────────────────────────────────────┘");
125
126    println!("\n\x1b[1;33mTUI & Terminal Keybindings:\x1b[0m");
127    println!("┌───────────────────────┬────────────────────────────────────────────────────────────────┐");
128    println!("│ Key / Keystroke       │ TUI Navigation & Review Action                                 │");
129    println!("├───────────────────────┼────────────────────────────────────────────────────────────────┤");
130    for (k, v) in &config.tui_keybindings {
131        println!("│ \x1b[35m{:<21}\x1b[0m │ {:<62} │", k, v);
132    }
133    println!("└───────────────────────┴────────────────────────────────────────────────────────────────┘");
134
135    if !config.custom_user_shortcuts.is_empty() {
136        println!("\n\x1b[1;33mCustom User Shortcuts (~/.git-notes/shortcuts.json):\x1b[0m");
137        for (k, v) in &config.custom_user_shortcuts {
138            println!("  • \x1b[36m{}\x1b[0m ➜ {}", k, v);
139        }
140    } else {
141        println!("\n\x1b[90m💡 Customize shortcuts anytime: \x1b[36mgn shortcuts --set <alias>=<command>\x1b[0m");
142    }
143    println!();
144
145    Ok(())
146}
147
148fn dirs_fallback() -> Option<PathBuf> {
149    std::env::var_os("USERPROFILE")
150        .or_else(|| std::env::var_os("HOME"))
151        .map(|h| PathBuf::from(h).join(".git-notes"))
152}