From 9014b50618bd9861de6765c313a834791ed7d309 Mon Sep 17 00:00:00 2001 From: hrdl <31923882+hrdl-github@users.noreply.github.com> Date: Sat, 12 Oct 2024 09:49:15 +0000 Subject: [PATCH] Command bindings followup (#317) * Add basic vertical scrolling to help menu. Overscroll prevention and mouse wheel support are missing. * Fix message selection command * Handle esc in command handler * Indent and stylize content of help panel * Swallow unbound key events in help menu --- README.md | 9 +++- src/app.rs | 25 ++++++----- src/command.rs | 36 +++++++++++++++ src/ui/draw.rs | 116 ++++++++++++++++++++++++++++++++++--------------- 4 files changed, 137 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 78dbe17..417ee42 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ libraries that are not available on crates.io. * `ctrl+a / Home` Move cursor to the beginning of the line. * `ctrl+e / End` Move cursor the the end of the line. * Message/channel selection - * `Esc` Reset message selection. + * `esc` Reset message selection or close channel selection popup. * `alt+Up / alt+k / PgUp` Select previous message. * `alt+Down / alt+j / PgDown` Select next message. * `ctrl+j / Up` Select previous channel. @@ -99,6 +99,10 @@ libraries that are not available on crates.io. * `ctrl+p` Open / close channel selection popup. * Clipboard * `alt+y` Copy selected message to clipboard. +* Help menu + * `esc` Close help panel. + * `ctrl+j / Up / PgUp` Previous line + * `ctrl+k / Down / PgDown` Next line ## Custom keybindings The default keybindings can be overwritten at startup by configuring @@ -117,7 +121,8 @@ quit toggle_channel_modal toggle_multiline react -move_text up|down character|word|line +scroll help up|down entry +move_text previous|next character|word|line select_channel previous|next select_channel_modal previous|text select_message previous|next entry diff --git a/src/app.rs b/src/app.rs index 5ba47f2..0aff060 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,7 +59,7 @@ pub struct App { pub storage: Box, pub channels: StatefulList, pub messages: BTreeMap>, - pub help_scroll: u64, + pub help_scroll: (u16, u16), pub user_id: Uuid, pub should_quit: bool, url_regex: LazyRegex, @@ -123,7 +123,7 @@ impl App { storage, channels, messages, - help_scroll: 0, + help_scroll: (0, 0), should_quit: false, url_regex: LazyRegex::new(URL_REGEX), attachment_regex: LazyRegex::new(ATTACHMENT_REGEX), @@ -295,15 +295,15 @@ impl App { self.should_quit = true; } Command::Scroll(Widget::Help, DirectionVertical::Up, MoveAmountVisual::Entry) => { - // TODO: rerender - if self.help_scroll >= 1 { - self.help_scroll -= 1 + if self.help_scroll.0 >= 1 { + self.help_scroll.0 -= 1 } } Command::Scroll(Widget::Help, DirectionVertical::Down, MoveAmountVisual::Entry) => { - // TODO: rerender - self.help_scroll += 1 + // TODO: prevent overscrolling + self.help_scroll.0 += 1 } + Command::NoOp => {} } Ok(()) } @@ -342,9 +342,7 @@ impl App { } } KeyCode::Esc => { - if self.select_channel.is_shown { - self.select_channel.is_shown = false; - } else if !self.reset_editing() { + if !self.reset_editing() { self.reset_message_selection(); } } @@ -1591,7 +1589,12 @@ impl App { } } } - None + if self.is_help() { + // Swallow event + Some(&Command::NoOp) + } else { + None + } } } diff --git a/src/command.rs b/src/command.rs index 6a6164f..50a5c08 100644 --- a/src/command.rs +++ b/src/command.rs @@ -150,6 +150,8 @@ pub enum WindowMode { )] #[strum(serialize_all = "snake_case")] pub enum Command { + #[strum(props(desc = "Do nothing"))] + NoOp, #[strum(props(desc = "Toggle help panel"))] Help, #[strum(props(desc = "Quit application"))] @@ -335,6 +337,31 @@ fn parse(input: &str) -> Result { Ok(Command::SelectChannelModal(direction)) // Ok(Command::SelectChannelModal(MoveDirection::from_str(args.first().unwrap_or(&""))?)) } + Command::SelectMessage(_, _) => { + let usage = E::InsufficientArgs { + cmd: cmd_str.to_string(), + hint: Some( + [ + MoveDirection::VARIANTS.join("|"), + MoveAmountVisual::VARIANTS.join("|"), + ] + .join(" "), + ), + }; + let direction = args.first().ok_or(usage.clone())?; + let amount = args.get(1).ok_or(usage)?; + let direction = MoveDirection::from_str(direction).map_err(|_e| E::BadEnumArg { + arg: direction.to_string(), + accept: MoveDirection::VARIANTS, + optional: false, + })?; + let amount = MoveAmountVisual::from_str(amount).map_err(|_e| E::BadEnumArg { + arg: amount.to_string(), + accept: MoveAmountText::VARIANTS, + optional: false, + })?; + Ok(Command::SelectMessage(direction, amount)) + } Command::CopyMessage(_) => { let usage = E::InsufficientArgs { cmd: cmd_str.to_string(), @@ -396,6 +423,8 @@ alt-y = "copy_message selected" ctrl-e = "edit_message" [channel_modal] +esc = "toggle_channel_modal" +ctrl-p = "toggle_channel_modal" down = "select_channel_modal next" up = "select_channel_modal previous" ctrl-j = "select_channel_modal next" @@ -408,6 +437,13 @@ ctrl-j = "move_text next line" ctrl-k = "move_text previous line" [help] +esc = "help" +ctrl-j = "scroll help down entry" +ctrl-k = "scroll help up entry" +down = "scroll help down entry" +up = "scroll help up entry" +pagedown = "scroll help down entry" +pageup = "scroll help up entry" "#; #[cfg(test)] diff --git a/src/ui/draw.rs b/src/ui/draw.rs index 993e6da..da1e9d3 100644 --- a/src/ui/draw.rs +++ b/src/ui/draw.rs @@ -5,7 +5,7 @@ use std::fmt; use chrono::Datelike; use itertools::Itertools; use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::{Color, Style}; +use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Borders, Clear, List, ListDirection, ListItem, Paragraph}; use ratatui::Frame; @@ -30,9 +30,9 @@ pub fn draw(f: &mut Frame, app: &mut App) { // Display shortcut panel let chunks = Layout::default() .constraints([ - Constraint::Percentage(15), - Constraint::Percentage(70), - Constraint::Percentage(15), + Constraint::Percentage(5), + Constraint::Percentage(90), + Constraint::Percentage(5), ]) .direction(Direction::Horizontal) .split(f.area()); @@ -665,45 +665,89 @@ fn add_edited(msg: &Message, out: &mut dyn fmt::Write) { } } -fn draw_help(f: &mut Frame, app: &mut App, area: Rect) { - let modes = vec![ +fn help_commands<'a>() -> Vec> { + let commands = ::iter() + .map(|cmd| { + ( + strum::EnumProperty::get_str(&cmd, "usage") + .unwrap_or(&cmd.to_string()) + .to_string(), + strum::EnumProperty::get_str(&cmd, "desc") + .unwrap_or("Undocumented") + .to_string(), + ) + }) + .collect_vec(); + let usage_len = commands.iter().map(|inf| inf.0.len()).max().unwrap_or(0); + let commands = commands + .iter() + .map(|inf| Line::raw(format!("{: Vec { + vec![ WindowMode::Normal, WindowMode::Anywhere, WindowMode::Help, WindowMode::ChannelModal, WindowMode::Multiline, WindowMode::MessageSelected, + ] + .iter() + .map(|mode| bindings_mode(app, mode)) + .concat() +} + +fn bindings_mode<'a>(app: &App, mode: &WindowMode) -> Vec> { + let bindings = if let Some(kb) = app.mode_keybindings.get(mode) { + kb.iter() + .map(|(kc, cmd)| { + ( + kc.to_string(), + cmd.to_string(), + strum::EnumProperty::get_str(cmd, "desc") + .unwrap_or("Undocumented") + .to_string(), + ) + }) + .sorted() + .collect_vec() + } else { + Vec::default() + }; + let kc_len = bindings.iter().map(|inf| inf.0.len()).max().unwrap_or(0); + let cmd_len = bindings.iter().map(|inf| inf.1.len()).max().unwrap_or(0); + let bindings = bindings.iter().map(|inf| { + Line::raw(format!( + "{: ::iter() - .map(|cmd| { - format!( - "{} {}", - &strum::EnumProperty::get_str(&cmd, "usage").unwrap_or(&cmd.to_string()), - &strum::EnumProperty::get_str(&cmd, "desc").unwrap_or("Undocumented") - ) - }) - .join("\n"); - let commands = Paragraph::new(commands + "\n" + &mode_shortcuts) - .block(Block::bordered().title("Available commands and configured shortcuts")); - f.render_widget(commands, area); + v.extend(bindings); + v +} + +fn draw_help(f: &mut Frame, app: &mut App, area: Rect) { + let mut command_bindings = help_commands(); + command_bindings.extend(bindings(app)); + let command_bindings = Paragraph::new(Text::from(command_bindings)) + .block(Block::bordered().title("Available commands and configured shortcuts")) + .scroll(app.help_scroll); + f.render_widget(command_bindings, area); } fn displayed_quote(names: &NameResolver, quote: &Message) -> Option {