diff options
-rw-r--r-- | README.md | 100 | ||||
-rw-r--r-- | examples/skhdrc | 161 | ||||
-rw-r--r-- | makefile | 5 | ||||
-rw-r--r-- | src/carbon.c | 77 | ||||
-rw-r--r-- | src/carbon.h | 18 | ||||
-rw-r--r-- | src/hashtable.h | 13 | ||||
-rw-r--r-- | src/hotkey.c | 142 | ||||
-rw-r--r-- | src/hotkey.h | 44 | ||||
-rw-r--r-- | src/hotload.c | 288 | ||||
-rw-r--r-- | src/hotload.h | 16 | ||||
-rw-r--r-- | src/locale.c | 112 | ||||
-rw-r--r-- | src/locale.h | 10 | ||||
-rw-r--r-- | src/log.h | 42 | ||||
-rw-r--r-- | src/parse.c | 308 | ||||
-rw-r--r-- | src/parse.h | 25 | ||||
-rw-r--r-- | src/sbuffer.h | 6 | ||||
-rw-r--r-- | src/skhd.c | 364 | ||||
-rw-r--r-- | src/synthesize.c | 93 | ||||
-rw-r--r-- | src/synthesize.h | 7 | ||||
-rw-r--r-- | src/timing.h | 76 | ||||
-rw-r--r-- | src/tokenize.c | 48 | ||||
-rw-r--r-- | src/tokenize.h | 41 |
22 files changed, 1581 insertions, 415 deletions
@@ -1,9 +1,13 @@ [![Build Status](https://travis-ci.org/koekeishiya/skhd.svg?branch=master)](https://travis-ci.org/koekeishiya/skhd) -**skhd** is a simple hotkey daemon for macOS. It is a stripped version of [**khd**](https://github.com/koekeishiya/khd) +**skhd** is a simple hotkey daemon for macOS. It is a stripped version of [**khd** (*no longer maintained*)](https://github.com/koekeishiya/khd) (although rewritten from scratch), that sacrifices the more advanced features in favour of increased responsiveness and performance. **skhd** is able to hotload its config file, meaning that hotkeys can be edited and updated live while **skhd** is running. +**skhd** uses a pid-file to make sure that only one instance is running at any moment in time. This also allows for the ability to trigger +a manual reload of the config file by invoking `skhd --reload` at any time while an instance of **skhd** is running. The pid-file is saved +as `/tmp/skhd_$USER.pid` and so the user that is running **skhd** must have write permission to said path. + feature comparison between **skhd** and **khd** | feature | skhd | khd | @@ -11,8 +15,9 @@ feature comparison between **skhd** and **khd** | hotload config file | [x] | [ ] | | hotkey passthrough | [x] | [x] | | modal hotkey-system | [x] | [x] | +| application specific hotkey| [x] | [x] | +| blacklist applications | [x] | [ ] | | use media-keys as hotkey | [x] | [ ] | -| application specific hotkey| [ ] | [x] | | modifier only hotkey | [ ] | [x] | | caps-lock as hotkey | [ ] | [x] | | mouse-buttons as hotkey | [ ] | [x] | @@ -32,9 +37,6 @@ Requires xcode-8 command-line tools. brew install koekeishiya/formulae/skhd brew services start skhd - stdout -> /tmp/skhd.out - stderr -> /tmp/skhd.err - **Source**: Requires xcode-8 command-line tools. @@ -46,11 +48,32 @@ Requires xcode-8 command-line tools. ### Usage ``` +-V | --verbose: Output debug information + skhd -V + +-P | --profile: Output profiling information + skhd -P + -v | --version: Print version number to stdout skhd -v -c | --config: Specify location of config file skhd -c ~/.skhdrc + +-o | --observe: Output keycode and modifiers of event. Ctrl+C to quit + skhd -o + +-r | --reload: Signal a running instance of skhd to reload its config file + skhd -r + +-h | --no-hotload: Disable system for hotloading config file + skhd -h + +-k | --key: Synthesize a keypress (same syntax as when defining a hotkey) + skhd -k "shift + alt - 7" + +-t | --text: Synthesize a line of text + skhd -t "hello, worldã‚·" ``` ### Configuration @@ -63,35 +86,47 @@ A list of all built-in modifier and literal keywords can be found [here](https:/ A hotkey is written according to the following rules: ``` -hotkey = <mode> '<' <action> | <action> +hotkey = <mode> '<' <action> | <action> -mode = 'name of mode' | <mode> ',' <mode> +mode = 'name of mode' | <mode> ',' <mode> -action = <keysym> ':' <command> | <keysym> '->' ':' <command> - <keysym> ';' <mode> | <keysym> '->' ';' <mode> +action = <keysym> '[' <proc_map_lst> ']' | <keysym> '->' '[' <proc_map_lst> ']' + <keysym> ':' <command> | <keysym> '->' ':' <command> + <keysym> ';' <mode> | <keysym> '->' ';' <mode> -keysym = <mod> '-' <key> | <key> +keysym = <mod> '-' <key> | <key> -mod = 'modifier keyword' | <mod> '+' <mod> +mod = 'modifier keyword' | <mod> '+' <mod> -key = <literal> | <keycode> +key = <literal> | <keycode> -literal = 'single letter or built-in keyword' +literal = 'single letter or built-in keyword' -keycode = 'apple keyboard kVK_<Key> values (0x3C)' +keycode = 'apple keyboard kVK_<Key> values (0x3C)' --> = keypress is not consumed by skhd +proc_map_lst = * <proc_map> -command = command is executed through '$SHELL -c' and - follows valid shell syntax. if the $SHELL environment - variable is not set, it will default to '/bin/bash'. - when bash is used, the ';' delimeter can be specified - to chain commands. +proc_map = <string> ':' <command> | <string> '~' | + '*' ':' <command> | '*' '~' - to allow a command to extend into multiple lines, - prepend '\' at the end of the previous line. +string = '"' 'sequence of characters' '"' - an EOL character signifies the end of the bind. +command = command is executed through '$SHELL -c' and + follows valid shell syntax. if the $SHELL environment + variable is not set, it will default to '/bin/bash'. + when bash is used, the ';' delimeter can be specified + to chain commands. + + to allow a command to extend into multiple lines, + prepend '\' at the end of the previous line. + + an EOL character signifies the end of the bind. + +-> = keypress is not consumed by skhd + +* = matches every application not specified in <proc_map_lst> + +~ = application is unbound and keypress is forwarded per usual, when specified in a <proc_map> ``` A mode is declared according to the following rules: @@ -115,3 +150,22 @@ command = command is executed through '$SHELL -c' and an EOL character signifies the end of the bind. ``` + +General options that configure the behaviour of **skhd**: +``` +# specify a file that should be included as an additional config-file. +# treated as an absolutepath if the filename begins with '/' otherwise +# the file is relative to the path of the config-file it was loaded from. + +.load "/Users/Koe/.config/partial_skhdrc" +.load "partial_skhdrc" + +# prevents skhd from monitoring events for listed processes. + +.blacklist [ + "terminal" + "qutebrowser" + "kitty" + "google chrome" +] +``` diff --git a/examples/skhdrc b/examples/skhdrc index 2b0e6d8..3486c1f 100644 --- a/examples/skhdrc +++ b/examples/skhdrc @@ -3,36 +3,47 @@ # # A hotkey is written according to the following rules: # -# hotkey = <mode> '<' <action> | <action> +# hotkey = <mode> '<' <action> | <action> # -# mode = 'name of mode' | <mode> ',' <mode> +# mode = 'name of mode' | <mode> ',' <mode> # -# action = <keysym> ':' <command> | <keysym> '->' ':' <command> -# <keysym> ';' <mode> | <keysym> '->' ';' <mode> +# action = <keysym> '[' <proc_map_lst> ']' | <keysym> '->' '[' <proc_map_lst> ']' +# <keysym> ':' <command> | <keysym> '->' ':' <command> +# <keysym> ';' <mode> | <keysym> '->' ';' <mode> # -# keysym = <mod> '-' <key> | <key> +# keysym = <mod> '-' <key> | <key> # -# mod = 'built-in mod keyword' | <mod> '+' <mod> +# mod = 'modifier keyword' | <mod> '+' <mod> # -# key = <literal> | <keycode> +# key = <literal> | <keycode> # -# literal = 'single letter or built-in keyword' +# literal = 'single letter or built-in keyword' # -# keycode = 'apple keyboard kVK_<Key> values (0x3C)' +# keycode = 'apple keyboard kVK_<Key> values (0x3C)' # -# -> = keypress is not consumed by skhd +# proc_map_lst = * <proc_map> # -# command = command is executed through '$SHELL -c' and -# follows valid shell syntax. if the $SHELL environment -# variable is not set, it will default to '/bin/bash'. -# when bash is used, the ';' delimeter can be specified -# to chain commands. +# proc_map = <string> ':' <command> | <string> '~' | +# '*' ':' <command> | '*' '~' # -# to allow a command to extend into multiple lines, -# prepend '\' at the end of the previous line. +# string = '"' 'sequence of characters' '"' # -# an EOL character signifies the end of the bind. +# command = command is executed through '$SHELL -c' and +# follows valid shell syntax. if the $SHELL environment +# variable is not set, it will default to '/bin/bash'. +# when bash is used, the ';' delimeter can be specified +# to chain commands. # +# to allow a command to extend into multiple lines, +# prepend '\' at the end of the previous line. +# +# an EOL character signifies the end of the bind. +# +# -> = keypress is not consumed by skhd +# +# * = matches every application not specified in <proc_map_lst> +# +# ~ = application is unbound and keypress is forwarded per usual, when specified in a <proc_map> # # NOTE(koekeishiya): A mode is declared according to the following rules: # @@ -43,16 +54,16 @@ # # @ = capture keypresses regardless of being bound to an action # -# command = command is executed through '$SHELL -c' and -# follows valid shell syntax. if the $SHELL environment -# variable is not set, it will default to '/bin/bash'. -# when bash is used, the ';' delimeter can be specified -# to chain commands. +# command = command is executed through '$SHELL -c' and +# follows valid shell syntax. if the $SHELL environment +# variable is not set, it will default to '/bin/bash'. +# when bash is used, the ';' delimeter can be specified +# to chain commands. # -# to allow a command to extend into multiple lines, -# prepend '\' at the end of the previous line. +# to allow a command to extend into multiple lines, +# prepend '\' at the end of the previous line. # -# an EOL character signifies the end of the bind. +# an EOL character signifies the end of the bind. # add an on_enter command to the default mode # :: default : chunkc border::color 0xff775759 @@ -69,12 +80,40 @@ # launch a new terminal instance when in either 'default' or 'test' mode # default, test < cmd - return : open -na /Applications/Terminal.app +# application specific bindings +# +# cmd - n [ +# "kitty" : echo "hello kitty" +# * : echo "hello everyone" +# "qutebrowser" : echo "hello qutebrowser" +# "terminal" ~ +# "finder" : false +# ] + +# specify a file that should be included as an additional config-file. +# treated as an absolutepath if the filename begins with '/' otherwise +# the file is relative to the path of the config-file it was loaded from. +# +# .load "/Users/Koe/.config/partial_skhdrc" +# .load "partial_skhdrc" + +# prevent skhd from monitoring events for specific applications. +# +# .blacklist [ +# "kitty" +# "terminal" +# "qutebrowser" +# ] + # open terminal, blazingly fast compared to iTerm/Hyper -cmd - return : open -na /Applications/Kitty.app +cmd - return : /Applications/Kitty.app/Contents/MacOS/kitty --single-instance -d ~ # open qutebrowser cmd + shift - return : ~/Scripts/qtb.sh +# open mpv +cmd - m : open -na /Applications/mpv.app $(pbpaste) + # close focused window alt - w : chunkc tiling::window --close @@ -111,27 +150,48 @@ shift + alt - left : chunkc tiling::window --grid-layout 1:2:0:0:1:1 # make floating window fill right-half of screen shift + alt - right : chunkc tiling::window --grid-layout 1:2:1:0:1:1 -# send window to desktop -shift + alt - x : chunkc tiling::window --send-to-desktop $(chunkc get _last_active_desktop) -shift + alt - z : chunkc tiling::window --send-to-desktop prev -shift + alt - c : chunkc tiling::window --send-to-desktop next -shift + alt - 1 : chunkc tiling::window --send-to-desktop 1 -shift + alt - 2 : chunkc tiling::window --send-to-desktop 2 -shift + alt - 3 : chunkc tiling::window --send-to-desktop 3 -shift + alt - 4 : chunkc tiling::window --send-to-desktop 4 -shift + alt - 5 : chunkc tiling::window --send-to-desktop 5 -shift + alt - 6 : chunkc tiling::window --send-to-desktop 6 +# create desktop, move window and follow focus +shift + cmd - n : chunkc tiling::desktop --create;\ + id=$(chunkc tiling::query --desktops-for-monitor $(chunkc tiling::query --monitor-for-desktop $(chunkc tiling::query --desktop id)));\ + chunkc tiling::window --send-to-desktop $(echo ${id##* });\ + chunkc tiling::desktop --focus $(echo ${id##* }) + +# create desktop and follow focus +cmd + alt - n : chunkc tiling::desktop --create;\ + id=$(chunkc tiling::query --desktops-for-monitor $(chunkc tiling::query --monitor-for-desktop $(chunkc tiling::query --desktop id)));\ + chunkc tiling::desktop --focus $(echo ${id##* }) +# destroy desktop +cmd + alt - w : chunkc tiling::desktop --annihilate + +# fast focus desktop +cmd + alt - x : chunkc tiling::desktop --focus $(chunkc get _last_active_desktop) +cmd + alt - z : chunkc tiling::desktop --focus prev +cmd + alt - c : chunkc tiling::desktop --focus next +cmd + alt - 1 : chunkc tiling::desktop --focus 1 +cmd + alt - 2 : chunkc tiling::desktop --focus 2 +cmd + alt - 3 : chunkc tiling::desktop --focus 3 +cmd + alt - 4 : chunkc tiling::desktop --focus 4 +cmd + alt - 5 : chunkc tiling::desktop --focus 5 +cmd + alt - 6 : chunkc tiling::desktop --focus 6 +cmd + alt - 7 : chunkc tiling::desktop --focus 7 +cmd + alt - 8 : chunkc tiling::desktop --focus 8 +cmd + alt - 9 : chunkc tiling::desktop --focus 9 +cmd + alt - 0 : chunkc tiling::desktop --focus 10 # send window to desktop and follow focus -shift + cmd - x : chunkc tiling::window --send-to-desktop $(chunkc get _last_active_desktop); qes -k "cmd + alt - $(chunkc get _last_active_desktop)" -shift + cmd - z : chunkc tiling::window --send-to-desktop prev; qes -k "cmd + alt - z" -shift + cmd - c : chunkc tiling::window --send-to-desktop next; qes -k "cmd + alt - c" -shift + cmd - 1 : chunkc tiling::window --send-to-desktop 1; qes -k "cmd + alt - 1" -shift + cmd - 2 : chunkc tiling::window --send-to-desktop 2; qes -k "cmd + alt - 2" -shift + cmd - 3 : chunkc tiling::window --send-to-desktop 3; qes -k "cmd + alt - 3" -shift + cmd - 4 : chunkc tiling::window --send-to-desktop 4; qes -k "cmd + alt - 4" -shift + cmd - 5 : chunkc tiling::window --send-to-desktop 5; qes -k "cmd + alt - 5" -shift + cmd - 6 : chunkc tiling::window --send-to-desktop 6; qes -k "cmd + alt - 6" +shift + cmd - x : chunkc tiling::window --send-to-desktop $(chunkc get _last_active_desktop); chunkc tiling::desktop --focus $(chunkc get _last_active_desktop) +shift + cmd - z : chunkc tiling::window --send-to-desktop prev; chunkc tiling::desktop --focus prev +shift + cmd - c : chunkc tiling::window --send-to-desktop next; chunkc tiling::desktop --focus next +shift + cmd - 1 : chunkc tiling::window --send-to-desktop 1; chunkc tiling::desktop --focus 1 +shift + cmd - 2 : chunkc tiling::window --send-to-desktop 2; chunkc tiling::desktop --focus 2 +shift + cmd - 3 : chunkc tiling::window --send-to-desktop 3; chunkc tiling::desktop --focus 3 +shift + cmd - 4 : chunkc tiling::window --send-to-desktop 4; chunkc tiling::desktop --focus 4 +shift + cmd - 5 : chunkc tiling::window --send-to-desktop 5; chunkc tiling::desktop --focus 5 +shift + cmd - 6 : chunkc tiling::window --send-to-desktop 6; chunkc tiling::desktop --focus 6 +shift + cmd - 7 : chunkc tiling::window --send-to-desktop 7; chunkc tiling::desktop --focus 7 +shift + cmd - 8 : chunkc tiling::window --send-to-desktop 8; chunkc tiling::desktop --focus 8 +shift + cmd - 9 : chunkc tiling::window --send-to-desktop 9; chunkc tiling::desktop --focus 9 +shift + cmd - 0 : chunkc tiling::window --send-to-desktop 10; chunkc tiling::desktop --focus 10 # focus monitor ctrl + alt - z : chunkc tiling::monitor -f prev @@ -197,8 +257,11 @@ alt - q : chunkc tiling::window --toggle fade alt - t : chunkc tiling::window --toggle float;\ chunkc tiling::window --grid-layout 4:4:1:1:2:2 +# toggle sticky +alt - s : chunkc tiling::window --toggle sticky + # toggle sticky, float and resize to picture-in-picture size -alt - s : chunkc tiling::window --toggle sticky;\ +alt - p : chunkc tiling::window --toggle sticky;\ chunkc tiling::window --grid-layout 5:5:4:0:1:1 # float next window to be tiled @@ -210,7 +273,3 @@ ctrl + alt - s : chunkc tiling::desktop --layout monocle ctrl + alt - d : chunkc tiling::desktop --layout float ctrl + alt - w : chunkc tiling::desktop --deserialize ~/.chunkwm_layouts/dev_1 - -cmd - 7 : qes -k "shift + alt - 7" -cmd - 8 : qes -k "shift + alt - 8" -cmd - 9 : qes -k "shift + alt - 9" @@ -1,4 +1,4 @@ -FRAMEWORKS = -framework Carbon -F/System/Library/PrivateFrameworks -framework MultitouchSupport +FRAMEWORKS = -framework Carbon -framework CoreAudio -F/System/Library/PrivateFrameworks -framework MultitouchSupport BUILD_PATH = ./bin BUILD_FLAGS = -std=c99 -Wall -g -O0 SKHD_SRC = ./src/skhd.c @@ -11,9 +11,6 @@ all: clean $(BINS) install: BUILD_FLAGS=-std=c99 -O3 install: clean $(BINS) -segfault: BUILD_FLAGS=-O0 -g -Wall -std=c99 -D_FORTIFY_SOURCE=2 -fstack-protector-strong --param ssp-buffer-size=4 -fPIC -fno-strict-overflow -Wformat -Wformat-security -Werror=format-security -segfault: clean $(BINS) - clean: rm -rf $(BUILD_PATH) diff --git a/src/carbon.c b/src/carbon.c new file mode 100644 index 0000000..dcfc2d8 --- /dev/null +++ b/src/carbon.c @@ -0,0 +1,77 @@ +#include "carbon.h" + +#define internal static + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" +internal inline char * +find_process_name_for_psn(ProcessSerialNumber *psn) +{ + CFStringRef process_name_ref; + if (CopyProcessName(psn, &process_name_ref) == noErr) { + char *process_name = copy_cfstring(process_name_ref); + for (char *s = process_name; *s; ++s) *s = tolower(*s); + CFRelease(process_name_ref); + return process_name; + } + return NULL; +} + +inline char * +find_process_name_for_pid(pid_t pid) +{ + ProcessSerialNumber psn; + GetProcessForPID(pid, &psn); + return find_process_name_for_psn(&psn); +} + +internal inline char * +find_active_process_name(void) +{ + ProcessSerialNumber psn; + GetFrontProcess(&psn); + return find_process_name_for_psn(&psn); +} +#pragma clang diagnostic pop + +internal OSStatus +carbon_event_handler(EventHandlerCallRef ref, EventRef event, void *context) +{ + struct carbon_event *carbon = (struct carbon_event *) context; + + ProcessSerialNumber psn; + if (GetEventParameter(event, + kEventParamProcessID, + typeProcessSerialNumber, + NULL, + sizeof(psn), + NULL, + &psn) != noErr) { + return -1; + } + + if (carbon->process_name) { + free(carbon->process_name); + carbon->process_name = NULL; + } + + carbon->process_name = find_process_name_for_psn(&psn); + + return noErr; +} + +bool carbon_event_init(struct carbon_event *carbon) +{ + carbon->target = GetApplicationEventTarget(); + carbon->handler = NewEventHandlerUPP(carbon_event_handler); + carbon->type.eventClass = kEventClassApplication; + carbon->type.eventKind = kEventAppFrontSwitched; + carbon->process_name = find_active_process_name(); + + return InstallEventHandler(carbon->target, + carbon->handler, + 1, + &carbon->type, + carbon, + &carbon->handler_ref) == noErr; +} diff --git a/src/carbon.h b/src/carbon.h new file mode 100644 index 0000000..8ef9ed4 --- /dev/null +++ b/src/carbon.h @@ -0,0 +1,18 @@ +#ifndef SKHD_CARBON_H +#define SKHD_CARBON_H + +#include <Carbon/Carbon.h> + +struct carbon_event +{ + EventTargetRef target; + EventHandlerUPP handler; + EventTypeSpec type; + EventHandlerRef handler_ref; + char * volatile process_name; +}; + +char *find_process_name_for_pid(pid_t pid); +bool carbon_event_init(struct carbon_event *carbon); + +#endif diff --git a/src/hashtable.h b/src/hashtable.h index 42d0166..8f73066 100644 --- a/src/hashtable.h +++ b/src/hashtable.h @@ -33,7 +33,9 @@ void *table_reset(struct table *table, int *count); #include <stdlib.h> #include <string.h> -static struct bucket * +#define internal static + +internal struct bucket * table_new_bucket(void *key, void *value) { struct bucket *bucket = malloc(sizeof(struct bucket)); @@ -43,7 +45,7 @@ table_new_bucket(void *key, void *value) return bucket; } -static struct bucket ** +internal struct bucket ** table_get_bucket(struct table *table, void *key) { struct bucket **bucket = table->buckets + (table->hash(key) % table->capacity); @@ -56,6 +58,8 @@ table_get_bucket(struct table *table, void *key) return bucket; } +#undef internal + void table_init(struct table *table, int capacity, table_hash_func hash, table_compare_func compare) { table->count = 0; @@ -76,7 +80,10 @@ void table_free(struct table *table) bucket = next; } } - free(table->buckets); + if (table->buckets) { + free(table->buckets); + table->buckets = NULL; + } } void *table_find(struct table *table, void *key) diff --git a/src/hotkey.c b/src/hotkey.c index a2c383a..780aa3a 100644 --- a/src/hotkey.c +++ b/src/hotkey.c @@ -1,7 +1,11 @@ #include "hotkey.h" #define internal static -#define local_persist static +#define global static + +#define HOTKEY_FOUND ((1) << 0) +#define MODE_CAPTURE(a) ((a) << 1) +#define HOTKEY_PASSTHROUGH(a) ((a) << 2) #define LRMOD_ALT 0 #define LRMOD_CMD 6 @@ -10,6 +14,9 @@ #define LMOD_OFFS 1 #define RMOD_OFFS 2 +global char arg[] = "-c"; +global char *shell = NULL; + internal uint32_t cgevent_lrmod_flag[] = { Event_Mask_Alt, Event_Mask_LAlt, Event_Mask_RAlt, @@ -67,7 +74,7 @@ unsigned long hash_hotkey(struct hotkey *a) return a->key; } -bool same_mode(char *a, char *b) +bool compare_string(char *a, char *b) { while (*a && *b && *a == *b) { ++a; @@ -76,7 +83,7 @@ bool same_mode(char *a, char *b) return *a == '\0' && *b == '\0'; } -unsigned long hash_mode(char *key) +unsigned long hash_string(char *key) { unsigned long hash = 0, high; while(*key) { @@ -90,48 +97,86 @@ unsigned long hash_mode(char *key) return hash; } -internal void +internal inline void fork_and_exec(char *command) { - local_persist char arg[] = "-c"; - local_persist char *shell = NULL; - if (!shell) { - char *env_shell = getenv("SHELL"); - shell = env_shell ? env_shell : "/bin/bash"; - } - int cpid = fork(); if (cpid == 0) { + setsid(); char *exec[] = { shell, arg, command, NULL}; int status_code = execvp(exec[0], exec); exit(status_code); } } -internal inline bool -passthrough(struct hotkey *hotkey) +internal inline void +passthrough(struct hotkey *hotkey, uint32_t *capture) { - return !has_flags(hotkey, Hotkey_Flag_Passthrough); + *capture |= HOTKEY_PASSTHROUGH((int)has_flags(hotkey, Hotkey_Flag_Passthrough)); } internal inline struct hotkey * -find_hotkey(struct mode *mode, struct hotkey *hotkey) +find_hotkey(struct mode *mode, struct hotkey *hotkey, uint32_t *capture) { - return table_find(&mode->hotkey_map, hotkey); + struct hotkey *result = table_find(&mode->hotkey_map, hotkey); + if (result) *capture |= HOTKEY_FOUND; + return result; } -bool find_and_exec_hotkey(struct hotkey *k, struct table *t, struct mode **m) +internal inline bool +should_capture_hotkey(uint32_t capture) { - bool c = (*m)->capture; - for (struct hotkey *h = find_hotkey(*m, k); h; c |= passthrough(h), h = 0) { - char *cmd = h->command; + if ((capture & HOTKEY_FOUND)) { + if (!(capture & MODE_CAPTURE(1)) && + !(capture & HOTKEY_PASSTHROUGH(1))) { + return true; + } + + if (!(capture & HOTKEY_PASSTHROUGH(1)) && + (capture & MODE_CAPTURE(1))) { + return true; + } + + return false; + } + + return (capture & MODE_CAPTURE(1)); +} + +internal inline char * +find_process_command_mapping(struct hotkey *hotkey, uint32_t *capture, struct carbon_event *carbon) +{ + char *result = NULL; + bool found = false; + + for (int i = 0; i < buf_len(hotkey->process_name); ++i) { + if (same_string(carbon->process_name, hotkey->process_name[i])) { + result = hotkey->command[i]; + found = true; + break; + } + } + + if (!found) result = hotkey->wildcard_command; + if (!result) *capture &= ~HOTKEY_FOUND; + + return result; +} + +bool find_and_exec_hotkey(struct hotkey *k, struct table *t, struct mode **m, struct carbon_event *carbon) +{ + uint32_t c = MODE_CAPTURE((int)(*m)->capture); + for (struct hotkey *h = find_hotkey(*m, k, &c); h; passthrough(h, &c), h = 0) { + char *cmd = h->command[0]; if (has_flags(h, Hotkey_Flag_Activate)) { *m = table_find(t, cmd); cmd = (*m)->command; + } else if (buf_len(h->process_name) > 0) { + cmd = find_process_command_mapping(h, &c, carbon); } if (cmd) fork_and_exec(cmd); } - return c; + return should_capture_hotkey(c); } void free_mode_map(struct table *mode_map) @@ -156,7 +201,17 @@ void free_mode_map(struct table *mode_map) buf_push(freed_pointers, hotkey); buf_free(hotkey->mode_list); - free(hotkey->command); + + for (int i = 0; i < buf_len(hotkey->process_name); ++i) { + free(hotkey->process_name[i]); + } + buf_free(hotkey->process_name); + + for (int i = 0; i < buf_len(hotkey->command); ++i) { + free(hotkey->command[i]); + } + buf_free(hotkey->command); + free(hotkey); next:; } @@ -173,6 +228,16 @@ next:; } } +void free_blacklist(struct table *blacklst) +{ + int count; + void **items = table_reset(blacklst, &count); + for (int index = 0; index < count; ++index) { + char *item = (char *) items[index]; + free(item); + } +} + internal void cgevent_lrmod_flag_to_hotkey_lrmod_flag(CGEventFlags eventflags, uint32_t *flags, int mod) { @@ -216,21 +281,30 @@ struct hotkey create_eventkey(CGEventRef event) return eventkey; } -struct systemkey create_systemkey(CGEventRef event) +bool intercept_systemkey(CGEventRef event, struct hotkey *eventkey) { CFDataRef event_data = CGEventCreateData(kCFAllocatorDefault, event); - const uint8_t *data = CFDataGetBytePtr(event_data); - uint8_t event_subtype = data[123]; - uint8_t key_code = data[129]; + const uint8_t *data = CFDataGetBytePtr(event_data); + uint8_t key_code = data[129]; uint8_t key_state = data[130]; + uint8_t key_stype = data[123]; CFRelease(event_data); - struct systemkey systemkey = { - .eventkey = { - .key = key_code, - .flags = cgevent_flags_to_hotkey_flags(CGEventGetFlags(event)) | Hotkey_Flag_NX - }, - .intercept = key_state == NX_KEYDOWN && event_subtype == NX_SUBTYPE_AUX_CONTROL_BUTTONS - }; - return systemkey; + bool result = ((key_state == NX_KEYDOWN) && + (key_stype == NX_SUBTYPE_AUX_CONTROL_BUTTONS)); + + if (result) { + eventkey->key = key_code; + eventkey->flags = cgevent_flags_to_hotkey_flags(CGEventGetFlags(event)) | Hotkey_Flag_NX; + } + + return result; +} + +void init_shell(void) +{ + if (!shell) { + char *env_shell = getenv("SHELL"); + shell = env_shell ? env_shell : "/bin/bash"; + } } diff --git a/src/hotkey.h b/src/hotkey.h index b0ca504..4611b09 100644 --- a/src/hotkey.h +++ b/src/hotkey.h @@ -5,6 +5,12 @@ #include <stdint.h> #include <stdbool.h> +#define Modifier_Keycode_Alt 0x3A +#define Modifier_Keycode_Shift 0x38 +#define Modifier_Keycode_Cmd 0x37 +#define Modifier_Keycode_Ctrl 0x3B +#define Modifier_Keycode_Fn 0x3F + enum osx_event_mask { Event_Mask_Alt = 0x00080000, @@ -43,15 +49,18 @@ enum hotkey_flag Hotkey_Flag_Hyper = (Hotkey_Flag_Cmd | Hotkey_Flag_Alt | Hotkey_Flag_Shift | - Hotkey_Flag_Control) + Hotkey_Flag_Control), + Hotkey_Flag_Meh = (Hotkey_Flag_Control | + Hotkey_Flag_Shift | + Hotkey_Flag_Alt) }; #include "hashtable.h" +struct carbon_event; + struct mode { - int line; - int cursor; char *name; char *command; bool capture; @@ -62,45 +71,48 @@ struct hotkey { uint32_t flags; uint32_t key; - char *command; + char **process_name; + char **command; + char *wildcard_command; struct mode **mode_list; }; -struct systemkey -{ - struct hotkey eventkey; - bool intercept; -}; +#define internal static -static inline void +internal inline void add_flags(struct hotkey *hotkey, uint32_t flag) { hotkey->flags |= flag; } -static inline bool +internal inline bool has_flags(struct hotkey *hotkey, uint32_t flag) { bool result = hotkey->flags & flag; return result; } -static inline void +internal inline void clear_flags(struct hotkey *hotkey, uint32_t flag) { hotkey->flags &= ~flag; } -bool same_mode(char *a, char *b); -unsigned long hash_mode(char *key); +#undef internal + +bool compare_string(char *a, char *b); +unsigned long hash_string(char *key); bool same_hotkey(struct hotkey *a, struct hotkey *b); unsigned long hash_hotkey(struct hotkey *a); struct hotkey create_eventkey(CGEventRef event); -struct systemkey create_systemkey(CGEventRef event); +bool intercept_systemkey(CGEventRef event, struct hotkey *eventkey); -bool find_and_exec_hotkey(struct hotkey *eventkey, struct table *mode_map, struct mode **current_mode); +bool find_and_exec_hotkey(struct hotkey *k, struct table *t, struct mode **m, struct carbon_event *carbon); void free_mode_map(struct table *mode_map); +void free_blacklist(struct table *blacklst); + +void init_shell(void); #endif diff --git a/src/hotload.c b/src/hotload.c index ad02d21..88d286a 100644 --- a/src/hotload.c +++ b/src/hotload.c @@ -8,23 +8,59 @@ #define FSEVENT_CALLBACK(name) void name(ConstFSEventStreamRef stream,\ void *context,\ - size_t count,\ - void *paths,\ + size_t file_count,\ + void *file_paths,\ const FSEventStreamEventFlags *flags,\ const FSEventStreamEventId *ids) +enum watch_kind +{ + WATCH_KIND_INVALID, + WATCH_KIND_CATALOG, + WATCH_KIND_FILE +}; + +struct watched_catalog +{ + char *directory; + char *extension; +}; + +struct watched_file +{ + char *absolutepath; + char *directory; + char *filename; +}; + +struct watched_entry +{ + enum watch_kind kind; + union { + struct watched_file file_info; + struct watched_catalog catalog_info; + }; +}; + +internal inline bool +same_string(const char *a, const char *b) +{ + bool result = a && b && strcmp(a, b) == 0; + return result; +} + internal char * copy_string(const char *s) { unsigned length = strlen(s); - char *result = malloc(length + 1); + char *result = (char *) malloc(length + 1); memcpy(result, s, length); result[length] = '\0'; return result; } internal char * -file_directory(const char *file) +file_directory(char *file) { char *last_slash = strrchr(file, '/'); *last_slash = '\0'; @@ -34,7 +70,7 @@ file_directory(const char *file) } internal char * -file_name(const char *file) +file_name(char *file) { char *last_slash = strrchr(file, '/'); char *name = copy_string(last_slash + 1); @@ -42,50 +78,73 @@ file_name(const char *file) } internal char * -resolve_symlink(char *file) +resolve_symlink(const char *file) { struct stat buffer; if (lstat(file, &buffer) != 0) { return NULL; } + if (S_ISDIR(buffer.st_mode)) { + return copy_string(file); + } + if (!S_ISLNK(buffer.st_mode)) { - return file; + return copy_string(file); } - ssize_t size = buffer.st_size + 1; - char *result = malloc(size); - ssize_t read = readlink(file, result, size); + char *result = realpath(file, NULL); + return result; +} - if (read != -1) { - result[read] = '\0'; - return result; +internal enum watch_kind +resolve_watch_kind(char *file) +{ + struct stat buffer; + if (lstat(file, &buffer) != 0) { + return WATCH_KIND_INVALID; } - free(result); - return NULL; + if (S_ISDIR(buffer.st_mode)) { + return WATCH_KIND_CATALOG; + } + + if (!S_ISLNK(buffer.st_mode)) { + return WATCH_KIND_FILE; + } + + return WATCH_KIND_INVALID; } -internal struct watched_file * -hotloader_watched_file(struct hotloader *hotloader, char *absolutepath) +internal char * +same_catalog(char *absolutepath, struct watched_catalog *catalog_info) { - struct watched_file *result = NULL; - for (unsigned index = 0; result == NULL && index < hotloader->watch_count; ++index) { - struct watched_file *watch_info = hotloader->watch_list + index; + char *last_slash = strrchr(absolutepath, '/'); + if (!last_slash) return NULL; - char *directory = file_directory(absolutepath); - char *filename = file_name(absolutepath); + char *filename = NULL; - if (strcmp(watch_info->directory, directory) == 0) { - if (strcmp(watch_info->filename, filename) == 0) { - result = watch_info; - } - } + // NOTE(koekeisihya): null terminate '/' to cut off filename + *last_slash = '\0'; - free(filename); - free(directory); + if (same_string(absolutepath, catalog_info->directory)) { + filename = !catalog_info->extension + ? last_slash + 1 + : same_string(catalog_info->extension, strrchr(last_slash + 1, '.')) + ? last_slash + 1 + : NULL; } + // NOTE(koekeisihya): revert '/' to restore filename + *last_slash = '/'; + + return filename; +} + +internal inline bool +same_file(char *absolutepath, struct watched_file *file_info) +{ + bool result = same_string(absolutepath, file_info->absolutepath); return result; } @@ -93,59 +152,120 @@ internal FSEVENT_CALLBACK(hotloader_handler) { /* NOTE(koekeishiya): We sometimes get two events upon file save. */ struct hotloader *hotloader = (struct hotloader *) context; - char **files = (char **) paths; + char **files = (char **) file_paths; - struct watched_file *watch_info; - for (unsigned index = 0; index < count; ++index) { - char *absolutepath = files[index]; - if ((watch_info = hotloader_watched_file(hotloader, absolutepath))) { - hotloader->callback(absolutepath, watch_info->directory, watch_info->filename); + for (unsigned file_index = 0; file_index < file_count; ++file_index) { + for (unsigned watch_index = 0; watch_index < hotloader->watch_count; ++watch_index) { + struct watched_entry *watch_info = hotloader->watch_list + watch_index; + if (watch_info->kind == WATCH_KIND_CATALOG) { + char *filename = same_catalog(files[file_index], &watch_info->catalog_info); + if (!filename) continue; + + hotloader->callback(files[file_index], + watch_info->catalog_info.directory, + filename); + break; + } else if (watch_info->kind == WATCH_KIND_FILE) { + bool match = same_file(files[file_index], &watch_info->file_info); + if (!match) continue; + + hotloader->callback(watch_info->file_info.absolutepath, + watch_info->file_info.directory, + watch_info->file_info.filename); + break; + } } } } -void hotloader_add_file(struct hotloader *hotloader, char *file) +internal inline void +hotloader_add_watched_entry(struct hotloader *hotloader, struct watched_entry entry) { - if (!hotloader->enabled) { - char *real_path = resolve_symlink(file); - if (real_path) { - struct watched_file watch_info; - watch_info.directory = file_directory(real_path); - watch_info.filename = file_name(real_path); + if (!hotloader->watch_list) { + hotloader->watch_capacity = 32; + hotloader->watch_list = (struct watched_entry *) malloc(hotloader->watch_capacity * sizeof(struct watched_entry)); + } - if (real_path != file) { - free(real_path); - } + if (hotloader->watch_count >= hotloader->watch_capacity) { + hotloader->watch_capacity = (unsigned) ceil(hotloader->watch_capacity * 1.5f); + hotloader->watch_list = (struct watched_entry *) realloc(hotloader->watch_list, hotloader->watch_capacity * sizeof(struct watched_entry)); + } + + hotloader->watch_list[hotloader->watch_count++] = entry; +} - hotloader->watch_list[hotloader->watch_count++] = watch_info; - printf("hotload: watching file '%s' in directory '%s'\n", watch_info.filename, watch_info.directory); - } else { - fprintf(stderr, "hotload: could not watch file '%s'\n", file); +bool hotloader_add_catalog(struct hotloader *hotloader, const char *directory, const char *extension) +{ + if (hotloader->enabled) return false; + + char *real_path = resolve_symlink(directory); + if (!real_path) return false; + + enum watch_kind kind = resolve_watch_kind(real_path); + if (kind != WATCH_KIND_CATALOG) return false; + + hotloader_add_watched_entry(hotloader, (struct watched_entry) { + .kind = WATCH_KIND_CATALOG, + .catalog_info = { + .directory = real_path, + .extension = extension + ? copy_string(extension) + : NULL } - } + }); + + return true; +} + +bool hotloader_add_file(struct hotloader *hotloader, const char *file) +{ + if (hotloader->enabled) return false; + + char *real_path = resolve_symlink(file); + if (!real_path) return false; + + enum watch_kind kind = resolve_watch_kind(real_path); + if (kind != WATCH_KIND_FILE) return false; + + hotloader_add_watched_entry(hotloader, (struct watched_entry) { + .kind = WATCH_KIND_FILE, + .file_info = { + .absolutepath = real_path, + .directory = file_directory(real_path), + .filename = file_name(real_path) + } + }); + + return true; } bool hotloader_begin(struct hotloader *hotloader, hotloader_callback *callback) { - if ((hotloader->enabled) || - (!hotloader->watch_count)) { - return false; - } + if (hotloader->enabled || !hotloader->watch_count) return false; CFStringRef string_refs[hotloader->watch_count]; for (unsigned index = 0; index < hotloader->watch_count; ++index) { + struct watched_entry *watch_info = hotloader->watch_list + index; + char *directory = watch_info->kind == WATCH_KIND_FILE + ? watch_info->file_info.directory + : watch_info->catalog_info.directory; string_refs[index] = CFStringCreateWithCString(kCFAllocatorDefault, - hotloader->watch_list[index].directory, + directory, kCFStringEncodingUTF8); } - FSEventStreamContext context = {}; - context.info = (void *) hotloader; + FSEventStreamContext context = { + .info = hotloader + }; + + hotloader->path = (CFArrayRef) CFArrayCreate(NULL, + (const void **) string_refs, + hotloader->watch_count, + &kCFTypeArrayCallBacks); + + hotloader->flags = kFSEventStreamCreateFlagNoDefer | + kFSEventStreamCreateFlagFileEvents; - hotloader->enabled = true; - hotloader->callback = callback; - hotloader->path = (CFArrayRef) CFArrayCreate(NULL, (const void **) string_refs, hotloader->watch_count, &kCFTypeArrayCallBacks); - hotloader->flags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; hotloader->stream = FSEventStreamCreate(NULL, hotloader_handler, &context, @@ -153,27 +273,43 @@ bool hotloader_begin(struct hotloader *hotloader, hotloader_callback *callback) kFSEventStreamEventIdSinceNow, 0.5, hotloader->flags); - FSEventStreamScheduleWithRunLoop(hotloader->stream, CFRunLoopGetMain(), kCFRunLoopDefaultMode); + + FSEventStreamScheduleWithRunLoop(hotloader->stream, + CFRunLoopGetMain(), + kCFRunLoopDefaultMode); + + hotloader->callback = callback; FSEventStreamStart(hotloader->stream); + hotloader->enabled = true; + return true; } void hotloader_end(struct hotloader *hotloader) { - if (hotloader->enabled) { - FSEventStreamStop(hotloader->stream); - FSEventStreamInvalidate(hotloader->stream); - FSEventStreamRelease(hotloader->stream); - - CFIndex count = CFArrayGetCount(hotloader->path); - for (unsigned index = 0; index < count; ++index) { - CFStringRef string_ref = (CFStringRef) CFArrayGetValueAtIndex(hotloader->path, index); - free(hotloader->watch_list[index].directory); - free(hotloader->watch_list[index].filename); - CFRelease(string_ref); - } + if (!hotloader->enabled) return; + + FSEventStreamStop(hotloader->stream); + FSEventStreamInvalidate(hotloader->stream); + FSEventStreamRelease(hotloader->stream); - CFRelease(hotloader->path); - memset(hotloader, 0, sizeof(struct hotloader)); + CFIndex count = CFArrayGetCount(hotloader->path); + for (unsigned index = 0; index < count; ++index) { + struct watched_entry *watch_info = hotloader->watch_list + index; + if (watch_info->kind == WATCH_KIND_FILE) { + free(watch_info->file_info.absolutepath); + free(watch_info->file_info.directory); + free(watch_info->file_info.filename); + } else if (watch_info->kind == WATCH_KIND_CATALOG) { + free(watch_info->catalog_info.directory); + if (watch_info->catalog_info.extension) { + free(watch_info->catalog_info.extension); + } + } + CFRelease(CFArrayGetValueAtIndex(hotloader->path, index)); } + + CFRelease(hotloader->path); + free(hotloader->watch_list); + memset(hotloader, 0, sizeof(struct hotloader)); } diff --git a/src/hotload.h b/src/hotload.h index ad782fb..1d3921b 100644 --- a/src/hotload.h +++ b/src/hotload.h @@ -1,18 +1,16 @@ #ifndef SKHD_HOTLOAD_H #define SKHD_HOTLOAD_H +#ifndef __cplusplus #include <stdbool.h> +#endif + #include <Carbon/Carbon.h> #define HOTLOADER_CALLBACK(name) void name(char *absolutepath, char *directory, char *filename) typedef HOTLOADER_CALLBACK(hotloader_callback); -struct watched_file -{ - char *directory; - char *filename; -}; - +struct watched_entry; struct hotloader { FSEventStreamEventFlags flags; @@ -21,12 +19,14 @@ struct hotloader bool enabled; hotloader_callback *callback; - struct watched_file watch_list[32]; + struct watched_entry *watch_list; + unsigned watch_capacity; unsigned watch_count; }; bool hotloader_begin(struct hotloader *hotloader, hotloader_callback *callback); void hotloader_end(struct hotloader *hotloader); -void hotloader_add_file(struct hotloader *hotloader, char *file); +bool hotloader_add_catalog(struct hotloader *hotloader, const char *directory, const char *extension); +bool hotloader_add_file(struct hotloader *hotloader, const char *file); #endif diff --git a/src/locale.c b/src/locale.c index 464c3ba..64f8b04 100644 --- a/src/locale.c +++ b/src/locale.c @@ -1,24 +1,25 @@ #include "locale.h" #include "hashtable.h" +#include "sbuffer.h" #include <Carbon/Carbon.h> #include <IOKit/hidsystem/ev_keymap.h> +#define array_count(a) (sizeof((a)) / sizeof(*(a))) + #define internal static -#define local_persist static +#define global static -internal struct table keymap_table; +global struct table keymap_table; +global char **keymap_keys; internal char * -copy_cf_string_to_c(CFStringRef string) +copy_cfstring(CFStringRef string) { - CFStringEncoding encoding = kCFStringEncodingUTF8; - CFIndex length = CFStringGetLength(string); - CFIndex bytes = CFStringGetMaximumSizeForEncoding(length, encoding); - char *result = malloc(bytes + 1); + CFIndex num_bytes = CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8); + char *result = malloc(num_bytes + 1); // NOTE(koekeishiya): Boolean: typedef -> unsigned char; false = 0, true != 0 - Boolean success = CFStringGetCString(string, result, bytes + 1, encoding); - if (!success) { + if (!CFStringGetCString(string, result, num_bytes + 1, kCFStringEncodingUTF8)) { free(result); result = NULL; } @@ -51,43 +52,45 @@ same_keymap(const char *a, const char *b) return *a == '\0' && *b == '\0'; } -internal CFStringRef -cfstring_from_keycode(UCKeyboardLayout *keyboard_layout, CGKeyCode keycode) +internal void +free_keycode_map(void) { - UInt32 dead_key_state = 0; - UniCharCount max_string_length = 255; - UniCharCount string_length = 0; - UniChar unicode_string[max_string_length]; - - OSStatus status = UCKeyTranslate(keyboard_layout, keycode, - kUCKeyActionDown, 0, - LMGetKbdType(), 0, - &dead_key_state, - max_string_length, - &string_length, - unicode_string); - - if (string_length == 0 && dead_key_state) { - status = UCKeyTranslate(keyboard_layout, kVK_Space, - kUCKeyActionDown, 0, - LMGetKbdType(), 0, - &dead_key_state, - max_string_length, - &string_length, - unicode_string); - } - - if (string_length > 0 && status == noErr) { - return CFStringCreateWithCharacters(NULL, unicode_string, string_length); + for (int i = 0; i < buf_len(keymap_keys); ++i) { + free(keymap_keys[i]); } - return NULL; + buf_free(keymap_keys); + keymap_keys = NULL; } +internal uint32_t layout_dependent_keycodes[] = +{ + kVK_ANSI_A, kVK_ANSI_B, kVK_ANSI_C, + kVK_ANSI_D, kVK_ANSI_E, kVK_ANSI_F, + kVK_ANSI_G, kVK_ANSI_H, kVK_ANSI_I, + kVK_ANSI_J, kVK_ANSI_K, kVK_ANSI_L, + kVK_ANSI_M, kVK_ANSI_N, kVK_ANSI_O, + kVK_ANSI_P, kVK_ANSI_Q, kVK_ANSI_R, + kVK_ANSI_S, kVK_ANSI_T, kVK_ANSI_U, + kVK_ANSI_V, kVK_ANSI_W, kVK_ANSI_X, + kVK_ANSI_Y, kVK_ANSI_Z, kVK_ANSI_0, + kVK_ANSI_1, kVK_ANSI_2, kVK_ANSI_3, + kVK_ANSI_4, kVK_ANSI_5, kVK_ANSI_6, + kVK_ANSI_7, kVK_ANSI_8, kVK_ANSI_9, + kVK_ANSI_Grave, kVK_ANSI_Equal, kVK_ANSI_Minus, + kVK_ANSI_RightBracket, kVK_ANSI_LeftBracket, kVK_ANSI_Quote, + kVK_ANSI_Semicolon, kVK_ANSI_Backslash, kVK_ANSI_Comma, + kVK_ANSI_Slash, kVK_ANSI_Period, kVK_ISO_Section +}; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" -bool initialize_keycode_map() +bool initialize_keycode_map(void) { + UniChar chars[255]; + UniCharCount len; + UInt32 state; + TISInputSourceRef keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); CFDataRef uchr = (CFDataRef) TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); CFRelease(keyboard); @@ -95,20 +98,33 @@ bool initialize_keycode_map() UCKeyboardLayout *keyboard_layout = (UCKeyboardLayout *) CFDataGetBytePtr(uchr); if (!keyboard_layout) return false; + free_keycode_map(); + table_free(&keymap_table); table_init(&keymap_table, - 131, + 61, (table_hash_func) hash_keymap, (table_compare_func) same_keymap); - for (unsigned index = 0; index < 128; ++index) { - CFStringRef key_string = cfstring_from_keycode(keyboard_layout, index); - if (!key_string) continue; - - char *c_key_string = copy_cf_string_to_c(key_string); - CFRelease(key_string); - if (!c_key_string) continue; - - table_add(&keymap_table, c_key_string, (void *)index); + for (int i = 0; i < array_count(layout_dependent_keycodes); ++i) { + if (UCKeyTranslate(keyboard_layout, + layout_dependent_keycodes[i], + kUCKeyActionDown, + 0, + LMGetKbdType(), + kUCKeyTranslateNoDeadKeysMask, + &state, + array_count(chars), + &len, + chars) == noErr && len > 0) { + CFStringRef key_cfstring = CFStringCreateWithCharacters(NULL, chars, len); + char *key_cstring = copy_cfstring(key_cfstring); + CFRelease(key_cfstring); + + if (key_cstring) { + table_add(&keymap_table, key_cstring, (void *)layout_dependent_keycodes[i]); + buf_push(keymap_keys, key_cstring); + } + } } return true; diff --git a/src/locale.h b/src/locale.h index 653bcfd..7a1ec6e 100644 --- a/src/locale.h +++ b/src/locale.h @@ -3,7 +3,15 @@ #include <stdint.h> -bool initialize_keycode_map(); +#define CF_NOTIFICATION_CALLBACK(name) \ + void name(CFNotificationCenterRef center, \ + void *observer, \ + CFNotificationName name, \ + const void *object, \ + CFDictionaryRef userInfo) +typedef CF_NOTIFICATION_CALLBACK(cf_notification_callback); + +bool initialize_keycode_map(void); uint32_t keycode_from_char(char key); #endif diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..88c12a2 --- /dev/null +++ b/src/log.h @@ -0,0 +1,42 @@ +#ifndef SKHD_LOG_H +#define SKHD_LOG_H + +#define internal static +#define global static + +global bool verbose; + +internal inline void +debug(const char *format, ...) +{ + if (!verbose) return; + + va_list args; + va_start(args, format); + vfprintf(stdout, format, args); + va_end(args); +} + +internal inline void +warn(const char *format, ...) +{ + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +internal inline void +error(const char *format, ...) +{ + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + exit(EXIT_FAILURE); +} + +#undef internal +#undef global + +#endif diff --git a/src/parse.c b/src/parse.c index 79f4aba..e2b4c4d 100644 --- a/src/parse.c +++ b/src/parse.c @@ -22,8 +22,6 @@ find_or_init_default_mode(struct parser *parser) } default_mode = malloc(sizeof(struct mode)); - default_mode->line = -1; - default_mode->cursor = -1; default_mode->name = copy_string("default"); table_init(&default_mode->hotkey_map, 131, @@ -74,13 +72,63 @@ keycode_from_hex(char *hex) return result; } -internal char * -parse_command(struct parser *parser) +internal void +parse_command(struct parser *parser, struct hotkey *hotkey) { struct token command = parser_previous(parser); char *result = copy_string_count(command.text, command.length); - printf("\tcmd: '%s'\n", result); - return result; + debug("\tcmd: '%s'\n", result); + buf_push(hotkey->command, result); +} + +internal void +parse_process_command_list(struct parser *parser, struct hotkey *hotkey) +{ + if (parser_match(parser, Token_String)) { + struct token name_token = parser_previous(parser); + char *name = copy_string_count(name_token.text, name_token.length); + for (char *s = name; *s; ++s) *s = tolower(*s); + buf_push(hotkey->process_name, name); + if (parser_match(parser, Token_Command)) { + parse_command(parser, hotkey); + parse_process_command_list(parser, hotkey); + } else if (parser_match(parser, Token_Unbound)) { + buf_push(hotkey->command, NULL); + parse_process_command_list(parser, hotkey); + } else { + parser_report_error(parser, parser_peek(parser), "expected '~' or ':' followed by command\n"); + } + } else if (parser_match(parser, Token_Wildcard)) { + if (parser_match(parser, Token_Command)) { + struct token command = parser_previous(parser); + char *result = copy_string_count(command.text, command.length); + debug("\tcmd: '%s'\n", result); + hotkey->wildcard_command = result; + parse_process_command_list(parser, hotkey); + } else if (parser_match(parser, Token_Unbound)) { + hotkey->wildcard_command = NULL; + parse_process_command_list(parser, hotkey); + } else { + parser_report_error(parser, parser_peek(parser), "expected '~' or ':' followed by command\n"); + } + } else if (parser_match(parser, Token_EndList)) { + if (!buf_len(hotkey->process_name)) { + parser_report_error(parser, parser_previous(parser), "list must contain at least one value\n"); + } + } else { + parser_report_error(parser, parser_peek(parser), "expected process command mapping or ']'\n"); + } +} + +internal void +parse_activate(struct parser *parser, struct hotkey *hotkey) +{ + parse_command(parser, hotkey); + hotkey->flags |= Hotkey_Flag_Activate; + + if (!table_find(parser->mode_map, hotkey->command[0])) { + parser_report_error(parser, parser_previous(parser), "undeclared identifier\n"); + } } internal uint32_t @@ -90,7 +138,7 @@ parse_key_hex(struct parser *parser) char *hex = copy_string_count(key.text, key.length); uint32_t keycode = keycode_from_hex(hex); free(hex); - printf("\tkey: '%.*s' (0x%02x)\n", key.length, key.text, keycode); + debug("\tkey: '%.*s' (0x%02x)\n", key.length, key.text, keycode); return keycode; } @@ -100,7 +148,7 @@ parse_key(struct parser *parser) uint32_t keycode; struct token key = parser_previous(parser); keycode = keycode_from_char(*key.text); - printf("\tkey: '%c' (0x%02x)\n", *key.text, keycode); + debug("\tkey: '%c' (0x%02x)\n", *key.text, keycode); return keycode; } @@ -146,7 +194,7 @@ parse_key_literal(struct parser *parser, struct hotkey *hotkey) if (token_equals(key, literal_keycode_str[i])) { handle_implicit_literal_flags(hotkey, i); hotkey->key = literal_keycode_value[i]; - printf("\tkey: '%.*s' (0x%02x)\n", key.length, key.text, hotkey->key); + debug("\tkey: '%.*s' (0x%02x)\n", key.length, key.text, hotkey->key); break; } } @@ -158,7 +206,7 @@ internal enum hotkey_flag modifier_flags_value[] = Hotkey_Flag_Shift, Hotkey_Flag_LShift, Hotkey_Flag_RShift, Hotkey_Flag_Cmd, Hotkey_Flag_LCmd, Hotkey_Flag_RCmd, Hotkey_Flag_Control, Hotkey_Flag_LControl, Hotkey_Flag_RControl, - Hotkey_Flag_Fn, Hotkey_Flag_Hyper, + Hotkey_Flag_Fn, Hotkey_Flag_Hyper, Hotkey_Flag_Meh, }; internal uint32_t @@ -170,7 +218,7 @@ parse_modifier(struct parser *parser) for (int i = 0; i < array_count(modifier_flags_str); ++i) { if (token_equals(modifier, modifier_flags_str[i])) { flags |= modifier_flags_value[i]; - printf("\tmod: '%s'\n", modifier_flags_str[i]); + debug("\tmod: '%s'\n", modifier_flags_str[i]); break; } } @@ -179,7 +227,7 @@ parse_modifier(struct parser *parser) if (parser_match(parser, Token_Modifier)) { flags |= parse_modifier(parser); } else { - parser_report_error(parser, Error_Unexpected_Token, "expected modifier"); + parser_report_error(parser, parser_peek(parser), "expected modifier\n"); } } @@ -198,18 +246,18 @@ parse_mode(struct parser *parser, struct hotkey *hotkey) if (!mode && token_equals(identifier, "default")) { mode = find_or_init_default_mode(parser); } else if (!mode) { - parser_report_error(parser, Error_Undeclared_Ident, "undeclared identifier"); + parser_report_error(parser, identifier, "undeclared identifier\n"); return; } buf_push(hotkey->mode_list, mode); - printf("\tmode: '%s'\n", mode->name); + debug("\tmode: '%s'\n", mode->name); if (parser_match(parser, Token_Comma)) { if (parser_match(parser, Token_Identifier)) { parse_mode(parser, hotkey); } else { - parser_report_error(parser, Error_Unexpected_Token, "expected identifier"); + parser_report_error(parser, parser_peek(parser), "expected identifier\n"); } } } @@ -221,7 +269,7 @@ parse_hotkey(struct parser *parser) memset(hotkey, 0, sizeof(struct hotkey)); bool found_modifier; - printf("hotkey :: #%d {\n", parser->current_token.line); + debug("hotkey :: #%d {\n", parser->current_token.line); if (parser_match(parser, Token_Identifier)) { parse_mode(parser, hotkey); @@ -232,7 +280,7 @@ parse_hotkey(struct parser *parser) if (buf_len(hotkey->mode_list) > 0) { if (!parser_match(parser, Token_Insert)) { - parser_report_error(parser, Error_Unexpected_Token, "expected '<'"); + parser_report_error(parser, parser_peek(parser), "expected '<'\n"); goto err; } } else { @@ -248,7 +296,7 @@ parse_hotkey(struct parser *parser) if (found_modifier) { if (!parser_match(parser, Token_Dash)) { - parser_report_error(parser, Error_Unexpected_Token, "expected '-'"); + parser_report_error(parser, parser_peek(parser), "expected '-'\n"); goto err; } } @@ -260,7 +308,7 @@ parse_hotkey(struct parser *parser) } else if (parser_match(parser, Token_Literal)) { parse_key_literal(parser, hotkey); } else { - parser_report_error(parser, Error_Unexpected_Token, "expected key-literal"); + parser_report_error(parser, parser_peek(parser), "expected key-literal\n"); goto err; } @@ -269,20 +317,23 @@ parse_hotkey(struct parser *parser) } if (parser_match(parser, Token_Command)) { - hotkey->command = parse_command(parser); + parse_command(parser, hotkey); + } else if (parser_match(parser, Token_BeginList)) { + parse_process_command_list(parser, hotkey); + if (parser->error) { + goto err; + } } else if (parser_match(parser, Token_Activate)) { - hotkey->flags |= Hotkey_Flag_Activate; - hotkey->command = parse_command(parser); - if (!table_find(parser->mode_map, hotkey->command)) { - parser_report_error(parser, Error_Undeclared_Ident, "undeclared identifier"); + parse_activate(parser, hotkey); + if (parser->error) { goto err; } } else { - parser_report_error(parser, Error_Unexpected_Token, "expected ':' followed by command or ';' followed by mode"); + parser_report_error(parser, parser_peek(parser), "expected ':' followed by command or ';' followed by mode\n"); goto err; } - printf("}\n"); + debug("}\n"); return hotkey; err: @@ -296,8 +347,6 @@ parse_mode_decl(struct parser *parser) struct mode *mode = malloc(sizeof(struct mode)); struct token identifier = parser_previous(parser); - mode->line = identifier.line; - mode->cursor = identifier.cursor; mode->name = copy_string_count(identifier.text, identifier.length); table_init(&mode->hotkey_map, 131, @@ -321,32 +370,96 @@ parse_mode_decl(struct parser *parser) void parse_declaration(struct parser *parser) { - struct mode *mode; parser_match(parser, Token_Decl); if (parser_match(parser, Token_Identifier)) { - mode = parse_mode_decl(parser); + struct token identifier = parser_previous(parser); + struct mode *mode = parse_mode_decl(parser); if (table_find(parser->mode_map, mode->name)) { - parser_report_error(parser, Error_Duplicate_Ident, - "#%d:%d duplicate declaration '%s'\n", - mode->line, mode->cursor, mode->name); + parser_report_error(parser, identifier, "duplicate declaration '%s'\n", mode->name); } else { table_add(parser->mode_map, mode->name, mode); } } else { - parser_report_error(parser, Error_Unexpected_Token, "expected identifier"); + parser_report_error(parser, parser_peek(parser), "expected identifier\n"); + } +} + +void parse_option_blacklist(struct parser *parser) +{ + if (parser_match(parser, Token_String)) { + struct token name_token = parser_previous(parser); + char *name = copy_string_count(name_token.text, name_token.length); + for (char *s = name; *s; ++s) *s = tolower(*s); + debug("\t%s\n", name); + table_add(parser->blacklst, name, name); + parse_option_blacklist(parser); + } else if (parser_match(parser, Token_EndList)) { + if (parser->blacklst->count == 0) { + parser_report_error(parser, parser_previous(parser), "list must contain at least one value\n"); + } + } else { + parser_report_error(parser, parser_peek(parser), "expected process name or ']'\n"); } } -void parse_config(struct parser *parser) +void parse_option_load(struct parser *parser, struct token option) +{ + struct token filename_token = parser_previous(parser); + char *filename = copy_string_count(filename_token.text, filename_token.length); + debug("\t%s\n", filename); + + if (*filename != '/') { + char *directory = file_directory(parser->file); + + size_t directory_length = strlen(directory); + size_t filename_length = strlen(filename); + size_t total_length = directory_length + filename_length + 2; + + char *absolutepath = malloc(total_length * sizeof(char)); + snprintf(absolutepath, total_length, "%s/%s", directory, filename); + free(filename); + + filename = absolutepath; + } + + buf_push(parser->load_directives, ((struct load_directive) { + .file = filename, + .option = option + })); +} + +void parse_option(struct parser *parser) +{ + parser_match(parser, Token_Option); + struct token option = parser_previous(parser); + if (token_equals(option, "blacklist")) { + if (parser_match(parser, Token_BeginList)) { + debug("blacklist :: #%d {\n", option.line); + parse_option_blacklist(parser); + debug("}\n"); + } else { + parser_report_error(parser, option, "expected '[' followed by list of process names\n"); + } + } else if (token_equals(option, "load")) { + if (parser_match(parser, Token_String)) { + debug("load :: #%d {\n", option.line); + parse_option_load(parser, option); + debug("}\n"); + } else { + parser_report_error(parser, option, "expected filename\n"); + } + } else { + parser_report_error(parser, option, "invalid option specified\n"); + } +} + +bool parse_config(struct parser *parser) { struct mode *mode; struct hotkey *hotkey; while (!parser_eof(parser)) { - if (parser->error) { - free_mode_map(parser->mode_map); - return; - } + if (parser->error) break; if ((parser_check(parser, Token_Identifier)) || (parser_check(parser, Token_Modifier)) || @@ -361,10 +474,64 @@ void parse_config(struct parser *parser) } } else if (parser_check(parser, Token_Decl)) { parse_declaration(parser); + } else if (parser_check(parser, Token_Option)) { + parse_option(parser); } else { - parser_report_error(parser, Error_Unexpected_Token, "expected decl, modifier or key-literal"); + parser_report_error(parser, parser_peek(parser), "expected decl, modifier or key-literal\n"); } } + + if (parser->error) { + free_mode_map(parser->mode_map); + free_blacklist(parser->blacklst); + return false; + } + + return true; +} + +struct hotkey * +parse_keypress(struct parser *parser) +{ + if ((parser_check(parser, Token_Modifier)) || + (parser_check(parser, Token_Literal)) || + (parser_check(parser, Token_Key_Hex)) || + (parser_check(parser, Token_Key))) { + struct hotkey *hotkey = malloc(sizeof(struct hotkey)); + memset(hotkey, 0, sizeof(struct hotkey)); + bool found_modifier; + + if ((found_modifier = parser_match(parser, Token_Modifier))) { + hotkey->flags = parse_modifier(parser); + if (parser->error) { + goto err; + } + } + + if (found_modifier) { + if (!parser_match(parser, Token_Dash)) { + goto err; + } + } + + if (parser_match(parser, Token_Key)) { + hotkey->key = parse_key(parser); + } else if (parser_match(parser, Token_Key_Hex)) { + hotkey->key = parse_key_hex(parser); + } else if (parser_match(parser, Token_Literal)) { + parse_key_literal(parser, hotkey); + } else { + goto err; + } + + return hotkey; + + err: + free(hotkey); + return NULL; + } + + return NULL; } struct token @@ -411,33 +578,58 @@ bool parser_match(struct parser *parser, enum token_type type) return false; } -void parser_report_error(struct parser *parser, enum parse_error_type error_type, const char *format, ...) +void parser_report_error(struct parser *parser, struct token token, const char *format, ...) { va_list args; va_start(args, format); + fprintf(stderr, "#%d:%d ", token.line, token.cursor); + vfprintf(stderr, format, args); + va_end(args); + parser->error = true; +} + +void parser_do_directives(struct parser *parser, struct hotloader *hotloader, bool thwart_hotloader) +{ + bool error = false; + + for (int i = 0; i < buf_len(parser->load_directives); ++i) { + struct load_directive load = parser->load_directives[i]; + + struct parser directive_parser; + if (parser_init(&directive_parser, parser->mode_map, parser->blacklst, load.file)) { + if (!thwart_hotloader) { + hotloader_add_file(hotloader, load.file); + } + + if (parse_config(&directive_parser)) { + parser_do_directives(&directive_parser, hotloader, thwart_hotloader); + } else { + error = true; + } + + parser_destroy(&directive_parser); + } else { + warn("skhd: could not open file '%s' from load directive #%d:%d\n", load.file, load.option.line, load.option.cursor); + } - if (error_type == Error_Unexpected_Token) { - fprintf(stderr, "#%d:%d ", parser->current_token.line, parser->current_token.cursor); - vfprintf(stderr, format, args); - fprintf(stderr, ", but got '%.*s'\n", parser->current_token.length, parser->current_token.text); - } else if (error_type == Error_Undeclared_Ident) { - fprintf(stderr, "#%d:%d ", parser->previous_token.line, parser->previous_token.cursor); - vfprintf(stderr, format, args); - fprintf(stderr, " '%.*s'\n", parser->previous_token.length, parser->previous_token.text); - } else if (error_type == Error_Duplicate_Ident) { - vfprintf(stderr, format, args); + free(load.file); } + buf_free(parser->load_directives); - va_end(args); - parser->error = true; + if (error) { + free_mode_map(parser->mode_map); + free_blacklist(parser->blacklst); + } } -bool parser_init(struct parser *parser, struct table *mode_map, char *file) +bool parser_init(struct parser *parser, struct table *mode_map, struct table *blacklst, char *file) { memset(parser, 0, sizeof(struct parser)); char *buffer = read_file(file); if (buffer) { + parser->file = file; parser->mode_map = mode_map; + parser->blacklst = blacklst; tokenizer_init(&parser->tokenizer, buffer); parser_advance(parser); return true; @@ -445,6 +637,14 @@ bool parser_init(struct parser *parser, struct table *mode_map, char *file) return false; } +bool parser_init_text(struct parser *parser, char *text) +{ + memset(parser, 0, sizeof(struct parser)); + tokenizer_init(&parser->tokenizer, text); + parser_advance(parser); + return true; +} + void parser_destroy(struct parser *parser) { free(parser->tokenizer.buffer); diff --git a/src/parse.h b/src/parse.h index d3543b9..6576f7a 100644 --- a/src/parse.h +++ b/src/parse.h @@ -4,25 +4,27 @@ #include "tokenize.h" #include <stdbool.h> +struct load_directive +{ + char *file; + struct token option; +}; + struct table; struct parser { + char *file; struct token previous_token; struct token current_token; struct tokenizer tokenizer; struct table *mode_map; + struct table *blacklst; + struct load_directive *load_directives; bool error; }; -enum parse_error_type -{ - Error_Unexpected_Token, - Error_Undeclared_Ident, - Error_Duplicate_Ident, -}; - - -void parse_config(struct parser *parser); +bool parse_config(struct parser *parser); +struct hotkey *parse_keypress(struct parser *parser); struct token parser_peek(struct parser *parser); struct token parser_previous(struct parser *parser); @@ -30,8 +32,9 @@ bool parser_eof(struct parser *parser); struct token parser_advance(struct parser *parser); bool parser_check(struct parser *parser, enum token_type type); bool parser_match(struct parser *parser, enum token_type type); -bool parser_init(struct parser *parser, struct table *mode_map, char *file); +bool parser_init(struct parser *parser, struct table *mode_map, struct table *blacklst, char *file); +bool parser_init_text(struct parser *parser, char *text); void parser_destroy(struct parser *parser); -void parser_report_error(struct parser *parser, enum parse_error_type error_type, const char *format, ...); +void parser_report_error(struct parser *parser, struct token token, const char *format, ...); #endif diff --git a/src/sbuffer.h b/src/sbuffer.h index 92ac811..3558bbc 100644 --- a/src/sbuffer.h +++ b/src/sbuffer.h @@ -24,7 +24,9 @@ struct buf_hdr #define buf_last(b) ((b)[buf_len(b)-1]) #define buf_free(b) ((b) ? free(buf__hdr(b)) : 0) -static void *buf__grow_f(const void *buf, size_t new_len, size_t elem_size) +#define internal static + +internal void *buf__grow_f(const void *buf, size_t new_len, size_t elem_size) { size_t new_cap = MAX(1 + 2*buf_cap(buf), new_len); size_t new_size = OFFSETOF(struct buf_hdr, buf) + new_cap*elem_size; @@ -36,4 +38,6 @@ static void *buf__grow_f(const void *buf, size_t new_len, size_t elem_size) return new_hdr->buf; } +#undef internal + #endif @@ -5,88 +5,145 @@ #include <getopt.h> #include <signal.h> #include <string.h> +#include <fcntl.h> +#include <sys/file.h> +#include <sys/types.h> +#include <sys/uio.h> #include <unistd.h> #include <Carbon/Carbon.h> +#include "timing.h" +#include "log.h" #define HASHTABLE_IMPLEMENTATION #include "hashtable.h" #include "sbuffer.h" #include "hotload.h" #include "event_tap.h" #include "locale.h" +#include "carbon.h" #include "tokenize.h" #include "parse.h" #include "hotkey.h" +#include "synthesize.h" #include "mtouch.h" #include "hotload.c" #include "event_tap.c" #include "locale.c" +#include "carbon.c" #include "tokenize.c" #include "parse.c" #include "hotkey.c" +#include "synthesize.c" #include "mtouch.c" -#define internal static -extern bool CGSIsSecureEventInputSet(); +extern CFDictionaryRef CGSCopyCurrentSessionDictionary(void); +extern bool CGSIsSecureEventInputSet(void); #define secure_keyboard_entry_enabled CGSIsSecureEventInputSet -#if 0 -#define BEGIN_TIMED_BLOCK() \ - clock_t timed_block_begin = clock() -#define END_TIMED_BLOCK() \ - clock_t timed_block_end = clock(); \ - double timed_block_elapsed = ((timed_block_end - timed_block_begin) / (double)CLOCKS_PER_SEC) * 1000.0f; \ - printf("elapsed time: %.4fms\n", timed_block_elapsed) -#else -#define BEGIN_TIMED_BLOCK() -#define END_TIMED_BLOCK() -#endif - -internal unsigned major_version = 0; -internal unsigned minor_version = 2; -internal unsigned patch_version = 1; - -internal struct mode *current_mode; -internal struct table mode_map; -internal char *config_file; +#define internal static +#define global static -internal void -error(const char *format, ...) -{ - va_list args; - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); - exit(EXIT_FAILURE); -} +#define SKHD_CONFIG_FILE ".skhdrc" +#define SKHD_PIDFILE_FMT "/tmp/skhd_%s.pid" -internal void -warn(const char *format, ...) -{ - va_list args; - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); -} +global unsigned major_version = 0; +global unsigned minor_version = 3; +global unsigned patch_version = 4; + +global struct carbon_event carbon; +global struct event_tap event_tap; +global struct hotloader hotloader; +global struct mode *current_mode; +global struct table mode_map; +global struct table blacklst; +global bool thwart_hotloader; +global char *config_file; + +internal HOTLOADER_CALLBACK(config_handler); internal void parse_config_helper(char *absolutepath) { struct parser parser; - if (parser_init(&parser, &mode_map, absolutepath)) { - parse_config(&parser); + if (parser_init(&parser, &mode_map, &blacklst, absolutepath)) { + if (!thwart_hotloader) { + hotloader_end(&hotloader); + hotloader_add_file(&hotloader, absolutepath); + } + + if (parse_config(&parser)) { + parser_do_directives(&parser, &hotloader, thwart_hotloader); + } parser_destroy(&parser); + + if (!thwart_hotloader) { + if (hotloader_begin(&hotloader, config_handler)) { + debug("skhd: watching files for changes:\n", absolutepath); + for (int i = 0; i < hotloader.watch_count; ++i) { + debug("\t%s\n", hotloader.watch_list[i].file_info.absolutepath); + } + } else { + warn("skhd: could not start watcher.. hotloading is not enabled\n"); + } + } } else { warn("skhd: could not open file '%s'\n", absolutepath); } + current_mode = table_find(&mode_map, "default"); } internal HOTLOADER_CALLBACK(config_handler) { + BEGIN_TIMED_BLOCK("hotload_config"); + debug("skhd: config-file has been modified.. reloading config\n"); free_mode_map(&mode_map); - parse_config_helper(absolutepath); + free_blacklist(&blacklst); + parse_config_helper(config_file); + END_TIMED_BLOCK(); +} + +internal CF_NOTIFICATION_CALLBACK(keymap_handler) +{ + BEGIN_TIMED_BLOCK("keymap_changed"); + if (initialize_keycode_map()) { + debug("skhd: input source changed.. reloading config\n"); + free_mode_map(&mode_map); + free_blacklist(&blacklst); + parse_config_helper(config_file); + } + END_TIMED_BLOCK(); +} + +internal EVENT_TAP_CALLBACK(key_observer_handler) +{ + switch (type) { + case kCGEventTapDisabledByTimeout: + case kCGEventTapDisabledByUserInput: { + debug("skhd: restarting event-tap\n"); + struct event_tap *event_tap = (struct event_tap *) reference; + CGEventTapEnable(event_tap->handle, 1); + } break; + case kCGEventKeyDown: + case kCGEventFlagsChanged: { + uint32_t flags = CGEventGetFlags(event); + uint32_t keycode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + + if (keycode == kVK_ANSI_C && flags & 0x40000) { + exit(0); + } + + printf("\rkeycode: 0x%.2X\tflags: ", keycode); + for (int i = 31; i >= 0; --i) { + printf("%c", (flags & (1 << i)) ? '1' : '0'); + } + fflush(stdout); + + return NULL; + } break; + } + return event; } internal EVENT_TAP_CALLBACK(key_handler) @@ -94,23 +151,28 @@ internal EVENT_TAP_CALLBACK(key_handler) switch (type) { case kCGEventTapDisabledByTimeout: case kCGEventTapDisabledByUserInput: { - printf("skhd: restarting event-tap\n"); + debug("skhd: restarting event-tap\n"); struct event_tap *event_tap = (struct event_tap *) reference; CGEventTapEnable(event_tap->handle, 1); } break; case kCGEventKeyDown: { + if (table_find(&blacklst, carbon.process_name)) return event; if (!current_mode) return event; + BEGIN_TIMED_BLOCK("handle_keypress"); struct hotkey eventkey = create_eventkey(event); - bool result = find_and_exec_hotkey(&eventkey, &mode_map, ¤t_mode); + bool result = find_and_exec_hotkey(&eventkey, &mode_map, ¤t_mode, &carbon); + END_TIMED_BLOCK(); + if (result) return NULL; } break; case NX_SYSDEFINED: { + if (table_find(&blacklst, carbon.process_name)) return event; if (!current_mode) return event; - struct systemkey systemkey = create_systemkey(event); - if (systemkey.intercept) { - bool result = find_and_exec_hotkey(&systemkey.eventkey, &mode_map, ¤t_mode); + struct hotkey eventkey; + if (intercept_systemkey(event, &eventkey)) { + bool result = find_and_exec_hotkey(&eventkey, &mode_map, ¤t_mode, &carbon); if (result) return NULL; } } break; @@ -146,25 +208,137 @@ internal MULTITOUCH_CALLBACK(touch_handler) return 0; } +internal void +sigusr1_handler(int signal) +{ + BEGIN_TIMED_BLOCK("sigusr1"); + debug("skhd: SIGUSR1 received.. reloading config\n"); + free_mode_map(&mode_map); + free_blacklist(&blacklst); + parse_config_helper(config_file); + END_TIMED_BLOCK(); +} + +internal pid_t +read_pid_file(void) +{ + char pid_file[255] = {}; + pid_t pid = 0; + + char *user = getenv("USER"); + if (user) { + snprintf(pid_file, sizeof(pid_file), SKHD_PIDFILE_FMT, user); + } else { + error("skhd: could not create path to pid-file because 'env USER' was not set! abort..\n"); + } + + int handle = open(pid_file, O_RDWR); + if (handle == -1) { + error("skhd: could not open pid-file..\n"); + } + + if (flock(handle, LOCK_EX | LOCK_NB) == 0) { + error("skhd: could not locate existing instance..\n"); + } else if (read(handle, &pid, sizeof(pid_t)) == -1) { + error("skhd: could not read pid-file..\n"); + } + + close(handle); + return pid; +} + +internal void +create_pid_file(void) +{ + char pid_file[255] = {}; + pid_t pid = getpid(); + + char *user = getenv("USER"); + if (user) { + snprintf(pid_file, sizeof(pid_file), SKHD_PIDFILE_FMT, user); + } else { + error("skhd: could not create path to pid-file because 'env USER' was not set! abort..\n"); + } + + int handle = open(pid_file, O_CREAT | O_RDWR, 0644); + if (handle == -1) { + error("skhd: could not create pid-file! abort..\n"); + } + + struct flock lockfd = { + .l_start = 0, + .l_len = 0, + .l_pid = pid, + .l_type = F_WRLCK, + .l_whence = SEEK_SET + }; + + if (fcntl(handle, F_SETLK, &lockfd) == -1) { + error("skhd: could not lock pid-file! abort..\n"); + } else if (write(handle, &pid, sizeof(pid_t)) == -1) { + error("skhd: could not write pid-file! abort..\n"); + } + + // NOTE(koekeishiya): we intentionally leave the handle open, + // as calling close(..) will release the lock we just acquired. + + debug("skhd: successfully created pid-file..\n"); +} + internal bool parse_arguments(int argc, char **argv) { int option; - const char *short_option = "vc:"; + const char *short_option = "VPvc:k:t:rho"; struct option long_option[] = { + { "verbose", no_argument, NULL, 'V' }, + { "profile", no_argument, NULL, 'P' }, { "version", no_argument, NULL, 'v' }, { "config", required_argument, NULL, 'c' }, + { "no-hotload", no_argument, NULL, 'h' }, + { "key", required_argument, NULL, 'k' }, + { "text", required_argument, NULL, 't' }, + { "reload", no_argument, NULL, 'r' }, + { "observe", no_argument, NULL, 'o' }, { NULL, 0, NULL, 0 } }; while ((option = getopt_long(argc, argv, short_option, long_option, NULL)) != -1) { switch (option) { + case 'V': { + verbose = true; + } break; + case 'P': { + profile = true; + } break; case 'v': { printf("skhd version %d.%d.%d\n", major_version, minor_version, patch_version); return true; } break; case 'c': { - config_file = strdup(optarg); + config_file = copy_string(optarg); + } break; + case 'h': { + thwart_hotloader = true; + } break; + case 'k': { + synthesize_key(optarg); + return true; + } break; + case 't': { + synthesize_text(optarg); + return true; + } break; + case 'r': { + pid_t pid = read_pid_file(); + if (pid) kill(pid, SIGUSR1); + return true; + } break; + case 'o': { + event_tap.mask = (1 << kCGEventKeyDown) | + (1 << kCGEventFlagsChanged); + event_tap_begin(&event_tap, key_observer_handler); + CFRunLoopRun(); } break; } } @@ -173,7 +347,7 @@ parse_arguments(int argc, char **argv) } internal bool -check_privileges() +check_privileges(void) { bool result; const void *keys[] = { kAXTrustedCheckOptionPrompt }; @@ -192,31 +366,63 @@ check_privileges() } internal void -set_config_path() +use_default_config_path(void) { char *home = getenv("HOME"); - if (home) { - int length = strlen(home) + strlen("/.skhdrc"); - config_file = (char *) malloc(length + 1); - strcpy(config_file, home); - strcat(config_file, "/.skhdrc"); - } else { - config_file = strdup(".skhdrc"); + if (!home) { + error("skhd: could not locate config because 'env HOME' was not set! abort..\n"); } + + int home_len = strlen(home); + int config_len = strlen("/"SKHD_CONFIG_FILE); + int length = home_len + config_len; + config_file = malloc(length + 1); + memcpy(config_file, home, home_len); + memcpy(config_file + home_len, "/"SKHD_CONFIG_FILE, config_len); + config_file[length] = '\0'; } -int main(int argc, char **argv) +internal void +dump_secure_keyboard_entry_process_info(void) { - if (parse_arguments(argc, argv)) { - return EXIT_SUCCESS; + CFDictionaryRef session; + CFNumberRef pid_ref; + char *process_name; + pid_t pid; + + session = CGSCopyCurrentSessionDictionary(); + if (!session) goto err; + + pid_ref = (CFNumberRef) CFDictionaryGetValue(session, CFSTR("kCGSSessionSecureInputPID")); + if (!pid_ref) goto err; + + CFNumberGetValue(pid_ref, CFNumberGetType(pid_ref), &pid); + process_name = find_process_name_for_pid(pid); + + if (process_name) { + error("skhd: secure keyboard entry is enabled by (%lld) '%s'! abort..\n", pid, process_name); } +err: + error("skhd: secure keyboard entry is enabled! abort..\n"); +} + +int main(int argc, char **argv) +{ if (getuid() == 0 || geteuid() == 0) { error("skhd: running as root is not allowed! abort..\n"); } + if (parse_arguments(argc, argv)) { + return EXIT_SUCCESS; + } + + BEGIN_SCOPED_TIMED_BLOCK("total_time"); + BEGIN_SCOPED_TIMED_BLOCK("init"); + create_pid_file(); + if (secure_keyboard_entry_enabled()) { - error("skhd: secure keyboard entry is enabled! abort..\n"); + dump_secure_keyboard_entry_process_info(); } if (!check_privileges()) { @@ -227,24 +433,42 @@ int main(int argc, char **argv) error("skhd: could not initialize keycode map! abort..\n"); } + if (!carbon_event_init(&carbon)) { + error("skhd: could not initialize carbon events! abort..\n"); + } + if (!config_file) { - set_config_path(); + use_default_config_path(); } - printf("skhd: using config '%s'\n", config_file); - table_init(&mode_map, 13, (table_hash_func) hash_mode, (table_compare_func) same_mode); - parse_config_helper(config_file); + CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), + NULL, + &keymap_handler, + kTISNotifySelectedKeyboardInputSourceChanged, + NULL, + CFNotificationSuspensionBehaviorCoalesce); + signal(SIGCHLD, SIG_IGN); + signal(SIGUSR1, sigusr1_handler); + + init_shell(); + table_init(&mode_map, 13, (table_hash_func) hash_string, (table_compare_func) compare_string); + table_init(&blacklst, 13, (table_hash_func) hash_string, (table_compare_func) compare_string); + END_SCOPED_TIMED_BLOCK(); - struct event_tap event_tap = { .mask = (1 << kCGEventKeyDown) | (1 << NX_SYSDEFINED) }; + BEGIN_SCOPED_TIMED_BLOCK("parse_config"); + debug("skhd: using config '%s'\n", config_file); + parse_config_helper(config_file); + END_SCOPED_TIMED_BLOCK(); + + BEGIN_SCOPED_TIMED_BLOCK("begin_eventtap"); + event_tap.mask = (1 << kCGEventKeyDown) | (1 << NX_SYSDEFINED); event_tap_begin(&event_tap, key_handler); + END_SCOPED_TIMED_BLOCK(); struct multitouch multitouch; multitouch_begin(&multitouch, touch_handler); - - struct hotloader hotloader = {}; - hotloader_add_file(&hotloader, config_file); - hotloader_begin(&hotloader, config_handler); + END_SCOPED_TIMED_BLOCK(); CFRunLoopRun(); return EXIT_SUCCESS; diff --git a/src/synthesize.c b/src/synthesize.c new file mode 100644 index 0000000..6eddd6f --- /dev/null +++ b/src/synthesize.c @@ -0,0 +1,93 @@ +#include <Carbon/Carbon.h> + +#include "synthesize.h" +#include "locale.h" +#include "parse.h" +#include "hotkey.h" + +#define internal static + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + +internal inline void +create_and_post_keyevent(uint16_t key, bool pressed) +{ + CGPostKeyboardEvent((CGCharCode)0, (CGKeyCode)key, pressed); +} + +internal inline void +synthesize_modifiers(struct hotkey *hotkey, bool pressed) +{ + if (has_flags(hotkey, Hotkey_Flag_Alt)) { + create_and_post_keyevent(Modifier_Keycode_Alt, pressed); + } + + if (has_flags(hotkey, Hotkey_Flag_Shift)) { + create_and_post_keyevent(Modifier_Keycode_Shift, pressed); + } + + if (has_flags(hotkey, Hotkey_Flag_Cmd)) { + create_and_post_keyevent(Modifier_Keycode_Cmd, pressed); + } + + if (has_flags(hotkey, Hotkey_Flag_Control)) { + create_and_post_keyevent(Modifier_Keycode_Ctrl, pressed); + } + + if (has_flags(hotkey, Hotkey_Flag_Fn)) { + create_and_post_keyevent(Modifier_Keycode_Fn, pressed); + } +} + +void synthesize_key(char *key_string) +{ + if (!initialize_keycode_map()) return; + + struct parser parser; + parser_init_text(&parser, key_string); + + close(1); + close(2); + + struct hotkey *hotkey = parse_keypress(&parser); + if (!hotkey) return; + + CGSetLocalEventsSuppressionInterval(0.0f); + CGEnableEventStateCombining(false); + + synthesize_modifiers(hotkey, true); + create_and_post_keyevent(hotkey->key, true); + + create_and_post_keyevent(hotkey->key, false); + synthesize_modifiers(hotkey, false); +} + +void synthesize_text(char *text) +{ + CFStringRef text_ref = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); + CFIndex text_length = CFStringGetLength(text_ref); + + CGEventRef de = CGEventCreateKeyboardEvent(NULL, 0, true); + CGEventRef ue = CGEventCreateKeyboardEvent(NULL, 0, false); + + CGEventSetFlags(de, 0); + CGEventSetFlags(ue, 0); + + UniChar c; + for (CFIndex i = 0; i < text_length; ++i) + { + c = CFStringGetCharacterAtIndex(text_ref, i); + CGEventKeyboardSetUnicodeString(de, 1, &c); + CGEventPost(kCGAnnotatedSessionEventTap, de); + usleep(1000); + CGEventKeyboardSetUnicodeString(ue, 1, &c); + CGEventPost(kCGAnnotatedSessionEventTap, ue); + } + + CFRelease(ue); + CFRelease(de); + CFRelease(text_ref); +} + +#pragma clang diagnostic pop diff --git a/src/synthesize.h b/src/synthesize.h new file mode 100644 index 0000000..4a2b262 --- /dev/null +++ b/src/synthesize.h @@ -0,0 +1,7 @@ +#ifndef SKHD_SYNTHESIZE_H +#define SKHD_SYNTHESIZE_H + +void synthesize_key(char *key_string); +void synthesize_text(char *text); + +#endif diff --git a/src/timing.h b/src/timing.h new file mode 100644 index 0000000..44cde56 --- /dev/null +++ b/src/timing.h @@ -0,0 +1,76 @@ +#ifndef MACOS_TIMING_H +#define MACOS_TIMING_H + +#include <stdint.h> +#include <CoreAudio/CoreAudio.h> + +#define BEGIN_SCOPED_TIMED_BLOCK(note) \ + do { \ + struct timing_info timing; \ + if (profile) begin_timing(&timing, note) +#define END_SCOPED_TIMED_BLOCK() \ + if (profile) end_timing(&timing); \ + } while (0) + +#define BEGIN_TIMED_BLOCK(note) \ + struct timing_info timing; \ + if (profile) begin_timing(&timing, note) +#define END_TIMED_BLOCK() \ + if (profile) end_timing(&timing) + +#define internal static +#define global static + +global bool profile; + +struct timing_info +{ + char *note; + uint64_t start; + uint64_t end; + float ms; +}; + +void begin_timing(struct timing_info *timing, char *note); +void end_timing(struct timing_info *timing); + +internal inline uint64_t +macos_get_wall_clock(void) +{ + uint64_t result = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()); + return result; +} + +internal inline float +macos_get_seconds_elapsed(uint64_t start, uint64_t end) +{ + float result = ((float)(end - start) / 1000.0f) / 1000000.0f; + return result; +} + +internal inline float +macos_get_milliseconds_elapsed(uint64_t start, uint64_t end) +{ + float result = 1000.0f * macos_get_seconds_elapsed(start, end); + return result; +} + +void begin_timing(struct timing_info *timing, char *note) { + timing->note = note; + timing->start = macos_get_wall_clock(); +} + +void end_timing(struct timing_info *timing) { + timing->end = macos_get_wall_clock(); + timing->ms = macos_get_milliseconds_elapsed(timing->start, timing->end); + if (timing->note) { + printf("%6.4fms (%s)\n", timing->ms, timing->note); + } else { + printf("%6.4fms\n", timing->ms); + } +} + +#undef internal +#undef global + +#endif diff --git a/src/tokenize.c b/src/tokenize.c index 5040a7c..eaf0e3a 100644 --- a/src/tokenize.c +++ b/src/tokenize.c @@ -63,6 +63,30 @@ eat_hex(struct tokenizer *tokenizer) } } +internal void +eat_string(struct tokenizer *tokenizer) +{ + /* + * NOTE(koekeishiya): This is NOT proper string parsing code, as we do + * not check for escaped '"' here. At the time of writing, this is only + * supposed to be used for parsing names of processes, and such names + * should not contain escaped quotes at all. We are lazy and simply do + * the most basic implementation that fulfills our current requirement. + */ + + while (*tokenizer->at && *tokenizer->at != '"') { + advance(tokenizer); + } +} + +internal void +eat_option(struct tokenizer *tokenizer) +{ + while (*tokenizer->at && !isspace(*tokenizer->at)) { + advance(tokenizer); + } +} + internal inline bool isidentifier(char c) { @@ -130,6 +154,30 @@ get_token(struct tokenizer *tokenizer) case ',': { token.type = Token_Comma; } break; case '<': { token.type = Token_Insert; } break; case '@': { token.type = Token_Capture; } break; + case '~': { token.type = Token_Unbound; } break; + case '*': { token.type = Token_Wildcard; } break; + case '[': { token.type = Token_BeginList; } break; + case ']': { token.type = Token_EndList; } break; + case '.': { + token.text = tokenizer->at; + token.line = tokenizer->line; + token.cursor = tokenizer->cursor; + + eat_option(tokenizer); + token.length = tokenizer->at - token.text; + token.type = Token_Option; + } break; + case '"': { + token.text = tokenizer->at; + token.line = tokenizer->line; + token.cursor = tokenizer->cursor; + + eat_string(tokenizer); + token.length = tokenizer->at - token.text; + token.type = Token_String; + + advance(tokenizer); + } break; case '#': { eat_comment(tokenizer); token = get_token(tokenizer); diff --git a/src/tokenize.h b/src/tokenize.h index d6bd4bf..52d76fc 100644 --- a/src/tokenize.h +++ b/src/tokenize.h @@ -1,29 +1,31 @@ #ifndef SKHD_TOKENIZE_H #define SKHD_TOKENIZE_H -static const char *modifier_flags_str[] = +#define global static + +global const char *modifier_flags_str[] = { "alt", "lalt", "ralt", "shift", "lshift", "rshift", "cmd", "lcmd", "rcmd", "ctrl", "lctrl", "rctrl", - "fn", "hyper", + "fn", "hyper", "meh", }; -static const char *literal_keycode_str[] = +global const char *literal_keycode_str[] = { - "return", "tab", "space", - "backspace", "escape", "delete", - "home", "end", "pageup", - "pagedown", "insert", "left", - "right", "up", "down", - "f1", "f2", "f3", - "f4", "f5", "f6", - "f7", "f8", "f9", - "f10", "f11", "f12", - "f13", "f14", "f15", - "f16", "f17", "f18", - "f19", "f20", + "return", "tab", "space", + "backspace", "escape", "delete", + "home", "end", "pageup", + "pagedown", "insert", "left", + "right", "up", "down", + "f1", "f2", "f3", + "f4", "f5", "f6", + "f7", "f8", "f9", + "f10", "f11", "f12", + "f13", "f14", "f15", + "f16", "f17", "f18", + "f19", "f20", "sound_up", "sound_down", "mute", "play", "previous", "next", @@ -31,6 +33,8 @@ static const char *literal_keycode_str[] = "brightness_down", "illumination_up", "illumination_down" }; +#undef global + enum token_type { Token_Identifier, @@ -49,6 +53,13 @@ enum token_type Token_Dash, Token_Arrow, Token_Capture, + Token_Unbound, + Token_Wildcard, + Token_String, + Token_Option, + + Token_BeginList, + Token_EndList, Token_Unknown, Token_EndOfStream, |