Skip to main content

gn_cli/commands/
picker.rs

1use anyhow::Result;
2use crossterm::{
3    cursor,
4    event::{self, Event, KeyCode},
5    execute,
6    terminal::{self, ClearType},
7};
8use gn_core::note::Note;
9use std::io::{stdout, Write};
10
11/// Interactively prompt the user to pick a note from a list using arrow keys and Enter.
12/// If user presses Esc or 'q', returns Ok(None).
13pub fn pick_note<'a>(title: &str, notes: &'a [Note]) -> Result<Option<&'a Note>> {
14    if notes.is_empty() {
15        return Ok(None);
16    }
17    if notes.len() == 1 {
18        return Ok(Some(&notes[0]));
19    }
20
21    terminal::enable_raw_mode()?;
22    let mut stdout = stdout();
23    execute!(stdout, cursor::Hide)?;
24
25    let mut selected = notes.len() - 1; // Default to latest
26
27    let render = |stdout: &mut std::io::Stdout, sel: usize| -> Result<()> {
28        execute!(stdout, cursor::MoveToColumn(0), terminal::Clear(ClearType::FromCursorDown))?;
29        println!("\x1b[1;36m? {}\x1b[0m \x1b[90m(Use ↑/↓ arrows, Enter to select, Esc to cancel)\x1b[0m\r", title);
30
31        let start = if sel > 5 { sel - 5 } else { 0 };
32        let end = (start + 8).min(notes.len());
33
34        for i in start..end {
35            let n = &notes[i];
36            let id_short = &n.id.to_string()[..8];
37            let file = n.file.as_deref().unwrap_or("-");
38            let line = n.line_start.unwrap_or(0);
39            let author = n.author.split('<').next().unwrap_or(&n.author).trim();
40            let body_preview = n.body.replace('\n', " ");
41            let body_short = if body_preview.len() > 38 {
42                format!("{}...", &body_preview[..35])
43            } else {
44                body_preview
45            };
46
47            if i == sel {
48                println!(
49                    "  \x1b[36m❯ [{}] {:<8} {:<18} {:<12} \"{}\"\x1b[0m\r",
50                    i + 1,
51                    id_short,
52                    format!("{}:{}", file, line),
53                    author,
54                    body_short
55                );
56            } else {
57                println!(
58                    "    [{}] {:<8} {:<18} {:<12} \"{}\"\r",
59                    i + 1,
60                    id_short,
61                    format!("{}:{}", file, line),
62                    author,
63                    body_short
64                );
65            }
66        }
67        stdout.flush()?;
68        Ok(())
69    };
70
71    render(&mut stdout, selected)?;
72
73    let result = loop {
74        if let Event::Key(key) = event::read()? {
75            match key.code {
76                KeyCode::Up => {
77                    if selected > 0 {
78                        selected -= 1;
79                        // Move cursor back up
80                        let lines_printed = ((selected + 1 - (if selected > 5 { selected - 5 } else { 0 })) + 2) as u16;
81                        execute!(stdout, cursor::MoveUp(lines_printed.min(10)))?;
82                        render(&mut stdout, selected)?;
83                    }
84                }
85                KeyCode::Down => {
86                    if selected + 1 < notes.len() {
87                        selected += 1;
88                        let lines_printed = ((selected - (if selected > 5 { selected - 5 } else { 0 })) + 2) as u16;
89                        execute!(stdout, cursor::MoveUp(lines_printed.min(10)))?;
90                        render(&mut stdout, selected)?;
91                    }
92                }
93                KeyCode::Enter => {
94                    break Ok(Some(&notes[selected]));
95                }
96                KeyCode::Esc | KeyCode::Char('q') => {
97                    break Ok(None);
98                }
99                _ => {}
100            }
101        }
102    };
103
104    execute!(stdout, cursor::Show)?;
105    terminal::disable_raw_mode()?;
106    println!();
107    result
108}