diff --git a/Cargo.lock b/Cargo.lock index 5b8c901..250d44c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1993,7 +1993,8 @@ dependencies = [ [[package]] name = "keybindings" version = "0.0.1" -source = "git+https://github.com/ulyssa/modalkit?rev=cb8c8aeb9a499b9b16615ce144f9014d78036e01#cb8c8aeb9a499b9b16615ce144f9014d78036e01" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680e4699c91c0622dd70da32c274881aadb1ac86252d738c3641266e90e4ca15" dependencies = [ "textwrap", "unicode-segmentation", @@ -2507,8 +2508,9 @@ dependencies = [ [[package]] name = "modalkit" -version = "0.0.17" -source = "git+https://github.com/ulyssa/modalkit?rev=cb8c8aeb9a499b9b16615ce144f9014d78036e01#cb8c8aeb9a499b9b16615ce144f9014d78036e01" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d68711785c96d06bede5bd38fee2e2ac856cfccce7ea0b3e302bc4c5688010" dependencies = [ "anymap2", "arboard", @@ -2528,8 +2530,9 @@ dependencies = [ [[package]] name = "modalkit-ratatui" -version = "0.0.17" -source = "git+https://github.com/ulyssa/modalkit?rev=cb8c8aeb9a499b9b16615ce144f9014d78036e01#cb8c8aeb9a499b9b16615ce144f9014d78036e01" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747e3dc36bfc4b62a152a37b6631f471797269afa094f6ba0d7aea768be31e2b" dependencies = [ "crossterm", "intervaltree", diff --git a/Cargo.toml b/Cargo.toml index d19b9ec..1a98f84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,14 +59,14 @@ url = {version = "^2.2.2", features = ["serde"]} edit = "0.1.4" [dependencies.modalkit] -version = "0.0.17" -git = "https://github.com/ulyssa/modalkit" -rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01" +version = "0.0.18" +#git = "https://github.com/ulyssa/modalkit" +#rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01" [dependencies.modalkit-ratatui] -version = "0.0.17" -git = "https://github.com/ulyssa/modalkit" -rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01" +version = "0.0.18" +#git = "https://github.com/ulyssa/modalkit" +#rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01" [dependencies.matrix-sdk] version = "0.7.1" diff --git a/docs/example_config.json b/docs/example_config.json index baf45e0..812d5a7 100644 --- a/docs/example_config.json +++ b/docs/example_config.json @@ -2,7 +2,7 @@ "default_profile": "default", "profiles": { "default": { - "user_id": "", + "user_id": "@user:matrix.org", "url": "https://matrix.org", "settings": {}, "dirs": {} @@ -34,6 +34,14 @@ } } }, + "layout": { + "style": "restore" + }, + "macros": { + "i": { + "jj": "" + } + }, "dirs": { "cache": "/home/user/.cache/iamb/", "logs": "/home/user/.local/share/iamb/logs/", diff --git a/flake.nix b/flake.nix index 22e29b1..ec4a5b3 100644 --- a/flake.nix +++ b/flake.nix @@ -24,9 +24,6 @@ src = ./.; cargoLock = { lockFile = ./Cargo.lock; - outputHashes = { - "keybindings-0.0.1" = "sha256-6gGviJF4/gzoPxgh0XJXXrhQoWxOTqyI9fwiOE+TY7s="; - }; }; nativeBuildInputs = [ pkg-config ]; buildInputs = [ openssl ] ++ lib.optionals stdenv.isDarwin diff --git a/src/config.rs b/src/config.rs index d3d3fe2..ca3da65 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,8 @@ use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer, Ser use tracing::Level; use url::Url; +use modalkit::{env::vim::VimMode, key::TerminalKey, keybindings::InputKey}; + use super::base::{ IambError, IambId, @@ -29,6 +31,8 @@ use super::base::{ SortOrder, }; +type Macros = HashMap>; + macro_rules! usage { ( $($args: tt)* ) => { println!($($args)*); @@ -136,6 +140,81 @@ pub enum ConfigError { Invalid(#[from] serde_json::Error), } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Keys(pub Vec, pub String); +pub struct KeysVisitor; + +impl<'de> Visitor<'de> for KeysVisitor { + type Value = Keys; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid Vim mode (e.g. \"normal\" or \"insert\")") + } + + fn visit_str(self, value: &str) -> Result + where + E: SerdeError, + { + match TerminalKey::from_macro_str(value) { + Ok(keys) => Ok(Keys(keys, value.to_string())), + Err(e) => Err(E::custom(format!("Could not parse key sequence: {e}"))), + } + } +} + +impl<'de> Deserialize<'de> for Keys { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(KeysVisitor) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VimModes(pub Vec); +pub struct VimModesVisitor; + +impl<'de> Visitor<'de> for VimModesVisitor { + type Value = VimModes; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid Vim mode (e.g. \"normal\" or \"insert\")") + } + + fn visit_str(self, value: &str) -> Result + where + E: SerdeError, + { + let mut modes = vec![]; + + for mode in value.split('|') { + let mode = match mode.to_ascii_lowercase().as_str() { + "insert" | "i" => VimMode::Insert, + "normal" | "n" => VimMode::Normal, + "visual" | "v" => VimMode::Visual, + "command" | "c" => VimMode::Command, + "select" => VimMode::Select, + "operator-pending" => VimMode::OperationPending, + _ => return Err(E::custom("Could not parse into a Vim mode")), + }; + + modes.push(mode); + } + + Ok(VimModes(modes)) + } +} + +impl<'de> Deserialize<'de> for VimModes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(VimModesVisitor) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct LogLevel(pub Level); pub struct LogLevelVisitor; @@ -276,7 +355,10 @@ fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides { } } -fn merge_users(a: Option, b: Option) -> Option { +fn merge_maps(a: Option>, b: Option>) -> Option> +where + K: Eq + Hash, +{ match (a, b) { (Some(a), None) => Some(a), (None, Some(b)) => Some(b), @@ -431,7 +513,7 @@ impl Tunables { sort: merge_sorts(self.sort, other.sort), typing_notice_send: self.typing_notice_send.or(other.typing_notice_send), typing_notice_display: self.typing_notice_display.or(other.typing_notice_display), - users: merge_users(self.users, other.users), + users: merge_maps(self.users, other.users), username_display: self.username_display.or(other.username_display), message_user_color: self.message_user_color.or(other.message_user_color), default_room: self.default_room.or(other.default_room), @@ -583,6 +665,7 @@ pub struct ProfileConfig { pub settings: Option, pub dirs: Option, pub layout: Option, + pub macros: Option, } #[derive(Clone, Deserialize)] @@ -592,6 +675,7 @@ pub struct IambConfig { pub settings: Option, pub dirs: Option, pub layout: Option, + pub macros: Option, } impl IambConfig { @@ -624,6 +708,7 @@ pub struct ApplicationSettings { pub tunables: TunableValues, pub dirs: DirectoryValues, pub layout: Layout, + pub macros: Macros, } impl ApplicationSettings { @@ -645,6 +730,7 @@ impl ApplicationSettings { dirs, settings: global, layout, + macros, } = IambConfig::load(config_json.as_path())?; validate_profile_names(&profiles); @@ -668,6 +754,7 @@ impl ApplicationSettings { ); }; + let macros = merge_maps(macros, profile.macros.take()).unwrap_or_default(); let layout = profile.layout.take().or(layout).unwrap_or_default(); let tunables = global.unwrap_or_default(); @@ -721,6 +808,7 @@ impl ApplicationSettings { tunables, dirs, layout, + macros, }; Ok(settings) @@ -851,22 +939,22 @@ mod tests { .into_iter() .collect::>(); - let res = merge_users(a.clone(), a.clone()); + let res = merge_maps(a.clone(), a.clone()); assert_eq!(res, None); - let res = merge_users(a.clone(), Some(b.clone())); + let res = merge_maps(a.clone(), Some(b.clone())); assert_eq!(res, Some(b.clone())); - let res = merge_users(Some(b.clone()), a.clone()); + let res = merge_maps(Some(b.clone()), a.clone()); assert_eq!(res, Some(b.clone())); - let res = merge_users(Some(b.clone()), Some(b.clone())); + let res = merge_maps(Some(b.clone()), Some(b.clone())); assert_eq!(res, Some(b.clone())); - let res = merge_users(Some(b.clone()), Some(c.clone())); + let res = merge_maps(Some(b.clone()), Some(c.clone())); assert_eq!(res, Some(c.clone())); - let res = merge_users(Some(c.clone()), Some(b.clone())); + let res = merge_maps(Some(c.clone()), Some(b.clone())); assert_eq!(res, Some(b.clone())); } @@ -1012,4 +1100,45 @@ mod tests { let tabs = vec![split1, split3]; assert_eq!(res, Layout::Config { tabs }); } + + #[test] + fn test_parse_macros() { + let res: Macros = serde_json::from_str("{\"i|c\":{\"jj\":\"\"}}").unwrap(); + assert_eq!(res.len(), 1); + + let modes = VimModes(vec![VimMode::Insert, VimMode::Command]); + let mapped = res.get(&modes).unwrap(); + assert_eq!(mapped.len(), 1); + + let j = "j".parse::().unwrap(); + let esc = "".parse::().unwrap(); + + let jj = Keys(vec![j.clone(), j], "jj".into()); + let run = mapped.get(&jj).unwrap(); + let exp = Keys(vec![esc], "".into()); + assert_eq!(run, &exp); + } + + #[test] + fn test_load_example_config_json() { + let path = PathBuf::from("docs/example_config.json"); + let config = IambConfig::load(&path).expect("can load example_config.json"); + + let IambConfig { + profiles, + default_profile, + settings, + dirs, + layout, + macros, + } = config; + + // There should be an example object for each top-level field. + assert!(!profiles.is_empty()); + assert!(default_profile.is_some()); + assert!(settings.is_some()); + assert!(dirs.is_some()); + assert!(layout.is_some()); + assert!(macros.is_some()); + } } diff --git a/src/keybindings.rs b/src/keybindings.rs index 1c10bf8..15132b2 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -3,16 +3,23 @@ //! The keybindings are set up here. We define some iamb-specific keybindings, but the default Vim //! keys come from [modalkit::env::vim::keybindings]. use modalkit::{ - actions::WindowAction, + actions::{MacroAction, WindowAction}, env::vim::keybindings::{InputStep, VimBindings}, env::vim::VimMode, + env::CommonKeyClass, key::TerminalKey, keybindings::{EdgeEvent, EdgeRepeat, InputBindings}, + prelude::Count, }; use crate::base::{IambAction, IambInfo, Keybindings, MATRIX_ID_WORD}; +use crate::config::{ApplicationSettings, Keys}; -type IambStep = InputStep; +pub type IambStep = InputStep; + +fn once(key: &TerminalKey) -> (EdgeRepeat, EdgeEvent) { + (EdgeRepeat::Once, EdgeEvent::Key(*key)) +} /// Initialize the default keybinding state. pub fn setup_keybindings() -> Keybindings { @@ -24,20 +31,14 @@ pub fn setup_keybindings() -> Keybindings { vim.setup(&mut ism); - let ctrl_w = EdgeEvent::Key("".parse::().unwrap()); - let ctrl_m = EdgeEvent::Key("".parse::().unwrap()); - let ctrl_z = EdgeEvent::Key("".parse::().unwrap()); - let key_m_lc = EdgeEvent::Key("m".parse::().unwrap()); - let key_z_lc = EdgeEvent::Key("z".parse::().unwrap()); + let ctrl_w = "".parse::().unwrap(); + let ctrl_m = "".parse::().unwrap(); + let ctrl_z = "".parse::().unwrap(); + let key_m_lc = "m".parse::().unwrap(); + let key_z_lc = "z".parse::().unwrap(); - let cwz = vec![ - (EdgeRepeat::Once, ctrl_w.clone()), - (EdgeRepeat::Once, key_z_lc), - ]; - let cwcz = vec![ - (EdgeRepeat::Once, ctrl_w.clone()), - (EdgeRepeat::Once, ctrl_z), - ]; + let cwz = vec![once(&ctrl_w), once(&key_z_lc)]; + let cwcz = vec![once(&ctrl_w), once(&ctrl_z)]; let zoom = IambStep::new() .actions(vec![WindowAction::ZoomToggle.into()]) .goto(VimMode::Normal); @@ -47,11 +48,8 @@ pub fn setup_keybindings() -> Keybindings { ism.add_mapping(VimMode::Normal, &cwcz, &zoom); ism.add_mapping(VimMode::Visual, &cwcz, &zoom); - let cwm = vec![ - (EdgeRepeat::Once, ctrl_w.clone()), - (EdgeRepeat::Once, key_m_lc), - ]; - let cwcm = vec![(EdgeRepeat::Once, ctrl_w), (EdgeRepeat::Once, ctrl_m)]; + let cwm = vec![once(&ctrl_w), once(&key_m_lc)]; + let cwcm = vec![once(&ctrl_w), once(&ctrl_m)]; let stoggle = IambStep::new() .actions(vec![IambAction::ToggleScrollbackFocus.into()]) .goto(VimMode::Normal); @@ -59,6 +57,21 @@ pub fn setup_keybindings() -> Keybindings { ism.add_mapping(VimMode::Visual, &cwm, &stoggle); ism.add_mapping(VimMode::Normal, &cwcm, &stoggle); ism.add_mapping(VimMode::Visual, &cwcm, &stoggle); - - return ism; + ism +} + +impl InputBindings for ApplicationSettings { + fn setup(&self, bindings: &mut Keybindings) { + for (modes, keys) in &self.macros { + for (Keys(input, _), Keys(_, run)) in keys { + let act = MacroAction::Run(run.clone(), Count::Contextual); + let step = IambStep::new().actions(vec![act.into()]); + let input = input.iter().map(once).collect::>(); + + for mode in &modes.0 { + bindings.add_mapping(*mode, &input, &step); + } + } + } + } } diff --git a/src/main.rs b/src/main.rs index 6e5494f..fc38227 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ use std::time::Duration; use clap::Parser; use matrix_sdk::crypto::encrypt_room_key_export; use matrix_sdk::ruma::OwnedUserId; +use modalkit::keybindings::InputBindings; use rand::{distributions::Alphanumeric, Rng}; use temp_dir::TempDir; use tokio::sync::Mutex as AsyncMutex; @@ -257,7 +258,8 @@ impl Application { let backend = CrosstermBackend::new(stdout); let terminal = Terminal::new(backend)?; - let bindings = crate::keybindings::setup_keybindings(); + let mut bindings = crate::keybindings::setup_keybindings(); + settings.setup(&mut bindings); let bindings = KeyManager::new(bindings); let mut locked = store.lock().await; diff --git a/src/tests.rs b/src/tests.rs index 645e5c9..f4ff074 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -217,10 +217,12 @@ pub fn mock_settings() -> ApplicationSettings { settings: None, dirs: None, layout: None, + macros: None, }, tunables: mock_tunables(), dirs: mock_dirs(), layout: Default::default(), + macros: HashMap::default(), } }