Implement keyboard shortcut popup

This commit is contained in:
Shav Kinderlehrer 2024-03-07 02:05:03 -05:00
parent c2acb446e7
commit fa3e812a30
6 changed files with 103 additions and 46 deletions

View File

@ -18,6 +18,7 @@
cargo cargo
rust-analyzer rust-analyzer
libiconv libiconv
clippy
]; ];
shellHook = '' shellHook = ''
exec zsh exec zsh

View File

@ -22,7 +22,7 @@ impl App {
pub fn new(tick_rate: Duration) -> Result<Self> { pub fn new(tick_rate: Duration) -> Result<Self> {
let tui = tui::init()?; let tui = tui::init()?;
let mut key_commands = vec![KeyCommand { let key_commands = vec![KeyCommand {
key_code: "q".to_string(), key_code: "q".to_string(),
description: "Quit molehole".to_string(), description: "Quit molehole".to_string(),
action: Some(AppAction::Quit), action: Some(AppAction::Quit),
@ -45,7 +45,7 @@ impl App {
} }
pub fn run(&mut self) -> Result<()> { pub fn run(&mut self) -> Result<()> {
for component in self.components.iter_mut() { for component in &mut self.components {
component.init()?; component.init()?;
} }
@ -73,10 +73,9 @@ impl App {
if let Some(event) = event { if let Some(event) = event {
let mut actions: Vec<AppAction> = vec![]; let mut actions: Vec<AppAction> = vec![];
for component in self.components.iter_mut() { for component in &mut self.components {
match component.handle_event(event)? { if let Some(action) = component.handle_event(event)? {
Some(action) => actions.push(action), actions.push(action);
None => (),
} }
} }
@ -90,11 +89,8 @@ impl App {
} }
self.tui.draw(|frame| { self.tui.draw(|frame| {
for (_i, component) in self.components.iter_mut().enumerate() { for component in &mut self.components {
match component.render(frame, frame.size()) { let _ = component.render(frame, frame.size());
Ok(_) => (),
Err(_) => (),
}
} }
})?; })?;

View File

@ -1,27 +1,38 @@
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{KeyEvent, KeyEventKind};
use ratatui::prelude::*; use ratatui::prelude::{
use ratatui::widgets::*; 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::app_action::AppAction;
use crate::component::Component; use crate::component::Component;
use crate::keys::key_commands::*; use crate::keys::key_commands::{serialize_key_event, KeyCommand};
#[derive(Default)] #[derive(Default)]
pub struct GlobalKeys { pub struct GlobalKeys {
pub key_commands: Vec<KeyCommand>, pub key_commands: Vec<KeyCommand>,
pub should_show: bool, pub should_show: bool,
pub scroll: u16, pub scroll: usize,
pub scroll_state: ScrollbarState,
} }
impl Component for GlobalKeys { impl Component for GlobalKeys {
fn init(&mut self) -> eyre::Result<()> { fn init(&mut self) -> eyre::Result<()> {
self.key_commands.push(KeyCommand { self.key_commands.push(KeyCommand {
key_code: "?".to_string(), key_code: "?".to_string(),
description: "Show help menu".to_string(), description: "Toggle help menu".to_string(),
action: None, action: None,
}); });
self.scroll_state =
ScrollbarState::new(self.key_commands.len()).position(self.scroll);
Ok(()) Ok(())
} }
@ -30,12 +41,36 @@ impl Component for GlobalKeys {
key: KeyEvent, key: KeyEvent,
) -> eyre::Result<Option<AppAction>> { ) -> eyre::Result<Option<AppAction>> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
for key_command in self.key_commands.iter_mut() { let key_event = serialize_key_event(key);
if key_command.key_code == serialize_key_event(key) { let eat_input = match key_event.as_str() {
if serialize_key_event(key) == "?" { "?" => {
self.should_show = !self.should_show; 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); return Ok(key_command.action);
} }
} }
@ -45,12 +80,32 @@ impl Component for GlobalKeys {
} }
fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> { 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() let block = Block::default()
.title("Keyboard shortcuts") .title(
.borders(Borders::ALL); Title::from("Keyboard shortcuts").alignment(Alignment::Center),
)
.borders(Borders::ALL)
.border_type(BorderType::Thick);
let mut lines: Vec<Line> = vec![]; let mut lines: Vec<Line> = 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 command = Span::from(key_command.key_code.clone());
let description = let description =
Span::from(key_command.description.clone()).italic(); Span::from(key_command.description.clone()).italic();
@ -63,10 +118,20 @@ impl Component for GlobalKeys {
let commands = Paragraph::new(lines) let commands = Paragraph::new(lines)
.block(block) .block(block)
.wrap(Wrap { trim: true }) .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 { 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(()) Ok(())

View File

@ -1,9 +1,7 @@
use ratatui::prelude::*; use ratatui::prelude::{Frame, Rect};
use ratatui::widgets::Paragraph; use ratatui::widgets::{Paragraph, Wrap};
use crate::app_action::AppAction;
use crate::component::Component; use crate::component::Component;
use crate::keys::key_commands::*;
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct HelloWorld { pub struct HelloWorld {
@ -16,17 +14,11 @@ impl Component for HelloWorld {
Ok(()) Ok(())
} }
fn handle_key_event(
&mut self,
key: crossterm::event::KeyEvent,
) -> eyre::Result<Option<AppAction>> {
self.text = serialize_key_event(key);
Ok(None)
}
fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> { fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> {
frame.render_widget(
frame.render_widget(Paragraph::new(self.text.clone()), rect); Paragraph::new(self.text.clone()).wrap(Wrap { trim: true }),
rect,
);
Ok(()) Ok(())
} }

View File

@ -32,15 +32,14 @@ pub fn serialize_key_event(event: KeyEvent) -> String {
let char; let char;
let key = match event.code { let key = match event.code {
KeyCode::Backspace => "del", KeyCode::Backspace | KeyCode::Delete => "del",
KeyCode::Enter => "enter", KeyCode::Enter => "enter",
KeyCode::Left => "left", KeyCode::Left => "left",
KeyCode::Right => "right", KeyCode::Right => "right",
KeyCode::Up => "up", KeyCode::Up => "up",
KeyCode::Down => "down", KeyCode::Down => "down",
KeyCode::Tab => "tab", KeyCode::Tab => "tab",
KeyCode::Delete => "del", KeyCode::Char(' ') => "space",
KeyCode::Char(c) if c == ' ' => "space",
KeyCode::Char(c) => { KeyCode::Char(c) => {
char = c.to_string(); char = c.to_string();
&char &char
@ -48,7 +47,7 @@ pub fn serialize_key_event(event: KeyEvent) -> String {
KeyCode::Esc => "esc", KeyCode::Esc => "esc",
_ => "", _ => "",
}; };
let separator = if modifiers.len() > 0 { "-" } else { "" }; let separator = if modifiers.is_empty() { "-" } else { "" };
let serialized_event = let serialized_event =
format!("{}{}{}", modifiers.join("-"), separator, key); format!("{}{}{}", modifiers.join("-"), separator, key);

View File

@ -1,5 +1,9 @@
use crossterm::{event, event::Event, execute, terminal::*}; use crossterm::terminal::{
use ratatui::prelude::*; 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;
use std::io::{stdout, Stdout}; use std::io::{stdout, Stdout};