Repository: asmvik/skhd Branch: master Commit: a7105d5b3db6 Files: 28 Total size: 109.1 KB Directory structure: gitextract_za2cgzfj/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── examples/ │ └── skhdrc ├── makefile └── src/ ├── carbon.c ├── carbon.h ├── event_tap.c ├── event_tap.h ├── hashtable.h ├── hotkey.c ├── hotkey.h ├── hotload.c ├── hotload.h ├── locale.c ├── locale.h ├── log.h ├── notify.c ├── parse.c ├── parse.h ├── sbuffer.h ├── service.h ├── skhd.c ├── synthesize.c ├── synthesize.h ├── timing.h ├── tokenize.c └── tokenize.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ bin/ ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2017 Åsmund Vikane Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ > **NOTE**: If you are having issues with skhd, or are looking for feature expansion and future development, you may want to check out [this backwards compatible skhd-port](https://github.com/jackielii/skhd.zig) written in Zig by [Jackie Li](https://github.com/jackielii). (I am not personally involved in this re-write.) **This repository is in maintenance mode; only critical issues that affect the core functionality of the software will be taken care of.** **skhd** is a simple hotkey daemon for macOS that focuses on responsiveness and performance. Hotkeys are defined in a text file through a simple DSL. **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. When running as a service (through launchd) log files can be found at `/tmp/skhd_$USER.out.log` and `/tmp/skhd_$USER.err.log`. list of features | feature | skhd | |:--------------------------:|:----:| | hotload config file | [x] | | hotkey passthrough | [x] | | modal hotkey-system | [x] | | application specific hotkey| [x] | | blacklist applications | [x] | | use media-keys as hotkey | [x] | | synthesize a key-press | [x] | ### Install The first time **skhd** is ran, it will request access to the accessibility API. After access has been granted, the application must be restarted. *Secure Keyboard Entry* must be disabled for **skhd** to receive key-events. **Homebrew**: Requires xcode-8 command-line tools. brew install asmvik/formulae/skhd skhd --start-service **Source**: Requires xcode-8 command-line tools. git clone https://github.com/asmvik/skhd make install # release version make # debug version ### Usage ``` --install-service: Install launchd service file into ~/Library/LaunchAgents/com.asmvik.skhd.plist skhd --install-service --uninstall-service: Remove launchd service file ~/Library/LaunchAgents/com.asmvik.skhd.plist skhd --uninstall-service --start-service: Run skhd as a service through launchd skhd --start-service --restart-service: Restart skhd service skhd --restart-service --stop-service: Stop skhd service from running skhd --stop-service -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 The default configuration file is located at one of the following places (in order): - `$XDG_CONFIG_HOME/skhd/skhdrc` - `$HOME/.config/skhd/skhdrc` - `$HOME/.skhdrc` A different location can be specified with the *--config | -c* argument. A sample config is available [here](https://github.com/asmvik/skhd/blob/master/examples/skhdrc) A list of all built-in modifier and literal keywords can be found [here](https://github.com/asmvik/skhd/issues/1) A hotkey is written according to the following rules: ``` hotkey = '<' | mode = 'name of mode' | ',' action = '[' ']' | '->' '[' ']' ':' | '->' ':' ';' | '->' ';' keysym = '-' | mod = 'modifier keyword' | '+' key = | literal = 'single letter or built-in keyword' keycode = 'apple keyboard kVK_ values (0x3C)' proc_map_lst = * proc_map = ':' | '~' | '*' ':' | '*' '~' string = '"' 'sequence of characters' '"' 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 ~ = application is unbound and keypress is forwarded per usual, when specified in a ``` A mode is declared according to the following rules: ``` mode_decl = '::' '@' ':' | '::' ':' | '::' '@' | '::' name = desired name for this mode, @ = 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. 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. ``` 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" ] ``` ================================================ FILE: examples/skhdrc ================================================ # NOTE(asmvik): A list of all built-in modifier and literal keywords can # be found at https://github.com/asmvik/skhd/issues/1 # # A hotkey is written according to the following rules: # # hotkey = '<' | # # mode = 'name of mode' | ',' # # action = '[' ']' | '->' '[' ']' # ':' | '->' ':' # ';' | '->' ';' # # keysym = '-' | # # mod = 'modifier keyword' | '+' # # key = | # # literal = 'single letter or built-in keyword' # # keycode = 'apple keyboard kVK_ values (0x3C)' # # proc_map_lst = * # # proc_map = ':' | '~' | # '*' ':' | '*' '~' # # string = '"' 'sequence of characters' '"' # # 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 # # ~ = application is unbound and keypress is forwarded per usual, when specified in a # # NOTE(asmvik): A mode is declared according to the following rules: # # mode_decl = '::' '@' ':' | '::' ':' | # '::' '@' | '::' # # name = desired name for this mode, # # @ = 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. # # 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. # add an on_enter command to the default mode # :: default : yabai -m config active_window_border_color 0xff775759 # # defines a new mode 'test' with an on_enter command, that captures keypresses # :: test @ : yabai -m config active_window_border_color 0xff24ccaa # # from 'default' mode, activate mode 'test' # cmd - x ; test # # from 'test' mode, activate mode 'default' # test < cmd - x ; default # # 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 : /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) ================================================ FILE: makefile ================================================ FRAMEWORKS = -framework Cocoa -framework Carbon -framework CoreServices BUILD_PATH = ./bin BUILD_FLAGS = -std=c99 -Wall -g -O0 SKHD_SRC = ./src/skhd.c BINS = $(BUILD_PATH)/skhd .PHONY: all clean install all: clean $(BINS) install: BUILD_FLAGS=-std=c99 -Wall -O2 install: clean $(BINS) clean: rm -rf $(BUILD_PATH) $(BUILD_PATH)/skhd: $(SKHD_SRC) mkdir -p $(BUILD_PATH) clang $^ $(BUILD_FLAGS) $(FRAMEWORKS) -o $@ ================================================ FILE: src/carbon.c ================================================ #include "carbon.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" static 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); } static inline char * find_active_process_name(void) { ProcessSerialNumber psn; GetFrontProcess(&psn); return find_process_name_for_psn(&psn); } #pragma clang diagnostic pop static 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; } ================================================ FILE: src/carbon.h ================================================ #ifndef SKHD_CARBON_H #define SKHD_CARBON_H #include 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 ================================================ FILE: src/event_tap.c ================================================ #include "event_tap.h" bool event_tap_enabled(struct event_tap *event_tap) { bool result = (event_tap->handle && CGEventTapIsEnabled(event_tap->handle)); return result; } bool event_tap_begin(struct event_tap *event_tap, event_tap_callback *callback) { event_tap->handle = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, event_tap->mask, callback, event_tap); bool result = event_tap_enabled(event_tap); if (result) { event_tap->runloop_source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event_tap->handle, 0); CFRunLoopAddSource(CFRunLoopGetMain(), event_tap->runloop_source, kCFRunLoopCommonModes); } return result; } void event_tap_end(struct event_tap *event_tap) { if (event_tap_enabled(event_tap)) { CGEventTapEnable(event_tap->handle, false); CFMachPortInvalidate(event_tap->handle); CFRunLoopRemoveSource(CFRunLoopGetMain(), event_tap->runloop_source, kCFRunLoopCommonModes); CFRelease(event_tap->runloop_source); CFRelease(event_tap->handle); event_tap->handle = NULL; } } ================================================ FILE: src/event_tap.h ================================================ #ifndef SKHD_EVENT_TAP_H #define SKHD_EVENT_TAP_H #include #include struct event_tap { CFMachPortRef handle; CFRunLoopSourceRef runloop_source; CGEventMask mask; }; #define EVENT_TAP_CALLBACK(name) \ CGEventRef name(CGEventTapProxy proxy, \ CGEventType type, \ CGEventRef event, \ void *reference) typedef EVENT_TAP_CALLBACK(event_tap_callback); bool event_tap_enabled(struct event_tap *event_tap); bool event_tap_begin(struct event_tap *event_tap, event_tap_callback *callback); void event_tap_end(struct event_tap *event_tap); #endif ================================================ FILE: src/hashtable.h ================================================ #ifndef HASHTABLE_H #define HASHTABLE_H typedef unsigned long (*table_hash_func)(void *key); typedef int (*table_compare_func)(void *key_a, void *key_b); struct bucket { void *key; void *value; struct bucket *next; }; struct table { int count; int capacity; table_hash_func hash; table_compare_func compare; struct bucket **buckets; }; void table_init(struct table *table, int capacity, table_hash_func hash, table_compare_func compare); void table_free(struct table *table); void *table_find(struct table *table, void *key); void table_add(struct table *table, void *key, void *value); void *table_remove(struct table *table, void *key); void *table_reset(struct table *table, int *count); #endif #ifdef HASHTABLE_IMPLEMENTATION #include #include static struct bucket * table_new_bucket(void *key, void *value) { struct bucket *bucket = malloc(sizeof(struct bucket)); bucket->key = key; bucket->value = value; bucket->next = NULL; return bucket; } static struct bucket ** table_get_bucket(struct table *table, void *key) { struct bucket **bucket = table->buckets + (table->hash(key) % table->capacity); while (*bucket) { if (table->compare((*bucket)->key, key)) { break; } bucket = &(*bucket)->next; } return bucket; } void table_init(struct table *table, int capacity, table_hash_func hash, table_compare_func compare) { table->count = 0; table->capacity = capacity; table->hash = hash; table->compare = compare; table->buckets = malloc(sizeof(struct bucket *) * capacity); memset(table->buckets, 0, sizeof(struct bucket *) * capacity); } void table_free(struct table *table) { for (int i = 0; i < table->capacity; ++i) { struct bucket *next, *bucket = table->buckets[i]; while (bucket) { next = bucket->next; free(bucket); bucket = next; } } if (table->buckets) { free(table->buckets); table->buckets = NULL; } } void *table_find(struct table *table, void *key) { struct bucket *bucket = *table_get_bucket(table, key); return bucket ? bucket->value : NULL; } void table_add(struct table *table, void *key, void *value) { struct bucket **bucket = table_get_bucket(table, key); if (*bucket) { if (!(*bucket)->value) { (*bucket)->value = value; } } else { *bucket = table_new_bucket(key, value); ++table->count; } } void *table_remove(struct table *table, void *key) { void *result = NULL; struct bucket *next, **bucket = table_get_bucket(table, key); if (*bucket) { result = (*bucket)->value; next = (*bucket)->next; free(*bucket); *bucket = next; --table->count; } return result; } void *table_reset(struct table *table, int *count) { void **values; int capacity; int index; int item; capacity = table->capacity; *count = table->count; values = malloc(sizeof(void *) * table->count); item = 0; for (index = 0; index < capacity; ++index) { struct bucket *next, **bucket = table->buckets + index; while (*bucket) { values[item++] = (*bucket)->value; next = (*bucket)->next; free(*bucket); *bucket = next; --table->count; } } return values; } #undef HASHTABLE_IMPLEMENTATION #endif ================================================ FILE: src/hotkey.c ================================================ #include "hotkey.h" #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 #define LRMOD_CTRL 9 #define LRMOD_SHIFT 3 #define LMOD_OFFS 1 #define RMOD_OFFS 2 static char arg[] = "-c"; static char *shell = NULL; static uint32_t cgevent_lrmod_flag[] = { Event_Mask_Alt, Event_Mask_LAlt, Event_Mask_RAlt, Event_Mask_Shift, Event_Mask_LShift, Event_Mask_RShift, Event_Mask_Cmd, Event_Mask_LCmd, Event_Mask_RCmd, Event_Mask_Control, Event_Mask_LControl, Event_Mask_RControl, }; static uint32_t hotkey_lrmod_flag[] = { Hotkey_Flag_Alt, Hotkey_Flag_LAlt, Hotkey_Flag_RAlt, 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, }; static bool compare_lr_mod(struct hotkey *a, struct hotkey *b, int mod) { bool result = has_flags(a, hotkey_lrmod_flag[mod]) ? has_flags(b, hotkey_lrmod_flag[mod + LMOD_OFFS]) || has_flags(b, hotkey_lrmod_flag[mod + RMOD_OFFS]) || has_flags(b, hotkey_lrmod_flag[mod]) : has_flags(a, hotkey_lrmod_flag[mod + LMOD_OFFS]) == has_flags(b, hotkey_lrmod_flag[mod + LMOD_OFFS]) && has_flags(a, hotkey_lrmod_flag[mod + RMOD_OFFS]) == has_flags(b, hotkey_lrmod_flag[mod + RMOD_OFFS]) && has_flags(a, hotkey_lrmod_flag[mod]) == has_flags(b, hotkey_lrmod_flag[mod]); return result; } static bool compare_fn(struct hotkey *a, struct hotkey *b) { return has_flags(a, Hotkey_Flag_Fn) == has_flags(b, Hotkey_Flag_Fn); } static bool compare_nx(struct hotkey *a, struct hotkey *b) { return has_flags(a, Hotkey_Flag_NX) == has_flags(b, Hotkey_Flag_NX); } bool same_hotkey(struct hotkey *a, struct hotkey *b) { return compare_lr_mod(a, b, LRMOD_ALT) && compare_lr_mod(a, b, LRMOD_CMD) && compare_lr_mod(a, b, LRMOD_CTRL) && compare_lr_mod(a, b, LRMOD_SHIFT) && compare_fn(a, b) && compare_nx(a, b) && a->key == b->key; } unsigned long hash_hotkey(struct hotkey *a) { return a->key; } bool compare_string(char *a, char *b) { while (*a && *b && *a == *b) { ++a; ++b; } return *a == '\0' && *b == '\0'; } unsigned long hash_string(char *key) { unsigned long hash = 0, high; while(*key) { hash = (hash << 4) + *key++; high = hash & 0xF0000000; if(high) { hash ^= (high >> 24); } hash &= ~high; } return hash; } static inline void fork_and_exec(char *command) { int cpid = fork(); if (cpid == 0) { setsid(); char *exec[] = { shell, arg, command, NULL}; int status_code = execvp(exec[0], exec); exit(status_code); } } static inline void passthrough(struct hotkey *hotkey, uint32_t *capture) { *capture |= HOTKEY_PASSTHROUGH((int)has_flags(hotkey, Hotkey_Flag_Passthrough)); } static inline struct hotkey * find_hotkey(struct mode *mode, struct hotkey *hotkey, uint32_t *capture) { struct hotkey *result = table_find(&mode->hotkey_map, hotkey); if (result) *capture |= HOTKEY_FOUND; return result; } static inline bool should_capture_hotkey(uint32_t capture) { 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)); } static 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 should_capture_hotkey(c); } void free_mode_map(struct table *mode_map) { struct hotkey **freed_pointers = NULL; int mode_count; void **modes = table_reset(mode_map, &mode_count); for (int mode_index = 0; mode_index < mode_count; ++mode_index) { struct mode *mode = (struct mode *) modes[mode_index]; int hk_count; void **hotkeys = table_reset(&mode->hotkey_map, &hk_count); for (int hk_index = 0; hk_index < hk_count; ++hk_index) { struct hotkey *hotkey = (struct hotkey *) hotkeys[hk_index]; for (int i = 0; i < buf_len(freed_pointers); ++i) { if (freed_pointers[i] == hotkey) { goto next; } } buf_push(freed_pointers, hotkey); buf_free(hotkey->mode_list); 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:; } if (hk_count) free(hotkeys); if (mode->command) free(mode->command); if (mode->name) free(mode->name); free(mode); } if (mode_count) { free(modes); buf_free(freed_pointers); } } 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); } } static void cgevent_lrmod_flag_to_hotkey_lrmod_flag(CGEventFlags eventflags, uint32_t *flags, int mod) { enum osx_event_mask mask = cgevent_lrmod_flag[mod]; enum osx_event_mask lmask = cgevent_lrmod_flag[mod + LMOD_OFFS]; enum osx_event_mask rmask = cgevent_lrmod_flag[mod + RMOD_OFFS]; if ((eventflags & mask) == mask) { bool left = (eventflags & lmask) == lmask; bool right = (eventflags & rmask) == rmask; if (left) *flags |= hotkey_lrmod_flag[mod + LMOD_OFFS]; if (right) *flags |= hotkey_lrmod_flag[mod + RMOD_OFFS]; if (!left && !right) *flags |= hotkey_lrmod_flag[mod]; } } static uint32_t cgevent_flags_to_hotkey_flags(uint32_t eventflags) { uint32_t flags = 0; cgevent_lrmod_flag_to_hotkey_lrmod_flag(eventflags, &flags, LRMOD_ALT); cgevent_lrmod_flag_to_hotkey_lrmod_flag(eventflags, &flags, LRMOD_CMD); cgevent_lrmod_flag_to_hotkey_lrmod_flag(eventflags, &flags, LRMOD_CTRL); cgevent_lrmod_flag_to_hotkey_lrmod_flag(eventflags, &flags, LRMOD_SHIFT); if ((eventflags & Event_Mask_Fn) == Event_Mask_Fn) { flags |= Hotkey_Flag_Fn; } return flags; } struct hotkey create_eventkey(CGEventRef event) { struct hotkey eventkey = { .key = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode), .flags = cgevent_flags_to_hotkey_flags(CGEventGetFlags(event)) }; return eventkey; } bool intercept_systemkey(CGEventRef event, struct hotkey *eventkey) { CFDataRef event_data = CGEventCreateData(kCFAllocatorDefault, event); 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); 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"; } } ================================================ FILE: src/hotkey.h ================================================ #ifndef SKHD_HOTKEY_H #define SKHD_HOTKEY_H #include #include #include #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, Event_Mask_LAlt = 0x00000020, Event_Mask_RAlt = 0x00000040, Event_Mask_Shift = 0x00020000, Event_Mask_LShift = 0x00000002, Event_Mask_RShift = 0x00000004, Event_Mask_Cmd = 0x00100000, Event_Mask_LCmd = 0x00000008, Event_Mask_RCmd = 0x00000010, Event_Mask_Control = 0x00040000, Event_Mask_LControl = 0x00000001, Event_Mask_RControl = 0x00002000, Event_Mask_Fn = kCGEventFlagMaskSecondaryFn, }; enum hotkey_flag { Hotkey_Flag_Alt = (1 << 0), Hotkey_Flag_LAlt = (1 << 1), Hotkey_Flag_RAlt = (1 << 2), Hotkey_Flag_Shift = (1 << 3), Hotkey_Flag_LShift = (1 << 4), Hotkey_Flag_RShift = (1 << 5), Hotkey_Flag_Cmd = (1 << 6), Hotkey_Flag_LCmd = (1 << 7), Hotkey_Flag_RCmd = (1 << 8), Hotkey_Flag_Control = (1 << 9), Hotkey_Flag_LControl = (1 << 10), Hotkey_Flag_RControl = (1 << 11), Hotkey_Flag_Fn = (1 << 12), Hotkey_Flag_Passthrough = (1 << 13), Hotkey_Flag_Activate = (1 << 14), Hotkey_Flag_NX = (1 << 15), Hotkey_Flag_Hyper = (Hotkey_Flag_Cmd | Hotkey_Flag_Alt | Hotkey_Flag_Shift | Hotkey_Flag_Control), Hotkey_Flag_Meh = (Hotkey_Flag_Control | Hotkey_Flag_Shift | Hotkey_Flag_Alt) }; #include "hashtable.h" struct carbon_event; struct mode { char *name; char *command; bool capture; bool initialized; struct table hotkey_map; }; struct hotkey { uint32_t flags; uint32_t key; char **process_name; char **command; char *wildcard_command; struct mode **mode_list; }; static inline void add_flags(struct hotkey *hotkey, uint32_t flag) { hotkey->flags |= flag; } static inline bool has_flags(struct hotkey *hotkey, uint32_t flag) { bool result = hotkey->flags & flag; return result; } static inline void clear_flags(struct hotkey *hotkey, uint32_t flag) { hotkey->flags &= ~flag; } 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); bool intercept_systemkey(CGEventRef event, struct hotkey *eventkey); 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 ================================================ FILE: src/hotload.c ================================================ #include "hotload.h" #include #include #include #include #define FSEVENT_CALLBACK(name) void name(ConstFSEventStreamRef stream,\ void *context,\ 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; }; }; static inline bool same_string(const char *a, const char *b) { bool result = a && b && strcmp(a, b) == 0; return result; } static char * copy_string(const char *s) { unsigned length = strlen(s); char *result = (char *) malloc(length + 1); memcpy(result, s, length); result[length] = '\0'; return result; } static char * file_directory(char *file) { char *last_slash = strrchr(file, '/'); *last_slash = '\0'; char *directory = copy_string(file); *last_slash = '/'; return directory; } static char * file_name(char *file) { char *last_slash = strrchr(file, '/'); char *name = copy_string(last_slash + 1); return name; } static char * 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 copy_string(file); } char *result = realpath(file, NULL); return result; } static enum watch_kind resolve_watch_kind(char *file) { struct stat buffer; if (lstat(file, &buffer) != 0) { return WATCH_KIND_INVALID; } if (S_ISDIR(buffer.st_mode)) { return WATCH_KIND_CATALOG; } if (!S_ISLNK(buffer.st_mode)) { return WATCH_KIND_FILE; } return WATCH_KIND_INVALID; } static char * same_catalog(char *absolutepath, struct watched_catalog *catalog_info) { char *last_slash = strrchr(absolutepath, '/'); if (!last_slash) return NULL; char *filename = NULL; // NOTE(koekeisihya): null terminate '/' to cut off filename *last_slash = '\0'; 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; } static inline bool same_file(char *absolutepath, struct watched_file *file_info) { bool result = same_string(absolutepath, file_info->absolutepath); return result; } static FSEVENT_CALLBACK(hotloader_handler) { /* NOTE(asmvik): We sometimes get two events upon file save. */ struct hotloader *hotloader = (struct hotloader *) context; char **files = (char **) file_paths; 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; } } } } static inline void hotloader_add_watched_entry(struct hotloader *hotloader, struct watched_entry entry) { if (!hotloader->watch_list) { hotloader->watch_capacity = 32; hotloader->watch_list = (struct watched_entry *) malloc(hotloader->watch_capacity * sizeof(struct watched_entry)); } 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; } 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; 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, directory, kCFStringEncodingUTF8); } FSEventStreamContext context = { .info = hotloader }; hotloader->path = (CFArrayRef) CFArrayCreate(NULL, (const void **) string_refs, hotloader->watch_count, &kCFTypeArrayCallBacks); hotloader->flags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; hotloader->stream = FSEventStreamCreate(NULL, hotloader_handler, &context, hotloader->path, kFSEventStreamEventIdSinceNow, 0.5, hotloader->flags); 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) return; FSEventStreamStop(hotloader->stream); FSEventStreamInvalidate(hotloader->stream); FSEventStreamRelease(hotloader->stream); 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)); } ================================================ FILE: src/hotload.h ================================================ #ifndef SKHD_HOTLOAD_H #define SKHD_HOTLOAD_H #ifndef __cplusplus #include #endif #include #define HOTLOADER_CALLBACK(name) void name(char *absolutepath, char *directory, char *filename) typedef HOTLOADER_CALLBACK(hotloader_callback); struct watched_entry; struct hotloader { FSEventStreamEventFlags flags; FSEventStreamRef stream; CFArrayRef path; bool enabled; hotloader_callback *callback; 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); bool hotloader_add_catalog(struct hotloader *hotloader, const char *directory, const char *extension); bool hotloader_add_file(struct hotloader *hotloader, const char *file); #endif ================================================ FILE: src/locale.c ================================================ #include "locale.h" #include "hashtable.h" #include "sbuffer.h" #include #include #define array_count(a) (sizeof((a)) / sizeof(*(a))) static struct table keymap_table; static char **keymap_keys; static char *copy_cfstring(CFStringRef string) { CFIndex num_bytes = CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8); char *result = malloc(num_bytes + 1); // NOTE(asmvik): Boolean: typedef -> unsigned char; false = 0, true != 0 if (!CFStringGetCString(string, result, num_bytes + 1, kCFStringEncodingUTF8)) { free(result); result = NULL; } return result; } static int hash_keymap(const char *a) { unsigned long hash = 0, high; while (*a) { hash = (hash << 4) + *a++; high = hash & 0xF0000000; if (high) { hash ^= (high >> 24); } hash &= ~high; } return hash; } static bool same_keymap(const char *a, const char *b) { while (*a && *b && *a == *b) { ++a; ++b; } return *a == '\0' && *b == '\0'; } static void free_keycode_map(void) { for (int i = 0; i < buf_len(keymap_keys); ++i) { free(keymap_keys[i]); } buf_free(keymap_keys); keymap_keys = NULL; } static 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(void) { UniChar chars[255]; UniCharCount len; UInt32 state; TISInputSourceRef keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); CFDataRef uchr = (CFDataRef) TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); CFRelease(keyboard); UCKeyboardLayout *keyboard_layout = (UCKeyboardLayout *) CFDataGetBytePtr(uchr); if (!keyboard_layout) return false; free_keycode_map(); table_free(&keymap_table); table_init(&keymap_table, 61, (table_hash_func) hash_keymap, (table_compare_func) same_keymap); 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; } #pragma clang diagnostic pop uint32_t keycode_from_char(char key) { char lookup_key[] = { key, '\0' }; uint32_t keycode = (uint32_t) (uintptr_t) table_find(&keymap_table, &lookup_key); return keycode; } ================================================ FILE: src/locale.h ================================================ #ifndef SKHD_LOCALE_H #define SKHD_LOCALE_H #include #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 ================================================ FILE: src/log.h ================================================ #ifndef SKHD_LOG_H #define SKHD_LOG_H static bool verbose; static inline void debug(const char *format, ...) { if (!verbose) return; va_list args; va_start(args, format); vfprintf(stdout, format, args); va_end(args); } static inline void warn(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); } static inline void error(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); exit(EXIT_FAILURE); } static inline void require(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); exit(EXIT_SUCCESS); } #endif ================================================ FILE: src/notify.c ================================================ void notify_init(void) { class_replaceMethod(objc_getClass("NSBundle"), sel_registerName("bundleIdentifier"), method_getImplementation((void *)^{return CFSTR("com.asmvik.skhd");}), NULL); } void notify(const char *subtitle, const char *format, ...) { va_list args; va_start(args, format); CFStringRef format_ref = CFStringCreateWithCString(NULL, format, kCFStringEncodingUTF8); CFStringRef subtitle_ref = CFStringCreateWithCString(NULL, subtitle, kCFStringEncodingUTF8); CFStringRef message_ref = CFStringCreateWithFormatAndArguments(NULL, NULL, format_ref, args); void *center = ((void * (*)(void *, SEL))objc_msgSend)((void *) objc_getClass("NSUserNotificationCenter"), sel_registerName("defaultUserNotificationCenter")); void *notification = ((void * (*)(void *, SEL, SEL))objc_msgSend)((void *) objc_getClass("NSUserNotification"), sel_registerName("alloc"), sel_registerName("init")); ((void (*)(void *, SEL, CFStringRef))objc_msgSend)(notification, sel_registerName("setTitle:"), CFSTR("skhd")); ((void (*)(void *, SEL, CFStringRef))objc_msgSend)(notification, sel_registerName("setSubtitle:"), subtitle_ref); ((void (*)(void *, SEL, CFStringRef))objc_msgSend)(notification, sel_registerName("setInformativeText:"), message_ref); ((void (*)(void *, SEL, void *))objc_msgSend)(center, sel_registerName("deliverNotification:"), notification); CFRelease(message_ref); CFRelease(subtitle_ref); CFRelease(format_ref); va_end(args); } ================================================ FILE: src/parse.c ================================================ #include "parse.h" #include "tokenize.h" #include "locale.h" #include "hotkey.h" #include "hashtable.h" #include #include #include #include #include static struct mode * find_or_init_default_mode(struct parser *parser) { struct mode *default_mode; if ((default_mode = table_find(parser->mode_map, "default"))) { return default_mode; } default_mode = malloc(sizeof(struct mode)); default_mode->name = copy_string("default"); default_mode->initialized = false; table_init(&default_mode->hotkey_map, 131, (table_hash_func) hash_hotkey, (table_compare_func) same_hotkey); default_mode->capture = false; default_mode->command = NULL; table_add(parser->mode_map, default_mode->name, default_mode); return default_mode; } static char * read_file(const char *file) { unsigned length; char *buffer = NULL; FILE *handle = fopen(file, "r"); if (handle) { fseek(handle, 0, SEEK_END); length = ftell(handle); fseek(handle, 0, SEEK_SET); buffer = malloc(length + 1); fread(buffer, length, 1, handle); buffer[length] = '\0'; fclose(handle); } return buffer; } static char * copy_string_count(char *s, int length) { char *result = malloc(length + 1); memcpy(result, s, length); result[length] = '\0'; return result; } static uint32_t keycode_from_hex(char *hex) { uint32_t result; sscanf(hex, "%x", &result); return result; } static void parse_command(struct parser *parser, struct hotkey *hotkey) { struct token command = parser_previous(parser); char *result = copy_string_count(command.text, command.length); debug("\tcmd: '%s'\n", result); buf_push(hotkey->command, result); } static 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"); } } static 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"); } } static uint32_t parse_key_hex(struct parser *parser) { struct token key = parser_previous(parser); char *hex = copy_string_count(key.text, key.length); uint32_t keycode = keycode_from_hex(hex); free(hex); debug("\tkey: '%.*s' (0x%02x)\n", key.length, key.text, keycode); return keycode; } static uint32_t parse_key(struct parser *parser) { uint32_t keycode; struct token key = parser_previous(parser); keycode = keycode_from_char(*key.text); debug("\tkey: '%c' (0x%02x)\n", *key.text, keycode); return keycode; } #define KEY_HAS_IMPLICIT_FN_MOD 4 #define KEY_HAS_IMPLICIT_NX_MOD 35 static uint32_t literal_keycode_value[] = { kVK_Return, kVK_Tab, kVK_Space, kVK_Delete, kVK_Escape, kVK_ForwardDelete, kVK_Home, kVK_End, kVK_PageUp, kVK_PageDown, kVK_Help, kVK_LeftArrow, kVK_RightArrow, kVK_UpArrow, kVK_DownArrow, kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6, kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12, kVK_F13, kVK_F14, kVK_F15, kVK_F16, kVK_F17, kVK_F18, kVK_F19, kVK_F20, NX_KEYTYPE_SOUND_UP, NX_KEYTYPE_SOUND_DOWN, NX_KEYTYPE_MUTE, NX_KEYTYPE_PLAY, NX_KEYTYPE_PREVIOUS, NX_KEYTYPE_NEXT, NX_KEYTYPE_REWIND, NX_KEYTYPE_FAST, NX_KEYTYPE_BRIGHTNESS_UP, NX_KEYTYPE_BRIGHTNESS_DOWN, NX_KEYTYPE_ILLUMINATION_UP, NX_KEYTYPE_ILLUMINATION_DOWN }; static inline void handle_implicit_literal_flags(struct hotkey *hotkey, int literal_index) { if ((literal_index > KEY_HAS_IMPLICIT_FN_MOD) && (literal_index < KEY_HAS_IMPLICIT_NX_MOD)) { hotkey->flags |= Hotkey_Flag_Fn; } else if (literal_index >= KEY_HAS_IMPLICIT_NX_MOD) { hotkey->flags |= Hotkey_Flag_NX; } } static void parse_key_literal(struct parser *parser, struct hotkey *hotkey) { struct token key = parser_previous(parser); for (int i = 0; i < array_count(literal_keycode_str); ++i) { if (token_equals(key, literal_keycode_str[i])) { handle_implicit_literal_flags(hotkey, i); hotkey->key = literal_keycode_value[i]; debug("\tkey: '%.*s' (0x%02x)\n", key.length, key.text, hotkey->key); break; } } } static enum hotkey_flag modifier_flags_value[] = { Hotkey_Flag_Alt, Hotkey_Flag_LAlt, Hotkey_Flag_RAlt, 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_Meh, }; static uint32_t parse_modifier(struct parser *parser) { struct token modifier = parser_previous(parser); uint32_t flags = 0; for (int i = 0; i < array_count(modifier_flags_str); ++i) { if (token_equals(modifier, modifier_flags_str[i])) { flags |= modifier_flags_value[i]; debug("\tmod: '%s'\n", modifier_flags_str[i]); break; } } if (parser_match(parser, Token_Plus)) { if (parser_match(parser, Token_Modifier)) { flags |= parse_modifier(parser); } else { parser_report_error(parser, parser_peek(parser), "expected modifier\n"); } } return flags; } static void parse_mode(struct parser *parser, struct hotkey *hotkey) { struct token identifier = parser_previous(parser); char *name = copy_string_count(identifier.text, identifier.length); struct mode *mode = table_find(parser->mode_map, name); free(name); if (!mode && token_equals(identifier, "default")) { mode = find_or_init_default_mode(parser); } else if (!mode) { parser_report_error(parser, identifier, "undeclared identifier\n"); return; } buf_push(hotkey->mode_list, mode); 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, parser_peek(parser), "expected identifier\n"); } } } static struct hotkey * parse_hotkey(struct parser *parser) { struct hotkey *hotkey = malloc(sizeof(struct hotkey)); memset(hotkey, 0, sizeof(struct hotkey)); bool found_modifier; debug("hotkey :: #%d {\n", parser->current_token.line); if (parser_match(parser, Token_Identifier)) { parse_mode(parser, hotkey); if (parser->error) { goto err; } } if (buf_len(hotkey->mode_list) > 0) { if (!parser_match(parser, Token_Insert)) { parser_report_error(parser, parser_peek(parser), "expected '<'\n"); goto err; } } else { buf_push(hotkey->mode_list, find_or_init_default_mode(parser)); } 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)) { parser_report_error(parser, parser_peek(parser), "expected '-'\n"); 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 { parser_report_error(parser, parser_peek(parser), "expected key-literal\n"); goto err; } if (parser_match(parser, Token_Arrow)) { hotkey->flags |= Hotkey_Flag_Passthrough; } if (parser_match(parser, Token_Command)) { 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)) { parse_activate(parser, hotkey); if (parser->error) { goto err; } } else { parser_report_error(parser, parser_peek(parser), "expected ':' followed by command or ';' followed by mode\n"); goto err; } debug("}\n"); return hotkey; err: free(hotkey); return NULL; } static struct mode * parse_mode_decl(struct parser *parser) { struct mode *mode = malloc(sizeof(struct mode)); struct token identifier = parser_previous(parser); mode->name = copy_string_count(identifier.text, identifier.length); mode->initialized = true; table_init(&mode->hotkey_map, 131, (table_hash_func) hash_hotkey, (table_compare_func) same_hotkey); if (parser_match(parser, Token_Capture)) { mode->capture = true; } else { mode->capture = false; } if (parser_match(parser, Token_Command)) { mode->command = copy_string_count(parser->previous_token.text, parser->previous_token.length); } else { mode->command = NULL; } return mode; } void parse_declaration(struct parser *parser) { parser_match(parser, Token_Decl); if (parser_match(parser, Token_Identifier)) { struct token identifier = parser_previous(parser); struct mode *mode = parse_mode_decl(parser); struct mode *existing_mode = table_find(parser->mode_map, mode->name); if (existing_mode) { if (same_string(existing_mode->name, "default") && !existing_mode->initialized) { existing_mode->initialized = true; existing_mode->capture = mode->capture; existing_mode->command = mode->command; } else { parser_report_error(parser, identifier, "duplicate declaration '%s'\n", mode->name); if (mode->command) free(mode->command); } free(mode->name); free(mode); } else { table_add(parser->mode_map, mode->name, mode); } } else { 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_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) break; if ((parser_check(parser, Token_Identifier)) || (parser_check(parser, Token_Modifier)) || (parser_check(parser, Token_Literal)) || (parser_check(parser, Token_Key_Hex)) || (parser_check(parser, Token_Key))) { if ((hotkey = parse_hotkey(parser))) { for (int i = 0; i < buf_len(hotkey->mode_list); ++i) { mode = hotkey->mode_list[i]; table_add(&mode->hotkey_map, hotkey, hotkey); } } } 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, 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 parser_peek(struct parser *parser) { return parser->current_token; } struct token parser_previous(struct parser *parser) { return parser->previous_token; } bool parser_eof(struct parser *parser) { struct token token = parser_peek(parser); return token.type == Token_EndOfStream; } struct token parser_advance(struct parser *parser) { if (!parser_eof(parser)) { parser->previous_token = parser->current_token; parser->current_token = get_token(&parser->tokenizer); } return parser_previous(parser); } bool parser_check(struct parser *parser, enum token_type type) { if (parser_eof(parser)) return false; struct token token = parser_peek(parser); return token.type == type; } bool parser_match(struct parser *parser, enum token_type type) { if (parser_check(parser, type)) { parser_advance(parser); return true; } return false; } 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); } free(load.file); } buf_free(parser->load_directives); if (error) { free_mode_map(parser->mode_map); free_blacklist(parser->blacklst); } } 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; } 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); } ================================================ FILE: src/parse.h ================================================ #ifndef SKHD_PARSE_H #define SKHD_PARSE_H #include "tokenize.h" #include 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; }; 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); 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, 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, struct token token, const char *format, ...); #endif ================================================ FILE: src/sbuffer.h ================================================ #ifndef SBUFFER_H #define SBUFFER_H #include #include struct buf_hdr { size_t len; size_t cap; char buf[0]; }; #define buf_MAX(a, b) ((a) > (b) ? (a) : (b)) #define buf_OFFSETOF(t, f) (size_t)((char *)&(((t *)0)->f) - (char *)0) #define buf__hdr(b) ((struct buf_hdr *)((char *)(b) - buf_OFFSETOF(struct buf_hdr, buf))) #define buf__should_grow(b, n) (buf_len(b) + (n) >= buf_cap(b)) #define buf__fit(b, n) (buf__should_grow(b, n) ? ((b) = buf__grow_f(b, buf_len(b) + (n), sizeof(*(b)))) : 0) #define buf_len(b) ((b) ? buf__hdr(b)->len : 0) #define buf_cap(b) ((b) ? buf__hdr(b)->cap : 0) #define buf_push(b, x) (buf__fit(b, 1), (b)[buf_len(b)] = (x), buf__hdr(b)->len++) #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) { size_t new_cap = buf_MAX(1 + 2*buf_cap(buf), new_len); size_t new_size = buf_OFFSETOF(struct buf_hdr, buf) + new_cap*elem_size; struct buf_hdr *new_hdr = realloc(buf ? buf__hdr(buf) : 0, new_size); new_hdr->cap = new_cap; if (!buf) { new_hdr->len = 0; } return new_hdr->buf; } #endif ================================================ FILE: src/service.h ================================================ #ifndef SERVICE_H #define SERVICE_H #include #include #define MAXLEN 512 #define _PATH_LAUNCHCTL "/bin/launchctl" #define _NAME_SKHD_PLIST "com.asmvik.skhd" #define _PATH_SKHD_PLIST "%s/Library/LaunchAgents/"_NAME_SKHD_PLIST".plist" #define _SKHD_PLIST \ "\n" \ "\n" \ "\n" \ "\n" \ " Label\n" \ " "_NAME_SKHD_PLIST"\n" \ " ProgramArguments\n" \ " \n" \ " %s\n" \ " \n" \ " EnvironmentVariables\n" \ " \n" \ " PATH\n" \ " %s\n" \ " \n" \ " RunAtLoad\n" \ " \n" \ " KeepAlive\n" \ " \n" \ " SuccessfulExit\n" \ " \n" \ " Crashed\n" \ " \n" \ " \n" \ " StandardOutPath\n" \ " /tmp/skhd_%s.out.log\n" \ " StandardErrorPath\n" \ " /tmp/skhd_%s.err.log\n" \ " ProcessType\n" \ " Interactive\n" \ " Nice\n" \ " -20\n" \ "\n" \ "" // // NOTE(asmvik): A launchd service has the following states: // // 1. Installed / Uninstalled // 2. Active (Enable / Disable) // 3. Bootstrapped (Load / Unload) // 4. Running (Start / Stop) // static int safe_exec(char *const argv[], bool suppress_output) { pid_t pid; posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); if (suppress_output) { posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null", O_WRONLY|O_APPEND, 0); posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY|O_APPEND, 0); } int status = posix_spawn(&pid, argv[0], &actions, NULL, argv, NULL); if (status) return 1; while ((waitpid(pid, &status, 0) == -1) && (errno == EINTR)) { usleep(1000); } if (WIFSIGNALED(status)) { return 1; } else if (WIFSTOPPED(status)) { return 1; } else { return WEXITSTATUS(status); } } static inline char *cfstring_copy(CFStringRef string) { CFIndex num_bytes = CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8); char *result = malloc(num_bytes + 1); if (!result) return NULL; if (!CFStringGetCString(string, result, num_bytes + 1, kCFStringEncodingUTF8)) { free(result); result = NULL; } return result; } extern CFURLRef CFCopyHomeDirectoryURLForUser(void *user); static char *populate_plist_path(void) { CFURLRef homeurl_ref = CFCopyHomeDirectoryURLForUser(NULL); CFStringRef home_ref = homeurl_ref ? CFURLCopyFileSystemPath(homeurl_ref, kCFURLPOSIXPathStyle) : NULL; char *home = home_ref ? cfstring_copy(home_ref) : NULL; if (!home) { error("skhd: unable to retrieve home directory! abort..\n"); } int size = strlen(_PATH_SKHD_PLIST)-2 + strlen(home) + 1; char *result = malloc(size); if (!result) { error("skhd: could not allocate memory for plist path! abort..\n"); } memset(result, 0, size); snprintf(result, size, _PATH_SKHD_PLIST, home); return result; } static char *populate_plist(int *length) { char *user = getenv("USER"); if (!user) { error("skhd: 'env USER' not set! abort..\n"); } char *path_env = getenv("PATH"); if (!path_env) { error("skhd: 'env PATH' not set! abort..\n"); } char exe_path[4096]; unsigned int exe_path_size = sizeof(exe_path); if (_NSGetExecutablePath(exe_path, &exe_path_size) < 0) { error("skhd: unable to retrieve path of executable! abort..\n"); } int size = strlen(_SKHD_PLIST)-8 + strlen(exe_path) + strlen(path_env) + (2*strlen(user)) + 1; char *result = malloc(size); if (!result) { error("skhd: could not allocate memory for plist contents! abort..\n"); } memset(result, 0, size); snprintf(result, size, _SKHD_PLIST, exe_path, path_env, user, user); *length = size-1; return result; } static inline bool directory_exists(char *filename) { struct stat buffer; if (stat(filename, &buffer) != 0) { return false; } return S_ISDIR(buffer.st_mode); } static inline void ensure_directory_exists(char *skhd_plist_path) { // // NOTE(asmvik): Temporarily remove filename. // We know the filepath will contain a slash, as // it is controlled by us, so don't bother checking // the result.. // char *last_slash = strrchr(skhd_plist_path, '/'); *last_slash = '\0'; if (!directory_exists(skhd_plist_path)) { mkdir(skhd_plist_path, 0755); } // // NOTE(asmvik): Restore original filename. // *last_slash = '/'; } static int service_install_internal(char *skhd_plist_path) { int skhd_plist_length; char *skhd_plist = populate_plist(&skhd_plist_length); ensure_directory_exists(skhd_plist_path); FILE *handle = fopen(skhd_plist_path, "w"); if (!handle) return 1; size_t bytes = fwrite(skhd_plist, skhd_plist_length, 1, handle); int result = bytes == 1 ? 0 : 1; fclose(handle); return result; } static bool file_exists(char *filename); static int service_install(void) { char *skhd_plist_path = populate_plist_path(); if (file_exists(skhd_plist_path)) { error("skhd: service file '%s' is already installed! abort..\n", skhd_plist_path); } return service_install_internal(skhd_plist_path); } static int service_uninstall(void) { char *skhd_plist_path = populate_plist_path(); if (!file_exists(skhd_plist_path)) { error("skhd: service file '%s' is not installed! abort..\n", skhd_plist_path); } return unlink(skhd_plist_path) == 0 ? 0 : 1; } static int service_start(void) { char *skhd_plist_path = populate_plist_path(); if (!file_exists(skhd_plist_path)) { warn("skhd: service file '%s' is not installed! attempting installation..\n", skhd_plist_path); int result = service_install_internal(skhd_plist_path); if (result) { error("skhd: service file '%s' could not be installed! abort..\n", skhd_plist_path); } } char service_target[MAXLEN]; snprintf(service_target, sizeof(service_target), "gui/%d/%s", getuid(), _NAME_SKHD_PLIST); char domain_target[MAXLEN]; snprintf(domain_target, sizeof(domain_target), "gui/%d", getuid()); // // NOTE(asmvik): Check if service is bootstrapped // const char *const args[] = { _PATH_LAUNCHCTL, "print", service_target, NULL }; int is_bootstrapped = safe_exec((char *const*)args, true); if (is_bootstrapped != 0) { // // NOTE(asmvik): Service is not bootstrapped and could be disabled. // There is no way to query if the service is disabled, and we cannot // bootstrap a disabled service. Try to enable the service. This will be // a no-op if the service is already enabled. // const char *const args[] = { _PATH_LAUNCHCTL, "enable", service_target, NULL }; safe_exec((char *const*)args, false); // // NOTE(asmvik): Bootstrap service into the target domain. // This will also start the program **iff* RunAtLoad is set to true. // const char *const args2[] = { _PATH_LAUNCHCTL, "bootstrap", domain_target, skhd_plist_path, NULL }; return safe_exec((char *const*)args2, false); } else { // // NOTE(asmvik): The service has already been bootstrapped. // Tell the bootstrapped service to launch immediately; it is an // error to bootstrap a service that has already been bootstrapped. // const char *const args[] = { _PATH_LAUNCHCTL, "kickstart", service_target, NULL }; return safe_exec((char *const*)args, false); } } static int service_restart(void) { char *skhd_plist_path = populate_plist_path(); if (!file_exists(skhd_plist_path)) { error("skhd: service file '%s' is not installed! abort..\n", skhd_plist_path); } char service_target[MAXLEN]; snprintf(service_target, sizeof(service_target), "gui/%d/%s", getuid(), _NAME_SKHD_PLIST); const char *const args[] = { _PATH_LAUNCHCTL, "kickstart", "-k", service_target, NULL }; return safe_exec((char *const*)args, false); } static int service_stop(void) { char *skhd_plist_path = populate_plist_path(); if (!file_exists(skhd_plist_path)) { error("skhd: service file '%s' is not installed! abort..\n", skhd_plist_path); } char service_target[MAXLEN]; snprintf(service_target, sizeof(service_target), "gui/%d/%s", getuid(), _NAME_SKHD_PLIST); char domain_target[MAXLEN]; snprintf(domain_target, sizeof(domain_target), "gui/%d", getuid()); // // NOTE(asmvik): Check if service is bootstrapped // const char *const args[] = { _PATH_LAUNCHCTL, "print", service_target, NULL }; int is_bootstrapped = safe_exec((char *const*)args, true); if (is_bootstrapped != 0) { // // NOTE(asmvik): Service is not bootstrapped, but the program // could still be running an instance that was started **while the service // was bootstrapped**, so we tell it to stop said service. // const char *const args[] = { _PATH_LAUNCHCTL, "kill", "SIGTERM", service_target, NULL }; return safe_exec((char *const*)args, false); } else { // // NOTE(asmvik): Service is bootstrapped; we stop a potentially // running instance of the program and unload the service, making it // not trigger automatically in the future. // // This is NOT the same as disabling the service, which will prevent // it from being boostrapped in the future (without explicitly re-enabling // it first). // const char *const args[] = { _PATH_LAUNCHCTL, "bootout", domain_target, skhd_plist_path, NULL }; return safe_exec((char *const*)args, false); } } #endif ================================================ FILE: src/skhd.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 "service.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 "notify.c" extern void NSApplicationLoad(void); extern CFDictionaryRef CGSCopyCurrentSessionDictionary(void); extern bool CGSIsSecureEventInputSet(void); #define secure_keyboard_entry_enabled CGSIsSecureEventInputSet #define GLOBAL_CONNECTION_CALLBACK(name) void name(uint32_t type, void *data, size_t data_length, void *context) typedef GLOBAL_CONNECTION_CALLBACK(global_connection_callback); extern CGError CGSRegisterNotifyProc(void *handler, uint32_t type, void *context); #define SKHD_CONFIG_FILE ".skhdrc" #define SKHD_PIDFILE_FMT "/tmp/skhd_%s.pid" #define VERSION_OPT_LONG "--version" #define VERSION_OPT_SHRT "-v" #define SERVICE_INSTALL_OPT "--install-service" #define SERVICE_UNINSTALL_OPT "--uninstall-service" #define SERVICE_START_OPT "--start-service" #define SERVICE_RESTART_OPT "--restart-service" #define SERVICE_STOP_OPT "--stop-service" #define MAJOR 0 #define MINOR 3 #define PATCH 9 static struct carbon_event carbon; static struct event_tap event_tap; static struct hotloader hotloader; static struct mode *current_mode; static struct table mode_map; static struct table blacklst; static bool thwart_hotloader; static char config_file[4096]; static HOTLOADER_CALLBACK(config_handler); static void parse_config_helper(char *absolutepath) { struct parser 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"); } static HOTLOADER_CALLBACK(config_handler) { BEGIN_TIMED_BLOCK("hotload_config"); debug("skhd: config-file has been modified.. reloading config\n"); free_mode_map(&mode_map); free_blacklist(&blacklst); parse_config_helper(config_file); END_TIMED_BLOCK(); } static 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(); } static 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; } static EVENT_TAP_CALLBACK(key_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: { 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, &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 hotkey eventkey; if (intercept_systemkey(event, &eventkey)) { bool result = find_and_exec_hotkey(&eventkey, &mode_map, ¤t_mode, &carbon); if (result) return NULL; } } break; } return event; } static 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(); } static 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; } static 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(asmvik): we intentionally leave the handle open, // as calling close(..) will release the lock we just acquired. debug("skhd: successfully created pid-file..\n"); } static inline bool string_equals(const char *a, const char *b) { return a && b && strcmp(a, b) == 0; } static bool parse_arguments(int argc, char **argv) { if ((string_equals(argv[1], VERSION_OPT_LONG)) || (string_equals(argv[1], VERSION_OPT_SHRT))) { fprintf(stdout, "skhd-v%d.%d.%d\n", MAJOR, MINOR, PATCH); exit(EXIT_SUCCESS); } if (string_equals(argv[1], SERVICE_INSTALL_OPT)) { exit(service_install()); } if (string_equals(argv[1], SERVICE_UNINSTALL_OPT)) { exit(service_uninstall()); } if (string_equals(argv[1], SERVICE_START_OPT)) { exit(service_start()); } if (string_equals(argv[1], SERVICE_RESTART_OPT)) { exit(service_restart()); } if (string_equals(argv[1], SERVICE_STOP_OPT)) { exit(service_stop()); } int option; const char *short_option = "VPvc:k:t:rho"; struct option long_option[] = { { "verbose", no_argument, NULL, 'V' }, { "profile", no_argument, NULL, 'P' }, { "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 'c': { snprintf(config_file, sizeof(config_file), "%s", 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; } } return false; } static bool check_privileges(void) { bool result; const void *keys[] = { kAXTrustedCheckOptionPrompt }; const void *values[] = { kCFBooleanTrue }; CFDictionaryRef options; options = CFDictionaryCreate(kCFAllocatorDefault, keys, values, sizeof(keys) / sizeof(*keys), &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); result = AXIsProcessTrustedWithOptions(options); CFRelease(options); return result; } static inline bool file_exists(char *filename) { struct stat buffer; if (stat(filename, &buffer) != 0) { return false; } if (buffer.st_mode & S_IFDIR) { return false; } return true; } static bool get_config_file(char *restrict filename, char *restrict buffer, int buffer_size) { char *xdg_home = getenv("XDG_CONFIG_HOME"); if (xdg_home && *xdg_home) { snprintf(buffer, buffer_size, "%s/skhd/%s", xdg_home, filename); if (file_exists(buffer)) return true; } char *home = getenv("HOME"); if (!home) return false; snprintf(buffer, buffer_size, "%s/.config/skhd/%s", home, filename); if (file_exists(buffer)) return true; snprintf(buffer, buffer_size, "%s/.%s", home, filename); return file_exists(buffer); } static char *secure_keyboard_entry_process_info(pid_t *pid) { char *process_name = NULL; CFDictionaryRef session = CGSCopyCurrentSessionDictionary(); if (!session) return NULL; CFNumberRef pid_ref = (CFNumberRef) CFDictionaryGetValue(session, CFSTR("kCGSSessionSecureInputPID")); if (pid_ref) { CFNumberGetValue(pid_ref, CFNumberGetType(pid_ref), pid); process_name = find_process_name_for_pid(*pid); } CFRelease(session); return process_name; } static void dump_secure_keyboard_entry_process_info(void) { pid_t pid; char *process_name = secure_keyboard_entry_process_info(&pid); if (process_name) { error("skhd: secure keyboard entry is enabled by (%lld) '%s'! abort..\n", pid, process_name); } else { error("skhd: secure keyboard entry is enabled! abort..\n"); } } static GLOBAL_CONNECTION_CALLBACK(connection_handler) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ pid_t pid; char *process_name = secure_keyboard_entry_process_info(&pid); if (type == 752) { if (process_name) { notify("Secure Keyboard Entry", "Enabled by '%s' (%d)", process_name, pid); } else { notify("Secure Keyboard Entry", "Enabled by unknown application.."); } } else if (type == 753) { if (process_name) { notify("Secure Keyboard Entry", "Disabled by '%s' (%d)", process_name, pid); } else { notify("Secure Keyboard Entry", "Disabled by unknown application.."); } } if (process_name) free(process_name); }); } int main(int argc, char **argv) { if (getuid() == 0 || geteuid() == 0) { require("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()) { dump_secure_keyboard_entry_process_info(); } if (!check_privileges()) { require("skhd: must be run with accessibility access! abort..\n"); } if (!initialize_keycode_map()) { 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[0] == 0) { get_config_file("skhdrc", config_file, sizeof(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(); 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(); END_SCOPED_TIMED_BLOCK(); NSApplicationLoad(); notify_init(); // CGSRegisterNotifyProc((void*)connection_handler, 752, NULL); // CGSRegisterNotifyProc((void*)connection_handler, 753, NULL); CFRunLoopRun(); return EXIT_SUCCESS; } ================================================ FILE: src/synthesize.c ================================================ #include #include "synthesize.h" #include "locale.h" #include "parse.h" #include "hotkey.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" static inline void create_and_post_keyevent(uint16_t key, bool pressed) { CGPostKeyboardEvent((CGCharCode)0, (CGKeyCode)key, pressed); } static 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 ================================================ FILE: src/synthesize.h ================================================ #ifndef SKHD_SYNTHESIZE_H #define SKHD_SYNTHESIZE_H void synthesize_key(char *key_string); void synthesize_text(char *text); #endif ================================================ FILE: src/timing.h ================================================ #ifndef MACOS_TIMING_H #define MACOS_TIMING_H #include #include #include #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) static bool profile; struct timing_info { char *note; uint64_t start; uint64_t end; double ms; }; void begin_timing(struct timing_info *timing, char *note); void end_timing(struct timing_info *timing); static inline uint64_t macos_get_wall_clock(void) { return mach_absolute_time(); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" static inline uint64_t macos_get_nanoseconds_elapsed(uint64_t start, uint64_t end) { uint64_t elapsed = end - start; Nanoseconds nano = AbsoluteToNanoseconds(*(AbsoluteTime *) &elapsed); return *(uint64_t *) &nano; } #pragma clang diagnostic pop static inline double macos_get_milliseconds_elapsed(uint64_t start, uint64_t end) { uint64_t ns = macos_get_nanoseconds_elapsed(start, end); return (double)(ns / 1000000.0); } static inline double macos_get_seconds_elapsed(uint64_t start, uint64_t end) { uint64_t ns = macos_get_nanoseconds_elapsed(start, end); return (double)(ns / 1000000000.0); } 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); } } #endif ================================================ FILE: src/tokenize.c ================================================ #include "tokenize.h" #include #define array_count(a) (sizeof((a)) / sizeof(*(a))) int token_equals(struct token token, const char *match) { const char *at = match; for (int i = 0; i < token.length; ++i, ++at) { if ((*at == 0) || (token.text[i] != *at)) { return false; } } return (*at == 0); } static void advance(struct tokenizer *tokenizer) { if (*tokenizer->at == '\n') { tokenizer->cursor = 0; ++tokenizer->line; } ++tokenizer->cursor; ++tokenizer->at; } static void eat_whitespace(struct tokenizer *tokenizer) { while (*tokenizer->at && isspace(*tokenizer->at)) { advance(tokenizer); } } static void eat_comment(struct tokenizer *tokenizer) { while (*tokenizer->at && *tokenizer->at != '\n') { advance(tokenizer); } } static void eat_command(struct tokenizer *tokenizer) { while (*tokenizer->at && *tokenizer->at != '\n') { if (*tokenizer->at == '\\') { advance(tokenizer); } advance(tokenizer); } } static void eat_hex(struct tokenizer *tokenizer) { while ((*tokenizer->at) && ((isdigit(*tokenizer->at)) || (*tokenizer->at >= 'A' && *tokenizer->at <= 'F'))) { advance(tokenizer); } } static void eat_string(struct tokenizer *tokenizer) { /* * NOTE(asmvik): 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); } } static void eat_option(struct tokenizer *tokenizer) { while (*tokenizer->at && !isspace(*tokenizer->at)) { advance(tokenizer); } } static inline bool isidentifier(char c) { return isalpha(c) || c == '_'; } static void eat_identifier(struct tokenizer *tokenizer) { while ((*tokenizer->at) && isidentifier(*tokenizer->at)) { advance(tokenizer); } while ((*tokenizer->at) && isdigit(*tokenizer->at)) { advance(tokenizer); } } static enum token_type resolve_identifier_type(struct token token) { if (token.length == 1) { return Token_Key; } for (int i = 0; i < array_count(modifier_flags_str); ++i) { if (token_equals(token, modifier_flags_str[i])) { return Token_Modifier; } } for (int i = 0; i < array_count(literal_keycode_str); ++i) { if (token_equals(token, literal_keycode_str[i])) { return Token_Literal; } } return Token_Identifier; } struct token peek_token(struct tokenizer tokenizer) { return get_token(&tokenizer); } struct token get_token(struct tokenizer *tokenizer) { struct token token; char c; eat_whitespace(tokenizer); token.length = 1; token.text = tokenizer->at; token.line = tokenizer->line; token.cursor = tokenizer->cursor; c = *token.text; advance(tokenizer); switch (c) { case '\0':{ token.type = Token_EndOfStream; } break; case '+': { token.type = Token_Plus; } break; 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); } break; case '-': { if (*tokenizer->at && *tokenizer->at == '>') { advance(tokenizer); token.length = tokenizer->at - token.text; token.type = Token_Arrow; } else { token.type = Token_Dash; } } break; case ';': { eat_whitespace(tokenizer); token.text = tokenizer->at; token.line = tokenizer->line; token.cursor = tokenizer->cursor; eat_identifier(tokenizer); token.length = tokenizer->at - token.text; token.type = Token_Activate; } break; case ':': { if (*tokenizer->at && *tokenizer->at == ':') { advance(tokenizer); token.length = tokenizer->at - token.text; token.type = Token_Decl; } else { eat_whitespace(tokenizer); token.text = tokenizer->at; token.line = tokenizer->line; token.cursor = tokenizer->cursor; eat_command(tokenizer); token.length = tokenizer->at - token.text; token.type = Token_Command; } } break; default: { if (c == '0' && *tokenizer->at == 'x') { advance(tokenizer); eat_hex(tokenizer); token.length = tokenizer->at - token.text; token.type = Token_Key_Hex; } else if (isdigit(c)) { token.type = Token_Key; } else if (isalpha(c)) { eat_identifier(tokenizer); token.length = tokenizer->at - token.text; token.type = resolve_identifier_type(token); } else { token.type = Token_Unknown; } } break; } return token; } void tokenizer_init(struct tokenizer *tokenizer, char *buffer) { tokenizer->buffer = buffer; tokenizer->at = buffer; tokenizer->line = 1; tokenizer->cursor = 1; } ================================================ FILE: src/tokenize.h ================================================ #ifndef SKHD_TOKENIZE_H #define SKHD_TOKENIZE_H static const char *modifier_flags_str[] = { "alt", "lalt", "ralt", "shift", "lshift", "rshift", "cmd", "lcmd", "rcmd", "ctrl", "lctrl", "rctrl", "fn", "hyper", "meh", }; static 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", "sound_up", "sound_down", "mute", "play", "previous", "next", "rewind", "fast", "brightness_up", "brightness_down", "illumination_up", "illumination_down" }; enum token_type { Token_Identifier, Token_Activate, Token_Command, Token_Modifier, Token_Literal, Token_Key_Hex, Token_Key, Token_Decl, Token_Comma, Token_Insert, Token_Plus, Token_Dash, Token_Arrow, Token_Capture, Token_Unbound, Token_Wildcard, Token_String, Token_Option, Token_BeginList, Token_EndList, Token_Unknown, Token_EndOfStream, }; struct token { enum token_type type; char *text; unsigned length; unsigned line; unsigned cursor; }; struct tokenizer { char *buffer; char *at; unsigned line; unsigned cursor; }; void tokenizer_init(struct tokenizer *tokenizer, char *buffer); struct token get_token(struct tokenizer *tokenizer); struct token peek_token(struct tokenizer tokenizer); int token_equals(struct token token, const char *match); #endif