diff --git a/Cargo.lock b/Cargo.lock index 9d7d2bed..832f20bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,6 +257,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "derive-ahk" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dirs" version = "3.0.2" @@ -544,8 +553,10 @@ dependencies = [ "bindings", "clap", "color-eyre", + "derive-ahk", "dirs", "fs-tail", + "heck", "komorebi-core", "paste", "powershell_script", diff --git a/Cargo.toml b/Cargo.toml index ef752337..512d9e7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "bindings", + "derive-ahk", "komorebi", "komorebi-core", "komorebic" diff --git a/README.md b/README.md index 45ae1b69..30acc6ba 100644 --- a/README.md +++ b/README.md @@ -213,9 +213,16 @@ manage-rule Add a rule to always manage the specified applicati workspace-rule Add a rule to associate an application with a workspace identify-tray-application Identify an application that closes to the system tray focus-follows-mouse Enable or disable focus follows mouse for the operating system +ahk-lib Generate a library of AutoHotKey helper functions help Print this message or the help of the given subcommand(s) ``` +Additionally, you may run `komorebic.exe ahk-lib` to generate [a helper library for AutoHotKey](komorebic.lib.sample.ahk) +which wraps every `komorebic` command in a native AHK function. + +If you include the generated library at the top of your `~/komorebi.ahk` configuration file, you will be able to call +any of the functions that it contains. + ## Features - [x] Multi-monitor @@ -248,6 +255,7 @@ help Print this message or the help of the given subcomm - [x] Load configuration on startup - [x] Manually reload configuration - [x] Watch configuration for changes +- [x] Helper library for AutoHotKey - [x] View window manager state ## Development diff --git a/derive-ahk/Cargo.toml b/derive-ahk/Cargo.toml new file mode 100644 index 00000000..87036425 --- /dev/null +++ b/derive-ahk/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "derive-ahk" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = "1.0" +syn = "1.0" +quote = "1.0" \ No newline at end of file diff --git a/derive-ahk/src/lib.rs b/derive-ahk/src/lib.rs new file mode 100644 index 00000000..e7836991 --- /dev/null +++ b/derive-ahk/src/lib.rs @@ -0,0 +1,100 @@ +#![warn(clippy::all, clippy::nursery, clippy::pedantic)] +#![allow(clippy::missing_errors_doc)] + +use std::stringify; + +use quote::quote; +use syn::parse_macro_input; +use syn::Data; +use syn::DataEnum; +use syn::DeriveInput; +use syn::Fields; +use syn::FieldsNamed; +use syn::FieldsUnnamed; + +#[proc_macro_derive(Ahk)] +pub fn ahk(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + match input.data { + Data::Struct(s) => match s.fields { + Fields::Named(FieldsNamed { named, .. }) => { + let idents = named.iter().map(|f| &f.ident); + let arguments = format!("{}", quote! {#(#idents), *}); + + let idents = named.iter().map(|f| &f.ident); + let called_arguments = format!("{}", quote! {#(%#idents%) *}) + .replace(" %", "%") + .replace("% ", "%") + .replace("%%", "% %"); + + quote! { + impl #name { + fn ahk_function() -> String { + format!(r#" +{}({}) {{ + Run, komorebic.exe {} {}, , Hide +}}"#, + stringify!(#name), + #arguments, + stringify!(#name).to_kebab_case(), + #called_arguments + ) + } + } + } + } + _ => unreachable!("only to be used on structs with named fields"), + }, + Data::Enum(DataEnum { variants, .. }) => { + let enums = variants.iter().filter(|&v| { + matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..)) + }); + + let mut stream = proc_macro2::TokenStream::new(); + + for variant in enums.clone() { + match &variant.fields { + Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { + for field in unnamed { + stream.extend(quote! { + v.push(#field::ahk_function()); + }); + } + } + Fields::Unit => { + let name = &variant.ident; + stream.extend(quote! { + v.push(format!(r#" +{}() {{ + Run, komorebic.exe {}, , Hide +}}"#, + stringify!(#name), + stringify!(#name).to_kebab_case() + )); + }); + } + Fields::Named(_) => { + unreachable!("only to be used with unnamed and unit fields"); + } + } + } + + quote! { + impl #name { + fn ahk_functions() -> Vec { + let mut v: Vec = vec![]; + v.push(String::from("; Generated by komorebic.exe ahk-lib")); + + #stream + + v + } + } + } + } + Data::Union(_) => unreachable!("only to be used on enums and structs"), + } + .into() +} diff --git a/komorebi-core/Cargo.toml b/komorebi-core/Cargo.toml index 12172084..82263c91 100644 --- a/komorebi-core/Cargo.toml +++ b/komorebi-core/Cargo.toml @@ -12,4 +12,4 @@ clap = "3.0.0-beta.4" color-eyre = "0.5" serde = { version = "1", features = ["derive"] } serde_json = "1" -strum = { version = "0.21", features = ["derive"] } +strum = { version = "0.21", features = ["derive"] } \ No newline at end of file diff --git a/komorebic.lib.sample.ahk b/komorebic.lib.sample.ahk new file mode 100644 index 00000000..c484aed0 --- /dev/null +++ b/komorebic.lib.sample.ahk @@ -0,0 +1,173 @@ +; Generated by komorebic.exe generate-ahk-library + +Start() { + Run, komorebic.exe start, , Hide +} + +Stop() { + Run, komorebic.exe stop, , Hide +} + +State() { + Run, komorebic.exe state, , Hide +} + +Log() { + Run, komorebic.exe log, , Hide +} + +Focus(operation_direction) { + Run, komorebic.exe focus %operation_direction%, , Hide +} + +Move(operation_direction) { + Run, komorebic.exe move %operation_direction%, , Hide +} + +Stack(operation_direction) { + Run, komorebic.exe stack %operation_direction%, , Hide +} + +Resize(edge, sizing) { + Run, komorebic.exe resize %edge% %sizing%, , Hide +} + +Unstack() { + Run, komorebic.exe unstack, , Hide +} + +CycleStack(cycle_direction) { + Run, komorebic.exe cycle-stack %cycle_direction%, , Hide +} + +MoveToMonitor(target) { + Run, komorebic.exe move-to-monitor %target%, , Hide +} + +MoveToWorkspace(target) { + Run, komorebic.exe move-to-workspace %target%, , Hide +} + +FocusMonitor(target) { + Run, komorebic.exe focus-monitor %target%, , Hide +} + +FocusWorkspace(target) { + Run, komorebic.exe focus-workspace %target%, , Hide +} + +NewWorkspace() { + Run, komorebic.exe new-workspace, , Hide +} + +AdjustContainerPadding(sizing, adjustment) { + Run, komorebic.exe adjust-container-padding %sizing% %adjustment%, , Hide +} + +AdjustWorkspacePadding(sizing, adjustment) { + Run, komorebic.exe adjust-workspace-padding %sizing% %adjustment%, , Hide +} + +ChangeLayout(layout) { + Run, komorebic.exe change-layout %layout%, , Hide +} + +FlipLayout(flip) { + Run, komorebic.exe flip-layout %flip%, , Hide +} + +Promote() { + Run, komorebic.exe promote, , Hide +} + +Retile() { + Run, komorebic.exe retile, , Hide +} + +EnsureWorkspaces(monitor, workspace_count) { + Run, komorebic.exe ensure-workspaces %monitor% %workspace_count%, , Hide +} + +ContainerPadding(monitor, workspace, size) { + Run, komorebic.exe container-padding %monitor% %workspace% %size%, , Hide +} + +WorkspacePadding(monitor, workspace, size) { + Run, komorebic.exe workspace-padding %monitor% %workspace% %size%, , Hide +} + +WorkspaceLayout(monitor, workspace, value) { + Run, komorebic.exe workspace-layout %monitor% %workspace% %value%, , Hide +} + +WorkspaceTiling(monitor, workspace, value) { + Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide +} + +WorkspaceName(monitor, workspace, value) { + Run, komorebic.exe workspace-name %monitor% %workspace% %value%, , Hide +} + +TogglePause() { + Run, komorebic.exe toggle-pause, , Hide +} + +ToggleTiling() { + Run, komorebic.exe toggle-tiling, , Hide +} + +ToggleFloat() { + Run, komorebic.exe toggle-float, , Hide +} + +ToggleMonocle() { + Run, komorebic.exe toggle-monocle, , Hide +} + +ToggleMaximize() { + Run, komorebic.exe toggle-maximize, , Hide +} + +RestoreWindows() { + Run, komorebic.exe restore-windows, , Hide +} + +Manage() { + Run, komorebic.exe manage, , Hide +} + +Unmanage() { + Run, komorebic.exe unmanage, , Hide +} + +ReloadConfiguration() { + Run, komorebic.exe reload-configuration, , Hide +} + +WatchConfiguration(boolean_state) { + Run, komorebic.exe watch-configuration %boolean_state%, , Hide +} + +FloatRule(identifier, id) { + Run, komorebic.exe float-rule %identifier% %id%, , Hide +} + +ManageRule(identifier, id) { + Run, komorebic.exe manage-rule %identifier% %id%, , Hide +} + +WorkspaceRule(identifier, id, monitor, workspace) { + Run, komorebic.exe workspace-rule %identifier% %id% %monitor% %workspace%, , Hide +} + +IdentifyTrayApplication(identifier, id) { + Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide +} + +FocusFollowsMouse(boolean_state) { + Run, komorebic.exe focus-follows-mouse %boolean_state%, , Hide +} + +AhkLib() { + Run, komorebic.exe ahk-lib, , Hide +} \ No newline at end of file diff --git a/komorebic/Cargo.toml b/komorebic/Cargo.toml index bb9eb86d..13dbf167 100644 --- a/komorebic/Cargo.toml +++ b/komorebic/Cargo.toml @@ -12,14 +12,16 @@ edition = "2018" [dependencies] bindings = { package = "bindings", path = "../bindings" } +derive-ahk = { path = "../derive-ahk" } komorebi-core = { path = "../komorebi-core" } clap = "3.0.0-beta.4" color-eyre = "0.5" dirs = "3" fs-tail = "0.1" +heck = "0.3" paste = "1" powershell_script = "0.2" serde = { version = "1", features = ["derive"] } serde_json = "1" -uds_windows = "1" +uds_windows = "1" \ No newline at end of file diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index ca658e7e..80054fe8 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -2,12 +2,14 @@ #![allow(clippy::missing_errors_doc)] use std::fs::File; +use std::fs::OpenOptions; use std::io::BufRead; use std::io::BufReader; use std::io::ErrorKind; use std::io::Write; use std::path::PathBuf; use std::process::Command; +use std::stringify; use clap::AppSettings; use clap::ArgEnum; @@ -15,6 +17,7 @@ use clap::Clap; use color_eyre::eyre::ContextCompat; use color_eyre::Result; use fs_tail::TailedFile; +use heck::KebabCase; use paste::paste; use uds_windows::UnixListener; use uds_windows::UnixStream; @@ -23,6 +26,7 @@ use bindings::Windows::Win32::Foundation::HWND; use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow; use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD; use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE; +use derive_ahk::Ahk; use komorebi_core::ApplicationIdentifier; use komorebi_core::CycleDirection; use komorebi_core::Flip; @@ -51,7 +55,7 @@ macro_rules! gen_enum_subcommand_args { ( $( $name:ident: $element:ty ),+ ) => { $( paste! { - #[derive(clap::Clap)] + #[derive(clap::Clap, derive_ahk::Ahk)] pub struct $name { #[clap(arg_enum)] [<$element:snake>]: $element @@ -67,7 +71,7 @@ gen_enum_subcommand_args! { Stack: OperationDirection, CycleStack: CycleDirection, FlipLayout: Flip, - SetLayout: Layout, + ChangeLayout: Layout, WatchConfiguration: BooleanState, FocusFollowsMouse: BooleanState } @@ -76,7 +80,7 @@ macro_rules! gen_target_subcommand_args { // SubCommand Pattern ( $( $name:ident ),+ ) => { $( - #[derive(clap::Clap)] + #[derive(clap::Clap, derive_ahk::Ahk)] pub struct $name { /// Target index (zero-indexed) target: usize, @@ -100,7 +104,7 @@ macro_rules! gen_workspace_subcommand_args { ( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ ) => ( paste! { $( - #[derive(clap::Clap)] + #[derive(clap::Clap, derive_ahk::Ahk)] pub struct [] { /// Monitor index (zero-indexed) monitor: usize, @@ -126,7 +130,7 @@ gen_workspace_subcommand_args! { Tiling: #[enum] BooleanState } -#[derive(Clap)] +#[derive(Clap, Ahk)] struct Resize { #[clap(arg_enum)] edge: OperationDirection, @@ -134,7 +138,7 @@ struct Resize { sizing: Sizing, } -#[derive(Clap)] +#[derive(Clap, Ahk)] struct EnsureWorkspaces { /// Monitor index (zero-indexed) monitor: usize, @@ -142,33 +146,70 @@ struct EnsureWorkspaces { workspace_count: usize, } -#[derive(Clap)] -struct Padding { - /// Monitor index (zero-indexed) - monitor: usize, - /// Workspace index on the specified monitor (zero-indexed) - workspace: usize, - /// Pixels to pad with as an integer - size: i32, +macro_rules! gen_padding_subcommand_args { + // SubCommand Pattern + ( $( $name:ident ),+ ) => { + $( + #[derive(clap::Clap, derive_ahk::Ahk)] + pub struct $name { + /// Monitor index (zero-indexed) + monitor: usize, + /// Workspace index on the specified monitor (zero-indexed) + workspace: usize, + /// Pixels to pad with as an integer + size: i32, + } + )+ + }; } -#[derive(Clap)] -struct PaddingAdjustment { - #[clap(arg_enum)] - sizing: Sizing, - /// Pixels to adjust by as an integer - adjustment: i32, +gen_padding_subcommand_args! { + ContainerPadding, + WorkspacePadding } -#[derive(Clap)] -struct ApplicationTarget { - #[clap(arg_enum)] - identifier: ApplicationIdentifier, - /// Identifier as a string - id: String, +macro_rules! gen_padding_adjustment_subcommand_args { + // SubCommand Pattern + ( $( $name:ident ),+ ) => { + $( + #[derive(clap::Clap, derive_ahk::Ahk)] + pub struct $name { + #[clap(arg_enum)] + sizing: Sizing, + /// Pixels to adjust by as an integer + adjustment: i32, + } + )+ + }; } -#[derive(Clap)] +gen_padding_adjustment_subcommand_args! { + AdjustContainerPadding, + AdjustWorkspacePadding +} + +macro_rules! gen_application_target_subcommand_args { + // SubCommand Pattern + ( $( $name:ident ),+ ) => { + $( + #[derive(clap::Clap, derive_ahk::Ahk)] + pub struct $name { + #[clap(arg_enum)] + identifier: ApplicationIdentifier, + /// Identifier as a string + id: String, + } + )+ + }; +} + +gen_application_target_subcommand_args! { + FloatRule, + ManageRule, + IdentifyTrayApplication +} + +#[derive(Clap, Ahk)] struct WorkspaceRule { #[clap(arg_enum)] identifier: ApplicationIdentifier, @@ -187,7 +228,7 @@ struct Opts { subcmd: SubCommand, } -#[derive(Clap)] +#[derive(Clap, Ahk)] enum SubCommand { /// Start komorebi.exe as a background process Start, @@ -230,12 +271,12 @@ enum SubCommand { NewWorkspace, /// Adjust container padding on the focused workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] - AdjustContainerPadding(PaddingAdjustment), + AdjustContainerPadding(AdjustContainerPadding), /// Adjust workspace padding on the focused workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] - AdjustWorkspacePadding(PaddingAdjustment), + AdjustWorkspacePadding(AdjustWorkspacePadding), /// Set the layout on the focused workspace - ChangeLayout(SetLayout), + ChangeLayout(ChangeLayout), /// Flip the layout on the focused workspace (BSP only) FlipLayout(FlipLayout), /// Promote the focused window to the top of the tree @@ -247,10 +288,10 @@ enum SubCommand { EnsureWorkspaces(EnsureWorkspaces), /// Set the container padding for the specified workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] - ContainerPadding(Padding), + ContainerPadding(ContainerPadding), /// Set the workspace padding for the specified workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] - WorkspacePadding(Padding), + WorkspacePadding(WorkspacePadding), /// Set the layout for the specified workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] WorkspaceLayout(WorkspaceLayout), @@ -283,18 +324,20 @@ enum SubCommand { WatchConfiguration(WatchConfiguration), /// Add a rule to always float the specified application #[clap(setting = AppSettings::ArgRequiredElseHelp)] - FloatRule(ApplicationTarget), + FloatRule(FloatRule), /// Add a rule to always manage the specified application #[clap(setting = AppSettings::ArgRequiredElseHelp)] - ManageRule(ApplicationTarget), + ManageRule(ManageRule), /// Add a rule to associate an application with a workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] WorkspaceRule(WorkspaceRule), /// Identify an application that closes to the system tray #[clap(setting = AppSettings::ArgRequiredElseHelp)] - IdentifyTrayApplication(ApplicationTarget), + IdentifyTrayApplication(IdentifyTrayApplication), /// Enable or disable focus follows mouse for the operating system FocusFollowsMouse(FocusFollowsMouse), + /// Generate a library of AutoHotKey helper functions + AhkLib, } pub fn send_message(bytes: &[u8]) -> Result<()> { @@ -311,6 +354,31 @@ fn main() -> Result<()> { let opts: Opts = Opts::parse(); match opts.subcmd { + SubCommand::AhkLib => { + let mut library = dirs::home_dir().context("there is no home directory")?; + library.push("komorebic.lib.ahk"); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(library.clone())?; + + let library_text: String = SubCommand::ahk_functions().join("\n"); + file.write_all(library_text.as_bytes())?; + + println!( + "\nAHK helper library for komorebic written to {}", + library + .to_str() + .context("could not find the path to the generated ahk lib file")? + ); + + println!( + "\nYou can include the library at the top of your ~/komorebi.ahk config with this line:" + ); + + println!("\n#Include %A_ScriptDir%\\komorebic.lib.ahk"); + } SubCommand::Log => { let mut color_log = std::env::temp_dir(); color_log.push("komorebi.log");