Skip to main content

gn_cli/commands/
show.rs

1use anyhow::{anyhow, Result};
2use clap::Args;
3use gn_core::NotesEngine;
4
5#[derive(Args)]
6pub struct ShowArgs {
7    /// Note ID, index number (1, 2, ...), or "latest" / "^" (interactive picker if omitted)
8    pub id: Option<String>,
9
10    /// Show entire thread
11    #[arg(short, long)]
12    pub thread: bool,
13
14    /// Output as JSON
15    #[arg(long)]
16    pub json: bool,
17}
18
19pub fn run(args: &ShowArgs) -> Result<()> {
20    let engine = NotesEngine::new(".");
21    let namespaces = vec!["comments", "review", "todos"];
22
23    let mut all_notes = Vec::new();
24
25    for ns in &namespaces {
26        let namespace_enum = gn_core::Namespace::Custom(ns.to_string());
27        if let Ok(notes) = engine.read_notes(&namespace_enum) {
28            all_notes.extend(notes);
29        }
30    }
31
32    if all_notes.is_empty() {
33        println!("No notes found in repository.");
34        return Ok(());
35    }
36
37    // If ID is omitted and in interactive terminal, prompt interactive picker!
38    let found_note = match &args.id {
39        None => {
40            if args.json {
41                all_notes.last().cloned()
42            } else {
43                match super::picker::pick_note("Select note to view:", &all_notes)? {
44                    Some(n) => Some(n.clone()),
45                    None => {
46                        println!("Cancelled.");
47                        return Ok(());
48                    }
49                }
50            }
51        }
52        Some(raw_id) => {
53            let target = raw_id.trim().trim_start_matches('#');
54            if target.eq_ignore_ascii_case("latest") || target == "^" {
55                all_notes.last().cloned()
56            } else if let Ok(idx) = target.parse::<usize>() {
57                if idx >= 1 && idx <= all_notes.len() {
58                    Some(all_notes[idx - 1].clone())
59                } else {
60                    None
61                }
62            } else {
63                all_notes
64                    .iter()
65                    .find(|n| n.id.to_string().starts_with(target))
66                    .cloned()
67            }
68        }
69    };
70
71    let note = found_note.ok_or_else(|| anyhow!("Note not found"))?;
72
73    if args.json {
74        if args.thread {
75            let mut thread = vec![note.clone()];
76            for n in &all_notes {
77                if n.thread_id == Some(note.id) {
78                    thread.push(n.clone());
79                }
80            }
81            println!("{}", serde_json::to_string_pretty(&thread)?);
82        } else {
83            println!("{}", serde_json::to_string_pretty(&note)?);
84        }
85    } else {
86        println!("\x1b[1;36mNote {}\x1b[0m", note.id);
87        println!("{}", "─".repeat(60));
88        println!("Commit:     {}", note.commit);
89        if let Some(f) = &note.file {
90            let l = note.line_start.unwrap_or(0);
91            println!("File:       {}:{}", f, l);
92        }
93        println!("Author:     {}", note.author);
94        println!("Date:       {}", note.timestamp);
95        println!("Namespace:  {:?}", note.namespace);
96        println!("Status:     {:?}", note.status);
97        println!("\nMessage:\n{}", note.body);
98
99        if args.thread {
100            let replies: Vec<&gn_core::note::Note> = all_notes
101                .iter()
102                .filter(|n| n.thread_id == Some(note.id))
103                .collect();
104
105            if !replies.is_empty() {
106                println!("\n\x1b[1mThread Replies ({}):\x1b[0m", replies.len());
107                println!("{}", "─".repeat(60));
108                for r in replies {
109                    println!("\x1b[36m↳ [{}]\x1b[0m {}:", &r.id.to_string()[..8], r.author);
110                    println!("  {}\n", r.body.trim());
111                }
112            }
113        }
114    }
115
116    Ok(())
117}