chore: added tui scroll for snipps b4b5c0de
Steve · 2026-02-18 21:46 1 file(s) · +103 −11
src/bin/tui.rs +103 −11
8 8
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
9 9
};
10 10
use sipp_rust::db::{self, Snippet};
11 +
use std::time::{Duration, Instant};
12 +
13 +
enum Focus {
14 +
    List,
15 +
    Content,
16 +
}
11 17
12 18
struct App {
13 19
    snippets: Vec<Snippet>,
14 20
    list_state: ListState,
15 21
    should_quit: bool,
22 +
    status_message: Option<(String, Instant)>,
23 +
    focus: Focus,
24 +
    content_scroll: u16,
16 25
}
17 26
18 27
impl App {
25 34
            snippets,
26 35
            list_state,
27 36
            should_quit: false,
37 +
            status_message: None,
38 +
            focus: Focus::List,
39 +
            content_scroll: 0,
28 40
        }
29 41
    }
30 42
42 54
            None => 0,
43 55
        };
44 56
        self.list_state.select(Some(i));
57 +
        self.content_scroll = 0;
45 58
    }
46 59
47 60
    fn move_down(&mut self) {
54 67
            None => 0,
55 68
        };
56 69
        self.list_state.select(Some(i));
70 +
        self.content_scroll = 0;
57 71
    }
58 72
59 -
    fn copy_selected(&self) {
73 +
    fn scroll_up(&mut self) {
74 +
        self.content_scroll = self.content_scroll.saturating_sub(1);
75 +
    }
76 +
77 +
    fn scroll_down(&mut self, max_lines: u16) {
78 +
        if self.content_scroll < max_lines {
79 +
            self.content_scroll += 1;
80 +
        }
81 +
    }
82 +
83 +
    fn copy_selected(&mut self) {
60 84
        if let Some(snippet) = self.selected_snippet() {
61 85
            if let Ok(mut clipboard) = Clipboard::new() {
62 86
                let _ = clipboard.set_text(&snippet.content);
87 +
                self.status_message = Some(("Copied!".to_string(), Instant::now()));
88 +
            }
89 +
        }
90 +
    }
91 +
92 +
    fn clear_expired_status(&mut self) {
93 +
        if let Some((_, time)) = &self.status_message {
94 +
            if time.elapsed() > Duration::from_secs(2) {
95 +
                self.status_message = None;
63 96
            }
64 97
        }
65 98
    }
77 110
    mut app: App,
78 111
) -> Result<(), Box<dyn std::error::Error>> {
79 112
    while !app.should_quit {
113 +
        app.clear_expired_status();
114 +
115 +
        let content_line_count = app
116 +
            .selected_snippet()
117 +
            .map(|s| s.content.lines().count() as u16)
118 +
            .unwrap_or(0);
119 +
80 120
        terminal.draw(|frame| {
121 +
            let outer = Layout::vertical([
122 +
                Constraint::Min(1),
123 +
                Constraint::Length(1),
124 +
            ])
125 +
            .split(frame.area());
126 +
81 127
            let chunks = Layout::horizontal([
82 128
                Constraint::Percentage(30),
83 129
                Constraint::Percentage(70),
84 130
            ])
85 -
            .split(frame.area());
131 +
            .split(outer[0]);
86 132
87 133
            let items: Vec<ListItem> = app
88 134
                .snippets
90 136
                .map(|s| ListItem::new(s.name.as_str()))
91 137
                .collect();
92 138
139 +
            let list_border_style = match app.focus {
140 +
                Focus::List => Style::default().fg(Color::Yellow),
141 +
                Focus::Content => Style::default().fg(Color::DarkGray),
142 +
            };
143 +
            let content_border_style = match app.focus {
144 +
                Focus::Content => Style::default().fg(Color::Yellow),
145 +
                Focus::List => Style::default().fg(Color::DarkGray),
146 +
            };
147 +
93 148
            let list = List::new(items)
94 -
                .block(Block::default().title(" Snippets ").borders(Borders::ALL))
149 +
                .block(
150 +
                    Block::default()
151 +
                        .title(" Snippets ")
152 +
                        .borders(Borders::ALL)
153 +
                        .border_style(list_border_style),
154 +
                )
95 155
                .highlight_style(
96 156
                    Style::default()
97 157
                        .fg(Color::Yellow)
107 167
                .unwrap_or("");
108 168
109 169
            let paragraph = Paragraph::new(Text::raw(content))
110 -
                .block(Block::default().title(" Content ").borders(Borders::ALL));
170 +
                .block(
171 +
                    Block::default()
172 +
                        .title(" Content ")
173 +
                        .borders(Borders::ALL)
174 +
                        .border_style(content_border_style),
175 +
                )
176 +
                .scroll((app.content_scroll, 0));
111 177
112 178
            frame.render_widget(paragraph, chunks[1]);
179 +
180 +
            if let Some((msg, _)) = &app.status_message {
181 +
                let status = Paragraph::new(Text::raw(msg.as_str()))
182 +
                    .style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD));
183 +
                frame.render_widget(status, outer[1]);
184 +
            }
113 185
        })?;
114 186
115 -
        if let Event::Key(key) = event::read()? {
116 -
            match key.code {
117 -
                KeyCode::Char('q') => app.should_quit = true,
118 -
                KeyCode::Char('j') | KeyCode::Down => app.move_down(),
119 -
                KeyCode::Char('k') | KeyCode::Up => app.move_up(),
120 -
                KeyCode::Char('y') => app.copy_selected(),
121 -
                _ => {}
187 +
        if event::poll(Duration::from_millis(100))? {
188 +
            if let Event::Key(key) = event::read()? {
189 +
                match app.focus {
190 +
                    Focus::List => match key.code {
191 +
                        KeyCode::Char('q') => app.should_quit = true,
192 +
                        KeyCode::Char('j') | KeyCode::Down => app.move_down(),
193 +
                        KeyCode::Char('k') | KeyCode::Up => app.move_up(),
194 +
                        KeyCode::Char('y') => app.copy_selected(),
195 +
                        KeyCode::Enter => {
196 +
                            if app.selected_snippet().is_some() {
197 +
                                app.focus = Focus::Content;
198 +
                            }
199 +
                        }
200 +
                        _ => {}
201 +
                    },
202 +
                    Focus::Content => match key.code {
203 +
                        KeyCode::Char(' ') | KeyCode::Esc | KeyCode::Char('q') => {
204 +
                            app.focus = Focus::List;
205 +
                        }
206 +
                        KeyCode::Char('j') | KeyCode::Down => {
207 +
                            app.scroll_down(content_line_count);
208 +
                        }
209 +
                        KeyCode::Char('k') | KeyCode::Up => app.scroll_up(),
210 +
                        KeyCode::Char('y') => app.copy_selected(),
211 +
                        _ => {}
212 +
                    },
213 +
                }
122 214
            }
123 215
        }
124 216
    }