diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 2da7794..de8681a 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -44,10 +44,15 @@ find = Find ## View view = View +zoom-in = Larger text +zoom-reset = Default text size +zoom-out = Smaller text +next-tab = Next tab +previous-tab = Previous tab +split-horizontal = Split horizontal +split-vertical = Split vertical +pane-toggle-maximize = Toggle maximized menu-settings = Settings... # Context menu show-headerbar = Show header bar -split-horizontal = Split Horizontal -split-vertical = Split Vertical -pane-toggle-maximize = Toggle Pane Maximized diff --git a/src/key_bind.rs b/src/key_bind.rs new file mode 100644 index 0000000..bf54cad --- /dev/null +++ b/src/key_bind.rs @@ -0,0 +1,105 @@ +use cosmic::iced::keyboard::{KeyCode, Modifiers}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt}; + +use crate::Action; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub enum Modifier { + Super, + Ctrl, + Alt, + Shift, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct KeyBind { + pub modifiers: Vec, + pub key_code: KeyCode, +} + +impl KeyBind { + pub fn matches(&self, modifiers: Modifiers, key_code: KeyCode) -> bool { + self.key_code == key_code + && modifiers.logo() == self.modifiers.contains(&Modifier::Super) + && modifiers.control() == self.modifiers.contains(&Modifier::Ctrl) + && modifiers.alt() == self.modifiers.contains(&Modifier::Alt) + && modifiers.shift() == self.modifiers.contains(&Modifier::Shift) + } +} + +impl fmt::Display for KeyBind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for modifier in self.modifiers.iter() { + write!(f, "{:?} + ", modifier)?; + } + write!(f, "{:?}", self.key_code) + } +} + +//TODO: load from config +pub fn key_binds() -> HashMap { + let mut key_binds = HashMap::new(); + + macro_rules! bind { + ([$($modifier:ident),+ $(,)?], $key_code:ident, $action:ident) => {{ + key_binds.insert( + KeyBind { + modifiers: vec![$(Modifier::$modifier),+], + key_code: KeyCode::$key_code, + }, + Action::$action, + ); + }}; + } + + // Standard key bindings + bind!([Ctrl, Shift], A, SelectAll); + bind!([Ctrl, Shift], C, Copy); + bind!([Ctrl, Shift], F, Find); + bind!([Ctrl, Shift], N, WindowNew); + bind!([Ctrl, Shift], Q, WindowClose); + bind!([Ctrl, Shift], T, TabNew); + bind!([Ctrl, Shift], V, Paste); + bind!([Shift], Insert, PastePrimary); + bind!([Ctrl, Shift], W, TabClose); + + // Ctrl+Alt+D splits horizontally, Ctrl+Alt+R splits vertically, Ctrl+Shift+X maximizes split + //TODO: Adjust bindings as desired by UX + bind!([Ctrl, Alt], D, PaneSplitHorizontal); + bind!([Ctrl, Alt], R, PaneSplitVertical); + bind!([Ctrl, Shift], X, PaneToggleMaximized); + + // Ctrl+Tab and Ctrl+Shift+Tab cycle through tabs + // Ctrl+Tab is not a special key for terminals and is free to use + bind!([Ctrl], Tab, TabNext); + bind!([Ctrl, Shift], Tab, TabPrev); + + // Ctrl+Shift+# activates tabs by index + bind!([Ctrl, Shift], Key1, TabActivate0); + bind!([Ctrl, Shift], Key2, TabActivate1); + bind!([Ctrl, Shift], Key3, TabActivate2); + bind!([Ctrl, Shift], Key4, TabActivate3); + bind!([Ctrl, Shift], Key5, TabActivate4); + bind!([Ctrl, Shift], Key6, TabActivate5); + bind!([Ctrl, Shift], Key7, TabActivate6); + bind!([Ctrl, Shift], Key8, TabActivate7); + bind!([Ctrl, Shift], Key9, TabActivate8); + + // Ctrl+0, Ctrl+-, and Ctrl+= are not special keys for terminals and are free to use + bind!([Ctrl], Key0, ZoomReset); + bind!([Ctrl], Minus, ZoomOut); + bind!([Ctrl], Equals, ZoomIn); + + // Ctrl+Arrows and Ctrl+HJKL move between splits + bind!([Ctrl, Shift], Left, PaneFocusLeft); + bind!([Ctrl, Shift], H, PaneFocusLeft); + bind!([Ctrl, Shift], Down, PaneFocusDown); + bind!([Ctrl, Shift], J, PaneFocusDown); + bind!([Ctrl, Shift], Up, PaneFocusUp); + bind!([Ctrl, Shift], K, PaneFocusUp); + bind!([Ctrl, Shift], Right, PaneFocusRight); + bind!([Ctrl, Shift], L, PaneFocusRight); + + key_binds +} diff --git a/src/main.rs b/src/main.rs index bbe6a28..b1f6faa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,9 @@ mod config; use icon_cache::IconCache; mod icon_cache; +use key_bind::{key_binds, KeyBind}; +mod key_bind; + mod localize; use menu::menu_bar; @@ -159,31 +162,77 @@ pub struct Flags { term_config: TermConfig, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { Copy, - Paste, - SelectAll, - Settings, - ShowHeaderBar(bool), - TabNew, + Find, + PaneFocusDown, + PaneFocusLeft, + PaneFocusRight, + PaneFocusUp, PaneSplitHorizontal, PaneSplitVertical, PaneToggleMaximized, + Paste, + PastePrimary, + SelectAll, + Settings, + ShowHeaderBar(bool), + TabActivate0, + TabActivate1, + TabActivate2, + TabActivate3, + TabActivate4, + TabActivate5, + TabActivate6, + TabActivate7, + TabActivate8, + TabClose, + TabNew, + TabNext, + TabPrev, + WindowClose, + WindowNew, + ZoomIn, + ZoomOut, + ZoomReset, } impl Action { - pub fn message(self, entity: segmented_button::Entity) -> Message { + pub fn message(self, entity_opt: Option) -> Message { match self { - Action::Copy => Message::Copy(Some(entity)), - Action::Paste => Message::Paste(Some(entity)), - Action::SelectAll => Message::SelectAll(Some(entity)), + Action::Copy => Message::Copy(entity_opt), + Action::Find => Message::Find(true), + Action::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down), + Action::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left), + Action::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right), + Action::PaneFocusUp => Message::PaneFocusAdjacent(pane_grid::Direction::Up), + Action::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal), + Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical), + Action::PaneToggleMaximized => Message::PaneToggleMaximized, + Action::Paste => Message::Paste(entity_opt), + Action::PastePrimary => Message::PastePrimary(entity_opt), + Action::SelectAll => Message::SelectAll(entity_opt), Action::Settings => Message::ToggleContextPage(ContextPage::Settings), Action::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(show_headerbar), + Action::TabActivate0 => Message::TabActivateJump(0), + Action::TabActivate1 => Message::TabActivateJump(1), + Action::TabActivate2 => Message::TabActivateJump(2), + Action::TabActivate3 => Message::TabActivateJump(3), + Action::TabActivate4 => Message::TabActivateJump(4), + Action::TabActivate5 => Message::TabActivateJump(5), + Action::TabActivate6 => Message::TabActivateJump(6), + Action::TabActivate7 => Message::TabActivateJump(7), + Action::TabActivate8 => Message::TabActivateJump(8), + Action::TabClose => Message::TabClose(entity_opt), Action::TabNew => Message::TabNew, - Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical), - Action::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal), - Action::PaneToggleMaximized => Message::PaneToggleMaximized, + Action::TabNext => Message::TabNext, + Action::TabPrev => Message::TabPrev, + Action::WindowClose => Message::WindowClose, + Action::WindowNew => Message::WindowNew, + Action::ZoomIn => Message::ZoomIn, + Action::ZoomOut => Message::ZoomOut, + Action::ZoomReset => Message::ZoomReset, } } } @@ -201,6 +250,7 @@ pub enum Message { DefaultDimFontWeight(usize), DefaultBoldFontWeight(usize), DefaultZoomStep(usize), + Key(Modifiers, KeyCode), Find(bool), FindNext, FindPrevious, @@ -259,6 +309,7 @@ pub struct App { pane_model: TerminalPaneGrid, config_handler: Option, config: Config, + key_binds: HashMap, app_themes: Vec, font_names: Vec, font_size_names: Vec, @@ -777,6 +828,7 @@ impl Application for App { pane_model, config_handler: flags.config_handler, config: flags.config, + key_binds: key_binds(), app_themes, font_names, font_size_names, @@ -952,6 +1004,13 @@ impl Application for App { log::warn!("failed to find zoom step with index {}", index); } }, + Message::Key(modifiers, key_code) => { + for (key_bind, action) in self.key_binds.iter() { + if key_bind.matches(modifiers, key_code) { + return self.update(action.message(None)); + } + } + } Message::Find(find) => { self.find = find; @@ -1158,7 +1217,7 @@ impl Application for App { terminal.context_menu = None; } // Run action's message - return self.update(action.message(entity)); + return self.update(action.message(Some(entity))); } _ => {} } @@ -1324,7 +1383,7 @@ impl Application for App { } fn header_start(&self) -> Vec> { - vec![menu_bar().into()] + vec![menu_bar(&self.key_binds).into()] } /// Creates a view after each update. @@ -1367,7 +1426,7 @@ impl Application for App { let tab_element: Element<'_, Message> = match context_menu { Some(position) => widget::popover( terminal_box.context_menu(position), - menu::context_menu(&self.config, entity), + menu::context_menu(&self.config, &self.key_binds, entity), ) .position(position) .into(), @@ -1462,216 +1521,9 @@ impl Application for App { Subscription::batch([ event::listen_with(|event, _status| match event { Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::A, + key_code, modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::SelectAll(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::C, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::Copy(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::F, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::Find(true)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::T, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::TabNew) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::W, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::TabClose(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: key @ (KeyCode::PageUp | KeyCode::PageDown), - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - match key { - KeyCode::PageDown => Some(Message::TabPrev), - KeyCode::PageUp => Some(Message::TabNext), - _ => None, - } - } else { - None - } - } - // Ctrl + Shift + N to jump to a tab - Event::Keyboard(KeyEvent::KeyPressed { - key_code: - key @ (KeyCode::Key1 - | KeyCode::Key2 - | KeyCode::Key3 - | KeyCode::Key4 - | KeyCode::Key5 - | KeyCode::Key6 - | KeyCode::Key7 - | KeyCode::Key8 - | KeyCode::Key9), - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - // 0 to 8 - // Key1 is 0 and Key9 is 8 - // This does not seem to be platform specific according to iced's source - let code = key as u32 as usize; - debug_assert!(code <= 8); - - Some(Message::TabActivateJump(code)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::R, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::ALT { - Some(Message::PaneSplit(pane_grid::Axis::Vertical)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::D, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::ALT { - Some(Message::PaneSplit(pane_grid::Axis::Horizontal)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::X, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::PaneToggleMaximized) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Left, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::PaneFocusAdjacent(pane_grid::Direction::Left)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Right, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::PaneFocusAdjacent(pane_grid::Direction::Right)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Up, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::PaneFocusAdjacent(pane_grid::Direction::Up)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Down, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::PaneFocusAdjacent(pane_grid::Direction::Down)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::V, - modifiers, - }) => { - if modifiers == Modifiers::CTRL | Modifiers::SHIFT { - Some(Message::Paste(None)) - } else { - None - } - } - #[cfg(target_family = "unix")] - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Insert, - modifiers, - }) => { - if modifiers == Modifiers::SHIFT { - Some(Message::PastePrimary(None)) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Equals, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::ZoomIn) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Minus, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::ZoomOut) - } else { - None - } - } - Event::Keyboard(KeyEvent::KeyPressed { - key_code: KeyCode::Key0, - modifiers, - }) => { - if modifiers == Modifiers::CTRL { - Some(Message::ZoomReset) - } else { - None - } - } + }) => Some(Message::Key(modifiers, key_code)), Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Some(Message::Modifiers(modifiers)) } diff --git a/src/menu.rs b/src/menu.rs index 5c693b0..012682c 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -14,8 +14,9 @@ use cosmic::{ }, Element, }; +use std::collections::HashMap; -use crate::{fl, Action, Config, ContextPage, Message}; +use crate::{fl, Action, Config, KeyBind, Message}; macro_rules! menu_button { ($($x:expr),+ $(,)?) => ( @@ -32,9 +33,28 @@ macro_rules! menu_button { ); } -pub fn context_menu<'a>(config: &Config, entity: segmented_button::Entity) -> Element<'a, Message> { - let menu_action = |label, action| { - menu_button!(widget::text(label)).on_press(Message::TabContextAction(entity, action)) +pub fn context_menu<'a>( + config: &Config, + key_binds: &HashMap, + entity: segmented_button::Entity, +) -> Element<'a, Message> { + let find_key = |action: &Action| -> String { + for (key_bind, key_action) in key_binds.iter() { + if action == key_action { + return key_bind.to_string(); + } + } + String::new() + }; + + let menu_item = |label, action| { + let key = find_key(&action); + menu_button!( + widget::text(label), + horizontal_space(Length::Fill), + widget::text(key) + ) + .on_press(Message::TabContextAction(entity, action)) }; let menu_checkbox = |label, value, action| { @@ -50,16 +70,16 @@ pub fn context_menu<'a>(config: &Config, entity: segmented_button::Entity) -> El }; widget::container(column!( - menu_action(fl!("copy"), Action::Copy), - menu_action(fl!("paste"), Action::Paste), - menu_action(fl!("select-all"), Action::SelectAll), + menu_item(fl!("copy"), Action::Copy), + menu_item(fl!("paste"), Action::Paste), + menu_item(fl!("select-all"), Action::SelectAll), horizontal_rule(1), - menu_action(fl!("split-horizontal"), Action::PaneSplitHorizontal), - menu_action(fl!("split-vertical"), Action::PaneSplitVertical), - menu_action(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized), + menu_item(fl!("split-horizontal"), Action::PaneSplitHorizontal), + menu_item(fl!("split-vertical"), Action::PaneSplitVertical), + menu_item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized), horizontal_rule(1), - menu_action(fl!("new-tab"), Action::TabNew), - menu_action(fl!("settings"), Action::Settings), + menu_item(fl!("new-tab"), Action::TabNew), + menu_item(fl!("menu-settings"), Action::Settings), menu_checkbox( fl!("show-headerbar"), config.show_headerbar, @@ -84,7 +104,7 @@ pub fn context_menu<'a>(config: &Config, entity: segmented_button::Entity) -> El .into() } -pub fn menu_bar<'a>() -> Element<'a, Message> { +pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message> { //TODO: port to libcosmic let menu_root = |label| { widget::button(widget::text(label)) @@ -92,20 +112,24 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { .style(theme::Button::MenuRoot) }; - let find_key = |message: &Message| -> String { - //TODO: hotkey config + let find_key = |action: &Action| -> String { + for (key_bind, key_action) in key_binds.iter() { + if action == key_action { + return key_bind.to_string(); + } + } String::new() }; - let menu_item = |label, message| { - let key = find_key(&message); + let menu_item = |label, action| { + let key = find_key(&action); MenuTree::new( menu_button!( widget::text(label), horizontal_space(Length::Fill), widget::text(key) ) - .on_press(message), + .on_press(action.message(None)), ) }; @@ -113,30 +137,40 @@ pub fn menu_bar<'a>() -> Element<'a, Message> { MenuTree::with_children( menu_root(fl!("file")), vec![ - menu_item(fl!("new-tab"), Message::TabNew), - menu_item(fl!("new-window"), Message::WindowNew), + menu_item(fl!("new-tab"), Action::TabNew), + menu_item(fl!("new-window"), Action::WindowNew), MenuTree::new(horizontal_rule(1)), - menu_item(fl!("close-tab"), Message::TabClose(None)), + menu_item(fl!("close-tab"), Action::TabClose), MenuTree::new(horizontal_rule(1)), - menu_item(fl!("quit"), Message::WindowClose), + menu_item(fl!("quit"), Action::WindowClose), ], ), MenuTree::with_children( menu_root(fl!("edit")), vec![ - menu_item(fl!("copy"), Message::Copy(None)), - menu_item(fl!("paste"), Message::Paste(None)), - menu_item(fl!("select-all"), Message::SelectAll(None)), + menu_item(fl!("copy"), Action::Copy), + menu_item(fl!("paste"), Action::Paste), + menu_item(fl!("select-all"), Action::SelectAll), MenuTree::new(horizontal_rule(1)), - menu_item(fl!("find"), Message::Find(true)), + menu_item(fl!("find"), Action::Find), ], ), MenuTree::with_children( menu_root(fl!("view")), - vec![menu_item( - fl!("menu-settings"), - Message::ToggleContextPage(ContextPage::Settings), - )], + vec![ + menu_item(fl!("zoom-in"), Action::ZoomIn), + menu_item(fl!("zoom-reset"), Action::ZoomReset), + menu_item(fl!("zoom-out"), Action::ZoomOut), + MenuTree::new(horizontal_rule(1)), + menu_item(fl!("next-tab"), Action::TabNext), + menu_item(fl!("previous-tab"), Action::TabPrev), + MenuTree::new(horizontal_rule(1)), + menu_item(fl!("split-horizontal"), Action::PaneSplitHorizontal), + menu_item(fl!("split-vertical"), Action::PaneSplitVertical), + menu_item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized), + MenuTree::new(horizontal_rule(1)), + menu_item(fl!("menu-settings"), Action::Settings), + ], ), ]) .item_height(ItemHeight::Dynamic(40))