chore: added tui scroll for snipps
b4b5c0de
1 file(s) · +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 | } |
|