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 = sel.saturating_sub(5);
32        let end = (start + 8).min(notes.len());
33
34        for (i, n) in notes.iter().enumerate().take(end).skip(start) {
35            let id_short = &n.id.to_string()[..8];
36            let file = n.file.as_deref().unwrap_or("-");
37            let line = n.line_start.unwrap_or(0);
38            let author = n.author.split('<').next().unwrap_or(&n.author).trim();
39            let body_preview = n.body.replace('\n', " ");
40            let body_short = if body_preview.len() > 38 {
41                format!("{}...", &body_preview[..35])
42            } else {
43                body_preview
44            };
45
46            if i == sel {
47                println!(
48                    "  \x1b[36m❯ [{}] {:<8} {:<18} {:<12} \"{}\"\x1b[0m\r",
49                    i + 1,
50                    id_short,
51                    format!("{}:{}", file, line),
52                    author,
53                    body_short
54                );
55            } else {
56                println!(
57                    "    [{}] {:<8} {:<18} {:<12} \"{}\"\r",
58                    i + 1,
59                    id_short,
60                    format!("{}:{}", file, line),
61                    author,
62                    body_short
63                );
64            }
65        }
66        stdout.flush()?;
67        Ok(())
68    };
69
70    render(&mut stdout, selected)?;
71
72    let result = loop {
73        if let Event::Key(key) = event::read()? {
74            match key.code {
75                KeyCode::Up => {
76                    if selected > 0 {
77                        selected -= 1;
78                        // Move cursor back up
79                        let lines_printed = ((selected + 1 - selected.saturating_sub(5)) + 2) as u16;
80                        execute!(stdout, cursor::MoveUp(lines_printed.min(10)))?;
81                        render(&mut stdout, selected)?;
82                    }
83                }
84                KeyCode::Down => {
85                    if selected + 1 < notes.len() {
86                        selected += 1;
87                        let lines_printed = ((selected - selected.saturating_sub(5)) + 2) as u16;
88                        execute!(stdout, cursor::MoveUp(lines_printed.min(10)))?;
89                        render(&mut stdout, selected)?;
90                    }
91                }
92                KeyCode::Enter => {
93                    break Ok(Some(&notes[selected]));
94                }
95                KeyCode::Esc | KeyCode::Char('q') => {
96                    break Ok(None);
97                }
98                _ => {}
99            }
100        }
101    };
102
103    execute!(stdout, cursor::Show)?;
104    terminal::disable_raw_mode()?;
105    println!();
106    result
107}