Terminal Markdown previewer — GUI-like experience.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #25 from RivoLink/chore/improve-status-bar-shortcuts

chore: improve status-bar shortcuts

authored by

Rivo Link and committed by
GitHub
6e275cbb 0d3dc7b2

+121 -99
+5 -4
README.md
··· 112 112 | `g` / Home | Top | 113 113 | `G` / End | Bottom | 114 114 | `t` | Toggle TOC sidebar | 115 - | `T` | Open theme picker | 115 + | `Shift+T` | Open theme picker | 116 + | `Shift+E` | Open editor picker | 117 + | `Ctrl+E` | Open in editor | 118 + | `Ctrl+F` / `/` | Find | 119 + | `n` / `N` | Next / prev match | 116 120 | `?` | Show help popup | 117 - | `1`–`9` | Jump to TOC section N | 118 - | `/` / `Ctrl+F` | Search | 119 - | `n` / `N` | Next / prev match | 120 121 | `r` | Force reload (watch mode) | 121 122 | `q` | Quit | 122 123
+4
src/app/mod.rs
··· 65 65 watch: bool, 66 66 flash_active: bool, 67 67 editor_flash_active: bool, 68 + file_picker_open: bool, 69 + picker_loading: bool, 68 70 } 69 71 70 72 pub(crate) struct AppConfig { ··· 385 387 .as_ref() 386 388 .map(|(_, t)| t.elapsed() < Duration::from_millis(2000)) 387 389 .unwrap_or(false), 390 + file_picker_open: self.is_file_picker_open(), 391 + picker_loading: self.is_picker_loading(), 388 392 }; 389 393 390 394 if self.status_cache_key.as_ref() == Some(&cache_key) {
+82 -52
src/render/modal.rs
··· 13 13 14 14 use super::centered_rect; 15 15 16 + const FUZZY_PICKER_FOOTER: &[&str] = &[ 17 + "↑/↓ move", 18 + "<char> filter", 19 + "enter open", 20 + "esc clear", 21 + "ctrl+c quit", 22 + ]; 23 + 24 + const BROWSER_PICKER_FOOTER: &[&str] = &["↑/↓ move", "enter open", "esc parent", "q quit"]; 25 + 26 + const PICKER_FAILED_FOOTER: &[&str] = &["esc quit", "enter quit", "q quit"]; 27 + 28 + fn modal_footer_line(segments: &[&'static str], bg: Color) -> Line<'static> { 29 + let theme = app_theme(); 30 + let shortcut_style = Style::default().fg(theme.ui.status_shortcut_fg).bg(bg); 31 + let separator_style = Style::default().fg(theme.ui.status_separator).bg(bg); 32 + let mut spans = Vec::new(); 33 + for (idx, segment) in segments.iter().enumerate() { 34 + if idx > 0 { 35 + spans.push(Span::styled(" · ", separator_style)); 36 + } 37 + spans.push(Span::styled(*segment, shortcut_style)); 38 + } 39 + Line::from(spans) 40 + } 41 + 16 42 pub(super) fn render_help_popup(f: &mut Frame) { 17 43 let theme = app_theme(); 18 - let area = centered_rect(56, 17, f.area()); 44 + let area = centered_rect(54, 17, f.area()); 19 45 let section_style = Style::default() 20 46 .fg(theme.ui.toc_primary_active) 21 47 .add_modifier(Modifier::BOLD); ··· 23 49 .fg(theme.ui.toc_accent) 24 50 .add_modifier(Modifier::BOLD); 25 51 let text_style = Style::default().fg(theme.ui.toc_primary_inactive); 26 - let footer_style = Style::default().fg(theme.ui.status_shortcut_fg); 52 + 27 53 let title_style = Style::default() 28 54 .fg(theme.markdown.heading_2) 29 55 .add_modifier(Modifier::BOLD); ··· 43 69 Span::styled("scroll", text_style), 44 70 Span::raw(" "), 45 71 Span::styled("Ctrl+F ", key_style), 46 - Span::styled("search", text_style), 72 + Span::styled("find", text_style), 47 73 ]), 48 74 Line::from(vec![ 49 75 Span::styled("PgUp/PgDn ", key_style), ··· 59 85 Line::from(""), 60 86 Line::from(vec![Span::styled("Actions", section_style)]), 61 87 Line::from(vec![ 62 - Span::styled("r ", key_style), 63 - Span::styled("reload (watch)", text_style), 64 - Span::raw(" "), 88 + Span::styled("Shift+E ", key_style), 89 + Span::styled("editor picker", text_style), 90 + Span::raw(" "), 65 91 Span::styled("Ctrl+E ", key_style), 66 92 Span::styled("edit", text_style), 67 93 ]), 68 94 Line::from(vec![ 69 - Span::styled("t ", key_style), 70 - Span::styled("toggle toc", text_style), 71 - Span::raw(" "), 95 + Span::styled("Shift+T ", key_style), 96 + Span::styled("theme picker", text_style), 97 + Span::raw(" "), 72 98 Span::styled("? ", key_style), 73 99 Span::styled("help", text_style), 74 100 ]), 75 101 Line::from(vec![ 76 - Span::styled("T ", key_style), 77 - Span::styled("theme picker", text_style), 78 - Span::raw(" "), 102 + Span::styled("r ", key_style), 103 + Span::styled("reload (watch)", text_style), 104 + Span::raw(" "), 79 105 Span::styled("q ", key_style), 80 106 Span::styled("quit", text_style), 81 107 ]), 82 108 Line::from(vec![ 83 - Span::styled("E ", key_style), 84 - Span::styled("editor picker", text_style), 109 + Span::styled("t ", key_style), 110 + Span::styled("toggle toc", text_style), 85 111 ]), 86 112 Line::from(""), 87 - Line::from(vec![Span::styled("Esc or ? to close", footer_style)]), 113 + modal_footer_line(&["esc close", "? close"], theme.ui.toc_bg), 88 114 ]; 89 115 90 116 f.render_widget(Clear, area); ··· 103 129 104 130 pub(super) fn render_theme_picker(f: &mut Frame, app: &App) { 105 131 let theme = app_theme(); 106 - let area = centered_rect(38, 10, f.area()); 132 + let area = centered_rect(43, 10, f.area()); 107 133 let active = app.theme_picker_reference_preset(); 108 - let footer_style = Style::default().fg(theme.ui.status_shortcut_fg); 109 134 110 135 let mut lines = vec![ 111 136 Line::from(vec![Span::styled( ··· 122 147 } else { 123 148 theme.ui.toc_bg 124 149 }; 125 - let marker = if selected { "▸ " } else { " " }; 126 - let name = if is_active { 127 - format!("{} ✓", theme_preset_label(*preset)) 150 + let marker = if selected { "▎ " } else { " " }; 151 + let check = if is_active { " ✓" } else { "" }; 152 + let modifier = if is_active || selected { 153 + Modifier::BOLD 128 154 } else { 129 - theme_preset_label(*preset).to_string() 155 + Modifier::empty() 130 156 }; 131 157 lines.push(Line::from(vec![ 132 158 Span::styled( ··· 141 167 }), 142 168 ), 143 169 Span::styled( 144 - name, 170 + theme_preset_label(*preset), 145 171 Style::default() 146 172 .fg(if selected { 147 173 theme.ui.toc_primary_active ··· 149 175 theme.ui.toc_primary_inactive 150 176 }) 151 177 .bg(bg) 152 - .add_modifier(if is_active || selected { 153 - Modifier::BOLD 154 - } else { 155 - Modifier::empty() 156 - }), 178 + .add_modifier(modifier), 179 + ), 180 + Span::styled( 181 + check, 182 + Style::default() 183 + .fg(theme.ui.toc_accent) 184 + .bg(bg) 185 + .add_modifier(modifier), 157 186 ), 158 187 ])); 159 188 } 160 189 lines.push(Line::from("")); 161 - lines.push(Line::from(vec![Span::styled( 162 - "Enter keep • Esc restore", 163 - footer_style.bg(theme.ui.toc_bg), 164 - )])); 190 + lines.push(modal_footer_line( 191 + &["↑/↓ preview", "enter keep", "esc restore"], 192 + theme.ui.toc_bg, 193 + )); 165 194 166 195 f.render_widget(Clear, area); 167 196 f.render_widget( ··· 184 213 .fg(theme.markdown.heading_2) 185 214 .add_modifier(Modifier::BOLD); 186 215 let section_style = Style::default().fg(theme.ui.status_shortcut_fg); 187 - let footer_style = Style::default().fg(theme.ui.status_shortcut_fg); 216 + 188 217 let inner_height = area.height.saturating_sub(2) as usize; 189 218 let header_lines = if app.is_fuzzy_file_picker() { 4 } else { 3 }; 190 219 let total = app.file_picker_filtered_indices().len(); ··· 323 352 lines.push(Line::from("")); 324 353 } 325 354 326 - lines.push(Line::from(vec![Span::styled( 355 + lines.push(modal_footer_line( 327 356 if app.is_fuzzy_file_picker() { 328 - "↑/↓ move • enter open • type filter • esc clear • ctrl+c quit" 357 + FUZZY_PICKER_FOOTER 329 358 } else { 330 - "enter open • backspace up • ctrl+c quit" 359 + BROWSER_PICKER_FOOTER 331 360 }, 332 - footer_style.bg(theme.ui.toc_bg), 333 - )])); 361 + theme.ui.toc_bg, 362 + )); 334 363 335 364 f.render_widget(Clear, area); 336 365 f.render_widget( ··· 353 382 .fg(theme.markdown.heading_2) 354 383 .add_modifier(Modifier::BOLD); 355 384 let section_style = Style::default().fg(theme.ui.status_shortcut_fg); 356 - let footer_style = Style::default().fg(theme.ui.status_shortcut_fg); 385 + 357 386 let is_failed = app.is_picker_load_failed(); 358 387 let is_fuzzy = matches!( 359 388 app.pending_picker_mode(), ··· 383 412 lines.push(Line::from(vec![ 384 413 Span::styled("Query: ", section_style), 385 414 Span::styled( 386 - " type to filter ".to_string(), 415 + " type to filter ", 387 416 Style::default() 388 417 .fg(theme.ui.toc_primary_inactive) 389 418 .bg(theme.markdown.inline_code_bg), ··· 402 431 } 403 432 404 433 lines.push(Line::from("")); 405 - lines.push(Line::from(vec![Span::styled( 406 - if is_fuzzy { 407 - "↑/↓ move • enter open • type filter • esc clear • ctrl+c quit" 434 + lines.push(modal_footer_line( 435 + if is_failed { 436 + PICKER_FAILED_FOOTER 437 + } else if is_fuzzy { 438 + FUZZY_PICKER_FOOTER 408 439 } else { 409 - "enter open • backspace up • ctrl+c quit" 440 + BROWSER_PICKER_FOOTER 410 441 }, 411 - footer_style.bg(theme.ui.toc_bg), 412 - )])); 442 + theme.ui.toc_bg, 443 + )); 413 444 414 445 f.render_widget(Clear, area); 415 446 f.render_widget( ··· 521 552 let section_style = Style::default() 522 553 .fg(theme.ui.toc_primary_active) 523 554 .add_modifier(Modifier::BOLD); 524 - let footer_style = Style::default().fg(theme.ui.status_shortcut_fg); 525 555 526 556 let title_style = Style::default().fg(theme.ui.status_shortcut_fg); 527 557 ··· 558 588 if is_selected || is_current { 559 589 modifier |= Modifier::BOLD; 560 590 } 561 - let marker = if is_selected { "▸ " } else { " " }; 591 + let marker = if is_selected { "▎ " } else { " " }; 562 592 let check = if is_current { " ✓" } else { "" }; 563 593 Line::from(vec![ 564 594 Span::styled( ··· 603 633 } 604 634 605 635 lines.push(Line::from("")); 606 - lines.push(Line::from(vec![Span::styled( 607 - "Enter select • Esc cancel", 608 - footer_style, 609 - )])); 636 + lines.push(modal_footer_line( 637 + &["↑/↓ move", "enter confirm", "esc cancel"], 638 + theme.ui.toc_bg, 639 + )); 610 640 611 641 let height = (lines.len() as u16 + 2).min(18); 612 - let area = centered_rect(38, height, f.area()); 642 + let area = centered_rect(42, height, f.area()); 613 643 614 644 f.render_widget(Clear, area); 615 645 f.render_widget(
+9 -33
src/render/status.rs
··· 117 117 pub(crate) fn status_hint_segments(app: &App) -> &'static [&'static str] { 118 118 if app.is_search_mode() { 119 119 &["enter confirm", "esc cancel"] 120 - } else if app.is_file_picker_open() { 121 - if app.is_fuzzy_file_picker() { 122 - &["↑/↓ move", "enter open", "backspace delete", "ctrl+c quit"] 123 - } else { 124 - &["j/k move", "enter open", "backspace up", "ctrl+c quit"] 125 - } 126 - } else if app.is_theme_picker_open() { 127 - &["j/k preview", "enter keep", "esc restore"] 128 - } else if app.is_help_open() { 129 - &["esc close", "? close"] 130 120 } else if app.has_active_search() { 131 - &[ 132 - "enter next", 133 - "n/N next/prev", 134 - "/ search", 135 - "? help", 136 - "T theme", 137 - "esc clear", 138 - "q quit", 139 - ] 121 + &["n/N next/prev", "esc cancel"] 140 122 } else { 141 - &[ 142 - "j/k scroll", 143 - "g/G top/bot", 144 - "t toc", 145 - "T theme", 146 - "/ search", 147 - "? help", 148 - "n/N next/prev", 149 - "q quit", 150 - ] 123 + &["ctrl+e edit", "ctrl+f find", "t toc", "? help", "q quit"] 151 124 } 152 125 } 153 126 ··· 158 131 .iter() 159 132 .map(|segment| { 160 133 vec![Span::styled( 161 - (*segment).to_string(), 134 + *segment, 162 135 Style::default().fg(theme.ui.status_shortcut_fg).bg(bar_bg), 163 136 )] 164 137 }) ··· 215 188 left_section.extend(section); 216 189 } 217 190 218 - if let Some(section) = status_watch_section(app) { 219 - left_section.extend(section); 191 + let file_open = !app.is_file_picker_open() && !app.is_picker_loading(); 192 + if file_open { 193 + if let Some(section) = status_watch_section(app) { 194 + left_section.extend(section); 195 + } 220 196 } 221 197 222 198 let mut sections = vec![left_section, status_shortcuts_section(app, bar_bg)]; 223 - if !app.is_file_picker_open() && !app.is_picker_loading() { 199 + if file_open { 224 200 sections.push(status_percent_section(pct, bar_bg)); 225 201 } 226 202
+21 -10
src/runtime.rs
··· 144 144 if app.is_help_open() { 145 145 match key.code { 146 146 KeyCode::Esc | KeyCode::Char('?') => app.close_help(), 147 + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { 148 + app.close_help(); 149 + } 147 150 _ => state_changed = false, 148 151 } 149 152 } else if app.is_picker_loading() { 150 153 match key.code { 154 + KeyCode::Char('q') => break, 151 155 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { 152 156 break; 153 157 } ··· 155 159 } 156 160 } else if app.is_picker_load_failed() { 157 161 match key.code { 158 - KeyCode::Esc | KeyCode::Enter => break, 162 + KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => break, 159 163 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { 160 164 break; 161 165 } ··· 179 183 } 180 184 KeyCode::Up if app.is_fuzzy_file_picker() => app.move_file_picker_up(), 181 185 KeyCode::Esc => { 182 - if app.is_browser_file_picker() 183 - || app.file_picker_query().is_empty() 184 - { 186 + if app.is_browser_file_picker() { 187 + state_changed = app.open_file_picker_parent(); 188 + } else if app.file_picker_query().is_empty() { 185 189 state_changed = false; 186 190 } else { 187 191 app.clear_file_picker_query(); ··· 206 210 _ => state_changed = false, 207 211 } 208 212 } else if app.is_theme_picker_open() { 213 + let dismiss = matches!(key.code, KeyCode::Esc | KeyCode::Char('T')) 214 + || (key.code == KeyCode::Char('c') 215 + && key.modifiers.contains(KeyModifiers::CONTROL)); 216 + if dismiss { 217 + app.restore_theme_picker_preview(ss, themes); 218 + needs_redraw = true; 219 + state_changed = false; 220 + } 209 221 match key.code { 210 - KeyCode::Esc => { 211 - app.restore_theme_picker_preview(ss, themes); 212 - needs_redraw = true; 213 - state_changed = false; 214 - } 222 + _ if dismiss => {} 215 223 KeyCode::Enter => app.close_theme_picker(), 216 224 KeyCode::Char('j') | KeyCode::Down => { 217 225 app.move_theme_picker_down(); ··· 236 244 } 237 245 } else if app.is_editor_picker_open() { 238 246 match key.code { 239 - KeyCode::Esc => app.cancel_editor_picker(), 247 + KeyCode::Esc | KeyCode::Char('E') => app.cancel_editor_picker(), 248 + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { 249 + app.cancel_editor_picker(); 250 + } 240 251 KeyCode::Enter => app.close_editor_picker(), 241 252 KeyCode::Char('j') | KeyCode::Down => app.move_editor_picker_down(), 242 253 KeyCode::Char('k') | KeyCode::Up => app.move_editor_picker_up(),