Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): support modifier (ctrl and alt) in keys #8

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
direction = "horizontal"
tree_size = 40

[header]
disable = false
format = "{version} - {data_source} ({content_type}) - {data_size}"

[keys]
move_up = ["k", "<up>"]
move_down = ["j", "<down>"]
Expand All @@ -14,15 +18,16 @@ select_last = ["G"]
close_parent = ["<backspace>"]
change_root = ["r"]
reset = ["<esc>"]
page_up = ["<page-up>", "u"]
page_down = ["<page-down>", "d"]
page_up = ["<page-up>", "<ctrl-y>"]
page_down = ["<page-down>", "<ctrl-e>"]
change_layout = ["v"]
tree_scale_up = ["["]
tree_scale_down = ["]"]
switch = ["<tab>"]
quit = ["q"]
quit = ["<ctrl-c>", "q"]

[colors]
header = {bold = true}
focus_border = {fg = "magenta", bold = true}

[colors.tree]
Expand Down
176 changes: 141 additions & 35 deletions src/config/keys.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashSet;

use anyhow::{bail, Context, Result};
use crossterm::event::KeyCode;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};

macro_rules! generate_keys_default {
Expand Down Expand Up @@ -41,7 +41,7 @@ macro_rules! generate_actions {
let mut unique = HashSet::new();
self.actions = vec![
$(
(parse_keys(&self.$field, &mut unique).with_context(|| format!("parse key {}", stringify!($field)))?, Action::$value),
(Key::parse_keys(&self.$field, &mut unique).with_context(|| format!("parse keys for action {}", stringify!($field)))?, Action::$value),
)+
];
Ok(())
Expand All @@ -50,42 +50,147 @@ macro_rules! generate_actions {
};
}

fn parse_keys(keys: &[String], unique: &mut HashSet<String>) -> Result<Vec<KeyCode>> {
let mut codes = Vec::with_capacity(keys.len());
for key in keys {
if unique.contains(key) {
bail!("the key '{key}' is used by another action, cannot be used twice");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Key {
Char(char),

Ctrl(char),
Alt(char),

F(u8),

Backspace,
Enter,
Left,
Right,
Up,
Down,
PageUp,
PageDown,
Tab,
Esc,
}

impl Key {
fn from_event(event: KeyEvent) -> Option<Self> {
if event.modifiers == KeyModifiers::CONTROL {
if let KeyCode::Char(char) = event.code {
return Some(Self::Ctrl(char));
}

return None;
}

if event.modifiers == KeyModifiers::ALT {
if let KeyCode::Char(char) = event.code {
return Some(Self::Alt(char));
}

return None;
}
unique.insert(key.clone());

if event.modifiers == KeyModifiers::SHIFT {
if let KeyCode::Char(char) = event.code {
return Some(Self::Char(char));
}

return None;
}

if event.modifiers != KeyModifiers::NONE {
return None;
}

match event.code {
KeyCode::Char(char) => Some(Self::Char(char)),
KeyCode::Backspace => Some(Self::Backspace),
KeyCode::Enter => Some(Self::Enter),
KeyCode::Left => Some(Self::Left),
KeyCode::Right => Some(Self::Right),
KeyCode::Up => Some(Self::Up),
KeyCode::Down => Some(Self::Down),
KeyCode::PageUp => Some(Self::PageUp),
KeyCode::PageDown => Some(Self::PageDown),
KeyCode::Tab => Some(Self::Tab),
KeyCode::Esc => Some(Self::Esc),
KeyCode::F(n) => Some(Self::F(n)),
_ => None,
}
}

fn parse(key: &str) -> Result<Self> {
if !key.starts_with('<') {
if key.len() != 1 {
bail!("key length should be equal to 1");
bail!("invalid key '{key}', length should be equal to 1");
}
let char = key.chars().next().unwrap();
let code = KeyCode::Char(char);
codes.push(code);
continue;
return Ok(Self::Char(char));
}

let key = key.replace(['-', '_'], "");
let raw_key = key;

let code = match key.as_str() {
"<backspace>" => KeyCode::Backspace,
"<enter>" => KeyCode::Enter,
"<left>" => KeyCode::Left,
"<right>" => KeyCode::Right,
"<up>" => KeyCode::Up,
"<down>" => KeyCode::Down,
"<pageup>" => KeyCode::PageUp,
"<pagedown>" => KeyCode::PageDown,
"<tab>" => KeyCode::Tab,
"<esc>" => KeyCode::Esc,
_ => bail!("unsupported key: '{}'", key),
let key = key.strip_prefix('<').unwrap();
let key = match key.strip_suffix('>') {
Some(key) => key,
None => bail!("invalid key '{raw_key}', should be ends with '>'"),
};
codes.push(code);

if let Some(key) = key.strip_prefix("ctrl-") {
if key.len() != 1 {
bail!("invalid key '{raw_key}', should be '<ctrl-x>'");
}
let char = key.chars().next().unwrap();
return Ok(Self::Ctrl(char));
}

if let Some(key) = key.strip_prefix("alt-") {
if key.len() != 1 {
bail!("invalid key '{raw_key}', should be '<alt-x>'");
}
let char = key.chars().next().unwrap();
return Ok(Self::Alt(char));
}

if let Some(key) = key.strip_prefix('f') {
let n = match key.parse::<u8>() {
Ok(n) => n,
Err(_) => bail!("invalid key '{raw_key}', should be '<fN>'"),
};

if n == 0 || n > 12 {
bail!("invalid key '{raw_key}', fN should be in range [1, 12]");
}

return Ok(Self::F(n));
}

let key = key.replace(['-', '_'], "");
Ok(match key.as_str() {
"backspace" => Self::Backspace,
"enter" => Self::Enter,
"left" => Self::Left,
"right" => Self::Right,
"up" => Self::Up,
"down" => Self::Down,
"pageup" => Self::PageUp,
"pagedown" => Self::PageDown,
"tab" => Self::Tab,
"esc" => Self::Esc,
_ => bail!("unsupported key '{raw_key}'"),
})
}

fn parse_keys(raw_keys: &[String], unique: &mut HashSet<String>) -> Result<Vec<Self>> {
let mut keys = Vec::with_capacity(raw_keys.len());
for raw_key in raw_keys {
if unique.contains(raw_key) {
bail!("the key '{raw_key}' is used by another action, cannot be used twice");
}
unique.insert(raw_key.clone());
keys.push(Self::parse(raw_key)?);
}
Ok(keys)
}
Ok(codes)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -139,7 +244,7 @@ pub struct Keys {
pub quit: Vec<String>,

#[serde(skip)]
pub actions: Vec<(Vec<KeyCode>, Action)>,
actions: Vec<(Vec<Key>, Action)>,
}

generate_keys_default!(
Expand All @@ -154,13 +259,13 @@ generate_keys_default!(
close_parent => ["<backspace>"],
change_root => ["r"],
reset => ["<esc>"],
page_up => ["<page-up>", "u"],
page_down => ["<page-down>", "d"],
page_up => ["<page-up>", "<ctrl-y>"],
page_down => ["<page-down>", "<ctrl-e>"],
change_layout => ["v"],
tree_scale_up => ["["],
tree_scale_down => ["]"],
switch => ["<tab>"],
quit => ["q"]
quit => ["<ctrl-c>", "q"]
);

generate_actions!(
Expand All @@ -185,10 +290,11 @@ generate_actions!(
);

impl Keys {
pub fn get_key_action(&self, key_code: KeyCode) -> Option<Action> {
for (codes, action) in self.actions.iter() {
for code in codes {
if *code == key_code {
pub fn get_key_action(&self, event: KeyEvent) -> Option<Action> {
let event_key = Key::from_event(event)?;
for (keys, action) in self.actions.iter() {
for key in keys {
if *key == event_key {
return Some(*action);
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,15 @@ fn run() -> Result<()> {
app.set_header(header_ctx);
}

app.show().context("show tui")?;
let mut terminal = ui::start().context("start tui")?;
let result = app.show(&mut terminal).context("show tui");

Ok(())
// Regardless of how the TUI app executes, we should always restore the terminal.
// Otherwise, if the app encounters an error (such as a draw error), the user's terminal
// will become a mess.
ui::restore(terminal).context("restore terminal")?;

result
}

fn main() {
Expand Down
28 changes: 6 additions & 22 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use std::io::Stdout;

use anyhow::Result;
use crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyEvent, MouseButton, MouseEventKind,
};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::event::{Event, KeyEvent, MouseButton, MouseEventKind};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Layout, Position, Rect};
use ratatui::{Frame, Terminal};
Expand Down Expand Up @@ -75,12 +74,7 @@ impl<'a> App<'a> {
}
}

pub fn show(&mut self) -> Result<()> {
terminal::enable_raw_mode()?;
let mut stdout = std::io::stdout();
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;

let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
pub fn show(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
terminal.draw(|frame| self.draw(frame))?;

loop {
Expand Down Expand Up @@ -108,17 +102,7 @@ impl<'a> App<'a> {
match refresh {
Refresh::Update => {}
Refresh::Skip => continue,
Refresh::Quit => {
// restore terminal
terminal::disable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
return Ok(());
}
Refresh::Quit => return Ok(()),
}
terminal.draw(|frame| self.draw(frame))?;
}
Expand Down Expand Up @@ -215,7 +199,7 @@ impl<'a> App<'a> {
}

fn on_key(&mut self, key: KeyEvent) -> Refresh {
let action = self.cfg.keys.get_key_action(key.code);
let action = self.cfg.keys.get_key_action(key);
if action.is_none() {
return Refresh::Skip;
}
Expand Down
42 changes: 37 additions & 5 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
use ratatui::style::Style;
use ratatui::widgets::BorderType;

use crate::config::colors::Color;

mod app;
mod data_block;
mod header;
mod tree_overview;

use std::io::Stdout;

use anyhow::{Context, Result};
use crossterm::{event, terminal};
use ratatui::backend::CrosstermBackend;
use ratatui::style::Style;
use ratatui::widgets::BorderType;
use ratatui::Terminal;

use crate::config::colors::Color;

pub use app::App;
pub use header::HeaderContext;

Expand All @@ -21,3 +27,29 @@ fn get_border_style(focus_color: &Color, normal_color: &Color, focus: bool) -> (

(color.style, border_type)
}

pub fn start() -> Result<Terminal<CrosstermBackend<Stdout>>> {
terminal::enable_raw_mode().context("enable terminal raw mode")?;
let mut stdout = std::io::stdout();
crossterm::execute!(
stdout,
terminal::EnterAlternateScreen,
event::EnableMouseCapture
)
.context("execute terminal commands for stdout")?;

let terminal = Terminal::new(CrosstermBackend::new(stdout)).context("init terminal")?;
Ok(terminal)
}

pub fn restore(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
terminal::disable_raw_mode().context("disable terminal raw mode")?;
crossterm::execute!(
terminal.backend_mut(),
terminal::LeaveAlternateScreen,
event::DisableMouseCapture
)
.context("execute terminal commands")?;
terminal.show_cursor().context("restore terminal cursor")?;
Ok(())
}