From fa3e812a301455c7bc7b4d829a7fba01ddaea413 Mon Sep 17 00:00:00 2001 From: Shav Kinderlehrer Date: Thu, 7 Mar 2024 02:05:03 -0500 Subject: [PATCH] Implement keyboard shortcut popup --- flake.nix | 1 + src/app.rs | 18 +++---- src/components/global_keys.rs | 95 +++++++++++++++++++++++++++++------ src/components/hello_world.rs | 20 +++----- src/keys/key_commands.rs | 7 ++- src/tui.rs | 8 ++- 6 files changed, 103 insertions(+), 46 deletions(-) diff --git a/flake.nix b/flake.nix index c59cb93..9eceed3 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,7 @@ cargo rust-analyzer libiconv + clippy ]; shellHook = '' exec zsh diff --git a/src/app.rs b/src/app.rs index a1fd1e4..03ba04a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,7 +22,7 @@ impl App { pub fn new(tick_rate: Duration) -> Result { let tui = tui::init()?; - let mut key_commands = vec![KeyCommand { + let key_commands = vec![KeyCommand { key_code: "q".to_string(), description: "Quit molehole".to_string(), action: Some(AppAction::Quit), @@ -45,7 +45,7 @@ impl App { } pub fn run(&mut self) -> Result<()> { - for component in self.components.iter_mut() { + for component in &mut self.components { component.init()?; } @@ -73,10 +73,9 @@ impl App { if let Some(event) = event { let mut actions: Vec = vec![]; - for component in self.components.iter_mut() { - match component.handle_event(event)? { - Some(action) => actions.push(action), - None => (), + for component in &mut self.components { + if let Some(action) = component.handle_event(event)? { + actions.push(action); } } @@ -90,11 +89,8 @@ impl App { } self.tui.draw(|frame| { - for (_i, component) in self.components.iter_mut().enumerate() { - match component.render(frame, frame.size()) { - Ok(_) => (), - Err(_) => (), - } + for component in &mut self.components { + let _ = component.render(frame, frame.size()); } })?; diff --git a/src/components/global_keys.rs b/src/components/global_keys.rs index 67b1b6a..27adab0 100644 --- a/src/components/global_keys.rs +++ b/src/components/global_keys.rs @@ -1,27 +1,38 @@ -use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; -use ratatui::prelude::*; -use ratatui::widgets::*; +use crossterm::event::{KeyEvent, KeyEventKind}; +use ratatui::prelude::{ + Alignment, Color, Constraint, Direction, Frame, Layout, Line, Margin, Rect, + Span, Style, Stylize, +}; +use ratatui::widgets::block::{Block, BorderType, Title}; +use ratatui::widgets::{ + Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, + Wrap, +}; use crate::app_action::AppAction; use crate::component::Component; -use crate::keys::key_commands::*; +use crate::keys::key_commands::{serialize_key_event, KeyCommand}; #[derive(Default)] pub struct GlobalKeys { pub key_commands: Vec, pub should_show: bool, - pub scroll: u16, + pub scroll: usize, + pub scroll_state: ScrollbarState, } impl Component for GlobalKeys { fn init(&mut self) -> eyre::Result<()> { self.key_commands.push(KeyCommand { key_code: "?".to_string(), - description: "Show help menu".to_string(), + description: "Toggle help menu".to_string(), action: None, }); + self.scroll_state = + ScrollbarState::new(self.key_commands.len()).position(self.scroll); + Ok(()) } @@ -30,12 +41,36 @@ impl Component for GlobalKeys { key: KeyEvent, ) -> eyre::Result> { if key.kind == KeyEventKind::Press { - for key_command in self.key_commands.iter_mut() { - if key_command.key_code == serialize_key_event(key) { - if serialize_key_event(key) == "?" { - self.should_show = !self.should_show; + let key_event = serialize_key_event(key); + let eat_input = match key_event.as_str() { + "?" => { + self.should_show = !self.should_show; + true + } + "down" => { + if self.scroll < self.key_commands.len() - 1 { + self.scroll += 1; + self.scroll_state = + self.scroll_state.position(self.scroll); } + true + } + "up" => { + if self.scroll > 0 { + self.scroll -= 1; + self.scroll_state = + self.scroll_state.position(self.scroll); + } + true + } + _ => false, + }; + if eat_input && self.should_show { + return Ok(None); + } + for key_command in &mut self.key_commands { + if key_command.key_code == key_event { return Ok(key_command.action); } } @@ -45,12 +80,32 @@ impl Component for GlobalKeys { } fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> { + let vertical_center = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(50 / 2), + Constraint::Percentage(50), + Constraint::Percentage(50 / 2), + ]) + .split(rect); + let center = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(50 / 2), + Constraint::Percentage(50), + Constraint::Percentage(50 / 2), + ]) + .split(vertical_center[1])[1]; + let block = Block::default() - .title("Keyboard shortcuts") - .borders(Borders::ALL); + .title( + Title::from("Keyboard shortcuts").alignment(Alignment::Center), + ) + .borders(Borders::ALL) + .border_type(BorderType::Thick); let mut lines: Vec = vec![]; - for key_command in self.key_commands.iter_mut() { + for key_command in &mut self.key_commands { let command = Span::from(key_command.key_code.clone()); let description = Span::from(key_command.description.clone()).italic(); @@ -63,10 +118,20 @@ impl Component for GlobalKeys { let commands = Paragraph::new(lines) .block(block) .wrap(Wrap { trim: true }) - .scroll((self.scroll, 0)); + .scroll((u16::try_from(self.scroll)?, 0)) + .style(Style::default().bg(Color::DarkGray).fg(Color::LightYellow)); if self.should_show { - frame.render_widget(commands, rect); + frame.render_widget(Clear, center); + frame.render_widget(commands, center); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight), + center.inner(&Margin { + vertical: 1, + horizontal: 0, + }), + &mut self.scroll_state, + ); } Ok(()) diff --git a/src/components/hello_world.rs b/src/components/hello_world.rs index 2e3a2ee..ac9d02b 100644 --- a/src/components/hello_world.rs +++ b/src/components/hello_world.rs @@ -1,9 +1,7 @@ -use ratatui::prelude::*; -use ratatui::widgets::Paragraph; +use ratatui::prelude::{Frame, Rect}; +use ratatui::widgets::{Paragraph, Wrap}; -use crate::app_action::AppAction; use crate::component::Component; -use crate::keys::key_commands::*; #[derive(Default, Clone)] pub struct HelloWorld { @@ -16,17 +14,11 @@ impl Component for HelloWorld { Ok(()) } - fn handle_key_event( - &mut self, - key: crossterm::event::KeyEvent, - ) -> eyre::Result> { - self.text = serialize_key_event(key); - Ok(None) - } - fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> { - - frame.render_widget(Paragraph::new(self.text.clone()), rect); + frame.render_widget( + Paragraph::new(self.text.clone()).wrap(Wrap { trim: true }), + rect, + ); Ok(()) } diff --git a/src/keys/key_commands.rs b/src/keys/key_commands.rs index 6124560..70e81b1 100644 --- a/src/keys/key_commands.rs +++ b/src/keys/key_commands.rs @@ -32,15 +32,14 @@ pub fn serialize_key_event(event: KeyEvent) -> String { let char; let key = match event.code { - KeyCode::Backspace => "del", + KeyCode::Backspace | KeyCode::Delete => "del", KeyCode::Enter => "enter", KeyCode::Left => "left", KeyCode::Right => "right", KeyCode::Up => "up", KeyCode::Down => "down", KeyCode::Tab => "tab", - KeyCode::Delete => "del", - KeyCode::Char(c) if c == ' ' => "space", + KeyCode::Char(' ') => "space", KeyCode::Char(c) => { char = c.to_string(); &char @@ -48,7 +47,7 @@ pub fn serialize_key_event(event: KeyEvent) -> String { KeyCode::Esc => "esc", _ => "", }; - let separator = if modifiers.len() > 0 { "-" } else { "" }; + let separator = if modifiers.is_empty() { "-" } else { "" }; let serialized_event = format!("{}{}{}", modifiers.join("-"), separator, key); diff --git a/src/tui.rs b/src/tui.rs index d8ddb90..546e939 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,5 +1,9 @@ -use crossterm::{event, event::Event, execute, terminal::*}; -use ratatui::prelude::*; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, + LeaveAlternateScreen, +}; +use crossterm::{event, event::Event, execute}; +use ratatui::prelude::{CrosstermBackend, Terminal}; use std::io; use std::io::{stdout, Stdout};