gn_cli/commands/
shortcuts.rs1use 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 #[arg(short, long)]
12 pub list: bool,
13
14 #[arg(short, long)]
16 pub set: Option<String>,
17
18 #[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 heal".into(), "Auto-heal & re-anchor notes after git rebase / amend [alias: rebase-heal]".into());
44 cli_aliases.insert("gn p2p / gn sync --p2p".into(), "Offline LAN peer-to-peer sync / USB bundle export & import".into());
45 cli_aliases.insert("gn imp".into(), "Import reviews from GitLab MR, Bitbucket PR, Jira, or GitHub [alias: import]".into());
46 cli_aliases.insert("gn i".into(), "1-second setup: configure fetch refspec & auto-sync hooks [alias: init]".into());
47 cli_aliases.insert("gn check / gn gate".into(), "CI quality gating and merge-readiness check [alias: gate]".into());
48 cli_aliases.insert("gn verify / gn sig".into(), "Verify cryptographic signatures of notes [alias: sig]".into());
49 cli_aliases.insert("gn validate / gn fsck".into(), "Validate data refspecs and JSON schema integrity [aliases: fsck, lint]".into());
50
51 let mut tui_keybindings = BTreeMap::new();
52 tui_keybindings.insert("↑ / ↓ or k / j".into(), "Navigate files and notes list with vertical scroll".into());
53 tui_keybindings.insert("Enter / →".into(), "Expand file / focus diff & note thread panel".into());
54 tui_keybindings.insert("Esc / ←".into(), "Navigate back to previous pane".into());
55 tui_keybindings.insert("r".into(), "Open quick reply bar for highlighted thread".into());
56 tui_keybindings.insert("a".into(), "Mark highlighted note as [✔ Approved]".into());
57 tui_keybindings.insert("x / ok".into(), "Mark highlighted note as [✓ Resolved]".into());
58 tui_keybindings.insert("q".into(), "Quit git-notes TUI".into());
59
60 let custom_user_shortcuts = BTreeMap::new();
61
62 Self {
63 cli_aliases,
64 tui_keybindings,
65 custom_user_shortcuts,
66 }
67 }
68}
69
70impl ShortcutConfig {
71 fn config_path() -> Option<PathBuf> {
72 dirs_fallback().map(|p| p.join("shortcuts.json"))
73 }
74
75 pub fn load() -> Self {
76 Self::config_path()
77 .and_then(|p| fs::read_to_string(p).ok())
78 .and_then(|s| serde_json::from_str(&s).ok())
79 .unwrap_or_default()
80 }
81
82 pub fn save(&self) -> Result<()> {
83 if let Some(path) = Self::config_path() {
84 if let Some(parent) = path.parent() {
85 let _ = fs::create_dir_all(parent);
86 }
87 let data = serde_json::to_string_pretty(self)?;
88 fs::write(path, data)?;
89 }
90 Ok(())
91 }
92}
93
94pub fn run(args: &ShortcutsArgs) -> Result<()> {
95 let mut config = ShortcutConfig::load();
96
97 if args.reset {
98 config = ShortcutConfig::default();
99 config.save()?;
100 println!("\x1b[32m✓ Reset all shortcuts and quickies to default configuration.\x1b[0m");
101 return Ok(());
102 }
103
104 if let Some(ref set_binding) = args.set {
105 if let Some((key, val)) = set_binding.split_once('=') {
106 config
107 .custom_user_shortcuts
108 .insert(key.trim().to_string(), val.trim().to_string());
109 config.save()?;
110 println!(
111 "\x1b[32m✓ Saved custom shortcut:\x1b[0m \x1b[36m{}\x1b[0m ➜ \x1b[1m{}\x1b[0m",
112 key.trim(),
113 val.trim()
114 );
115 return Ok(());
116 } else {
117 eprintln!("\x1b[31m✗ Invalid format. Use --set KEY=COMMAND (e.g., gn shortcuts --set c=\"add -m\")\x1b[0m");
118 return Ok(());
119 }
120 }
121
122 println!("\n\x1b[1;36m⚡ git-notes Quickies & Keyboard Shortcuts Cheat Sheet\x1b[0m\n");
123
124 println!("\x1b[1;33mCLI Quickies & Abbreviations (gn):\x1b[0m");
125 println!("┌───────────────────────┬────────────────────────────────────────────────────────────────┐");
126 println!("│ Quickie / Command │ Action Description │");
127 println!("├───────────────────────┼────────────────────────────────────────────────────────────────┤");
128 for (k, v) in &config.cli_aliases {
129 println!("│ \x1b[36m{:<21}\x1b[0m │ {:<62} │", k, v);
130 }
131 println!("└───────────────────────┴────────────────────────────────────────────────────────────────┘");
132
133 println!("\n\x1b[1;33mTUI & Terminal Keybindings:\x1b[0m");
134 println!("┌───────────────────────┬────────────────────────────────────────────────────────────────┐");
135 println!("│ Key / Keystroke │ TUI Navigation & Review Action │");
136 println!("├───────────────────────┼────────────────────────────────────────────────────────────────┤");
137 for (k, v) in &config.tui_keybindings {
138 println!("│ \x1b[35m{:<21}\x1b[0m │ {:<62} │", k, v);
139 }
140 println!("└───────────────────────┴────────────────────────────────────────────────────────────────┘");
141
142 if !config.custom_user_shortcuts.is_empty() {
143 println!("\n\x1b[1;33mCustom User Shortcuts (~/.git-notes/shortcuts.json):\x1b[0m");
144 for (k, v) in &config.custom_user_shortcuts {
145 println!(" • \x1b[36m{}\x1b[0m ➜ {}", k, v);
146 }
147 } else {
148 println!("\n\x1b[90m💡 Customize shortcuts anytime: \x1b[36mgn shortcuts --set <alias>=<command>\x1b[0m");
149 }
150
151 println!("\n┌─────────────────────────────────────────────────────────────┐");
152 println!("│ \x1b[1;36m💬 Enjoying git-notes? Help us grow!\x1b[0m │");
153 println!("│ \x1b[33m⭐ Star on GitHub:\x1b[0m https://github.com/isaim0011/git-notes │");
154 println!("│ \x1b[35m★ Review on Open VSX:\x1b[0m https://open-vsx.org/extension/isaim0011/vscode-git-notes │");
155 println!("└─────────────────────────────────────────────────────────────┘\n");
156
157 Ok(())
158}
159
160fn dirs_fallback() -> Option<PathBuf> {
161 std::env::var_os("USERPROFILE")
162 .or_else(|| std::env::var_os("HOME"))
163 .map(|h| PathBuf::from(h).join(".git-notes"))
164}