use crossterm::event::{KeyCode, KeyModifiers};
#[derive(Clone, Debug)]
pub struct KeyBinding {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyBinding {
pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
pub fn matches(&self, code: KeyCode, modifiers: KeyModifiers) -> bool {
self.code == code && modifiers.contains(self.modifiers)
}
}
fn key(code: KeyCode) -> KeyBinding {
KeyBinding::new(code, KeyModifiers::NONE)
}
fn alt(code: KeyCode) -> KeyBinding {
KeyBinding::new(code, KeyModifiers::ALT)
}
fn alt_shift(code: KeyCode) -> KeyBinding {
KeyBinding::new(code, KeyModifiers::ALT.union(KeyModifiers::SHIFT))
}
fn shift(code: KeyCode) -> KeyBinding {
KeyBinding::new(code, KeyModifiers::SHIFT)
}
pub fn matches_any(bindings: &[KeyBinding], code: KeyCode, modifiers: KeyModifiers) -> bool {
bindings.iter().any(|b| b.matches(code, modifiers))
}
const PROFILE_NAMES: &[&str] = &["term39", "hyprland"];
const PROFILE_DISPLAY_NAMES: &[&str] = &["Term39", "Hyprland"];
#[derive(Clone, Debug)]
pub struct KeybindingProfile {
pub name: String,
pub help: Vec<KeyBinding>,
pub cycle_window: Vec<KeyBinding>,
pub save_session: Vec<KeyBinding>,
pub copy: Vec<KeyBinding>,
pub paste: Vec<KeyBinding>,
pub new_terminal: Vec<KeyBinding>,
pub new_terminal_maximized: Vec<KeyBinding>,
pub toggle_window_mode: Vec<KeyBinding>,
pub exit: Vec<KeyBinding>,
pub settings: Vec<KeyBinding>,
pub calendar: Vec<KeyBinding>,
pub about: Vec<KeyBinding>,
#[allow(dead_code)]
pub launcher: Vec<KeyBinding>,
pub lock_screen: Vec<KeyBinding>,
pub wm_focus_left: Vec<KeyBinding>,
pub wm_focus_down: Vec<KeyBinding>,
pub wm_focus_up: Vec<KeyBinding>,
pub wm_focus_right: Vec<KeyBinding>,
pub wm_snap_left: Vec<KeyBinding>,
pub wm_snap_down: Vec<KeyBinding>,
pub wm_snap_up: Vec<KeyBinding>,
pub wm_snap_right: Vec<KeyBinding>,
pub wm_enter_move: Vec<KeyBinding>,
pub wm_enter_resize: Vec<KeyBinding>,
pub wm_close: Vec<KeyBinding>,
pub wm_maximize: Vec<KeyBinding>,
pub wm_minimize: Vec<KeyBinding>,
pub wm_toggle_auto_tiling: Vec<KeyBinding>,
pub direct_close_window: Vec<KeyBinding>,
pub direct_focus_left: Vec<KeyBinding>,
pub direct_focus_down: Vec<KeyBinding>,
pub direct_focus_up: Vec<KeyBinding>,
pub direct_focus_right: Vec<KeyBinding>,
pub direct_snap_left: Vec<KeyBinding>,
pub direct_snap_down: Vec<KeyBinding>,
pub direct_snap_up: Vec<KeyBinding>,
pub direct_snap_right: Vec<KeyBinding>,
pub direct_maximize: Vec<KeyBinding>,
pub direct_toggle_auto_tiling: Vec<KeyBinding>,
pub direct_new_terminal: Vec<KeyBinding>,
pub direct_new_terminal_maximized: Vec<KeyBinding>,
pub direct_settings: Vec<KeyBinding>,
#[allow(dead_code)]
pub uses_window_mode: bool,
}
impl KeybindingProfile {
pub fn term39() -> Self {
Self {
name: "term39".to_string(),
help: vec![
key(KeyCode::F(1)),
key(KeyCode::Char('?')),
key(KeyCode::Char('h')),
],
cycle_window: vec![key(KeyCode::F(2)), alt(KeyCode::Tab)],
save_session: vec![key(KeyCode::F(3))],
copy: vec![key(KeyCode::F(5))],
paste: vec![key(KeyCode::F(6))],
new_terminal: vec![key(KeyCode::F(7)), key(KeyCode::Char('t'))],
new_terminal_maximized: vec![key(KeyCode::Char('T'))],
toggle_window_mode: vec![key(KeyCode::Char('`')), key(KeyCode::F(8))],
exit: vec![
key(KeyCode::Esc),
key(KeyCode::Char('q')),
key(KeyCode::F(10)),
],
settings: vec![key(KeyCode::Char('s'))],
calendar: vec![key(KeyCode::Char('c'))],
about: vec![key(KeyCode::Char('l'))],
launcher: vec![], lock_screen: vec![shift(KeyCode::Char('Q'))],
wm_focus_left: vec![key(KeyCode::Char('h')), key(KeyCode::Left)],
wm_focus_down: vec![key(KeyCode::Char('j')), key(KeyCode::Down)],
wm_focus_up: vec![key(KeyCode::Char('k')), key(KeyCode::Up)],
wm_focus_right: vec![key(KeyCode::Char('l')), key(KeyCode::Right)],
wm_snap_left: vec![shift(KeyCode::Char('H')), shift(KeyCode::Left)],
wm_snap_down: vec![shift(KeyCode::Char('J')), shift(KeyCode::Down)],
wm_snap_up: vec![shift(KeyCode::Char('K')), shift(KeyCode::Up)],
wm_snap_right: vec![shift(KeyCode::Char('L')), shift(KeyCode::Right)],
wm_enter_move: vec![key(KeyCode::Char('m'))],
wm_enter_resize: vec![key(KeyCode::Char('r'))],
wm_close: vec![key(KeyCode::Char('x')), key(KeyCode::Char('q'))],
wm_maximize: vec![
key(KeyCode::Char('z')),
key(KeyCode::Char('+')),
key(KeyCode::Char(' ')),
],
wm_minimize: vec![key(KeyCode::Char('-')), key(KeyCode::Char('_'))],
wm_toggle_auto_tiling: vec![key(KeyCode::Char('a'))],
direct_close_window: vec![],
direct_focus_left: vec![],
direct_focus_down: vec![],
direct_focus_up: vec![],
direct_focus_right: vec![],
direct_snap_left: vec![],
direct_snap_down: vec![],
direct_snap_up: vec![],
direct_snap_right: vec![],
direct_maximize: vec![],
direct_toggle_auto_tiling: vec![],
direct_new_terminal: vec![],
direct_new_terminal_maximized: vec![],
direct_settings: vec![],
uses_window_mode: true,
}
}
pub fn hyprland() -> Self {
let mut direct_focus_left = vec![alt(KeyCode::Char('h'))];
let mut direct_focus_down = vec![alt(KeyCode::Char('j'))];
let mut direct_focus_up = vec![alt(KeyCode::Char('k'))];
let mut direct_focus_right = vec![alt(KeyCode::Char('l'))];
let mut direct_snap_left = vec![alt_shift(KeyCode::Char('H'))];
let mut direct_snap_down = vec![alt_shift(KeyCode::Char('J'))];
let mut direct_snap_up = vec![alt_shift(KeyCode::Char('K'))];
let mut direct_snap_right = vec![alt_shift(KeyCode::Char('L'))];
let mut direct_close = vec![alt(KeyCode::Char('q'))];
let mut direct_maximize = vec![alt(KeyCode::Char('f'))];
let mut direct_auto_tiling = vec![alt(KeyCode::Char('v'))];
let direct_new_term = vec![alt(KeyCode::Enter)];
let direct_new_term_max = vec![alt_shift(KeyCode::Enter)];
let mut direct_settings = vec![alt(KeyCode::Char('s'))];
if cfg!(target_os = "macos") {
direct_focus_left.push(key(KeyCode::Char('Ë™')));
direct_focus_down.push(key(KeyCode::Char('∆')));
direct_focus_up.push(key(KeyCode::Char('Ëš')));
direct_focus_right.push(key(KeyCode::Char('¬')));
direct_close.push(key(KeyCode::Char('Å“')));
direct_maximize.push(key(KeyCode::Char('Æ’')));
direct_auto_tiling.push(key(KeyCode::Char('√')));
direct_settings.push(key(KeyCode::Char('ß')));
direct_snap_left.push(key(KeyCode::Char('Ó')));
direct_snap_down.push(key(KeyCode::Char('Ô')));
direct_snap_up.push(key(KeyCode::Char('\u{f8ff}'))); direct_snap_right.push(key(KeyCode::Char('Ã’')));
}
Self {
name: "hyprland".to_string(),
help: vec![key(KeyCode::F(1)), key(KeyCode::Char('?'))],
cycle_window: vec![alt(KeyCode::Tab), key(KeyCode::F(2))],
save_session: vec![key(KeyCode::F(3))],
copy: vec![key(KeyCode::F(5))],
paste: vec![key(KeyCode::F(6))],
new_terminal: vec![key(KeyCode::Char('t'))],
new_terminal_maximized: vec![key(KeyCode::Char('T'))],
toggle_window_mode: vec![key(KeyCode::F(8))], exit: vec![key(KeyCode::Esc), key(KeyCode::F(10))], settings: vec![key(KeyCode::Char('s'))],
calendar: vec![key(KeyCode::Char('c'))],
about: vec![key(KeyCode::Char('l'))],
launcher: vec![alt(KeyCode::Char(' '))],
lock_screen: vec![shift(KeyCode::Char('Q'))],
wm_focus_left: vec![key(KeyCode::Char('h')), key(KeyCode::Left)],
wm_focus_down: vec![key(KeyCode::Char('j')), key(KeyCode::Down)],
wm_focus_up: vec![key(KeyCode::Char('k')), key(KeyCode::Up)],
wm_focus_right: vec![key(KeyCode::Char('l')), key(KeyCode::Right)],
wm_snap_left: vec![shift(KeyCode::Char('H')), shift(KeyCode::Left)],
wm_snap_down: vec![shift(KeyCode::Char('J')), shift(KeyCode::Down)],
wm_snap_up: vec![shift(KeyCode::Char('K')), shift(KeyCode::Up)],
wm_snap_right: vec![shift(KeyCode::Char('L')), shift(KeyCode::Right)],
wm_enter_move: vec![key(KeyCode::Char('m'))],
wm_enter_resize: vec![key(KeyCode::Char('r'))],
wm_close: vec![key(KeyCode::Char('x')), key(KeyCode::Char('q'))],
wm_maximize: vec![
key(KeyCode::Char('z')),
key(KeyCode::Char('+')),
key(KeyCode::Char(' ')),
],
wm_minimize: vec![key(KeyCode::Char('-')), key(KeyCode::Char('_'))],
wm_toggle_auto_tiling: vec![key(KeyCode::Char('a'))],
direct_close_window: direct_close,
direct_focus_left,
direct_focus_down,
direct_focus_up,
direct_focus_right,
direct_snap_left,
direct_snap_down,
direct_snap_up,
direct_snap_right,
direct_maximize,
direct_toggle_auto_tiling: direct_auto_tiling,
direct_new_terminal: direct_new_term,
direct_new_terminal_maximized: direct_new_term_max,
direct_settings,
uses_window_mode: false,
}
}
pub fn from_name(name: &str) -> Self {
match name.to_lowercase().as_str() {
"hyprland" => Self::hyprland(),
_ => Self::term39(),
}
}
#[allow(dead_code)]
pub fn all_names() -> &'static [&'static str] {
PROFILE_NAMES
}
pub fn next_name(current: &str) -> &'static str {
let idx = PROFILE_NAMES
.iter()
.position(|&n| n == current)
.unwrap_or(0);
PROFILE_NAMES[(idx + 1) % PROFILE_NAMES.len()]
}
pub fn prev_name(current: &str) -> &'static str {
let idx = PROFILE_NAMES
.iter()
.position(|&n| n == current)
.unwrap_or(0);
if idx == 0 {
PROFILE_NAMES[PROFILE_NAMES.len() - 1]
} else {
PROFILE_NAMES[idx - 1]
}
}
pub fn display_name(name: &str) -> &'static str {
let idx = PROFILE_NAMES.iter().position(|&n| n == name).unwrap_or(0);
PROFILE_DISPLAY_NAMES[idx]
}
pub fn has_direct_bindings(&self) -> bool {
!self.direct_close_window.is_empty()
|| !self.direct_focus_left.is_empty()
|| !self.direct_new_terminal.is_empty()
}
}