aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkoekeishiya <aasvi93@hotmail.com>2017-08-07 20:23:44 +0200
committerkoekeishiya <aasvi93@hotmail.com>2017-08-07 20:23:44 +0200
commitd69056799a399058005b4950751397a31110de4a (patch)
tree1dee43a2f247094c58d1263cee8c8477b893e376
downloadskhd-d69056799a399058005b4950751397a31110de4a.tar.gz
skhd-d69056799a399058005b4950751397a31110de4a.zip
v0.0.1
-rw-r--r--.gitignore1
-rw-r--r--LICENSE.txt21
-rw-r--r--README.md47
-rw-r--r--examples/com.koekeishiya.skhd.plist25
-rw-r--r--examples/skhdrc153
-rw-r--r--makefile19
-rw-r--r--src/event_tap.c39
-rw-r--r--src/event_tap.h25
-rw-r--r--src/hotkey.c186
-rw-r--r--src/hotkey.h81
-rw-r--r--src/hotload.c96
-rw-r--r--src/hotload.h39
-rw-r--r--src/locale.c117
-rw-r--r--src/locale.h11
-rw-r--r--src/parse.c270
-rw-r--r--src/parse.h26
-rw-r--r--src/skhd.c216
-rw-r--r--src/tokenize.c171
-rw-r--r--src/tokenize.h66
19 files changed, 1609 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e660fd9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+bin/
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..bd41b84
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5122771
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+**skhd** is a simple hotkey daemon for macOS. It is a stripped version of [**khd**](https://github.com/koekeishiya/khd)
+(although mostly rewritten from scratch), that sacrifices the more advanced features in favour of increased responsiveness and performance.
+
+| feature | skhd | khd |
+|:--------------------------:|:----:|:---:|
+| hotload config file | [x] | [ ] |
+| require unix domain socket | [ ] | [x] |
+| hotkey passthrough | [x] | [x] |
+| modal hotkey-system | [ ] | [x] |
+| app specific hotkey | [ ] | [x] |
+| modifier only hotkey | [ ] | [x] |
+| caps-lock as hotkey | [ ] | [x] |
+| mouse-buttons as hotkey | [ ] | [x] |
+| emit keypress | [ ] | [x] |
+| autowrite text | [ ] | [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.
+
+**Source**:
+
+Requires xcode-8 command-line tools,
+
+ make install # release version
+ make # debug version
+
+#### Usage
+
+```
+-v | --version: Print version number to stdout
+ skhd -v
+
+-c | --config: Specify location of config file
+ skhd -c ~/.skhdrc
+
+```
+
+#### Configuration
+
+**skhd** will load the configuration file `$HOME/.skhdrc`, unless otherwise specified.
+
+See [sample config](https://github.com/koekeishiya/skhd/blob/master/examples/skhdrc) for syntax information.
diff --git a/examples/com.koekeishiya.skhd.plist b/examples/com.koekeishiya.skhd.plist
new file mode 100644
index 0000000..d00e3df
--- /dev/null
+++ b/examples/com.koekeishiya.skhd.plist
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.koekeishiya.skhd</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/usr/local/bin/skhd</string>
+ </array>
+ <key>EnvironmentVariables</key>
+ <dict>
+ <key>PATH</key>
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
+ </dict>
+ <key>RunAtLoad</key>
+ <true/>
+ <key>KeepAlive</key>
+ <true/>
+ <key>StandardOutPath</key>
+ <string>/tmp/skhd.out</string>
+ <key>StandardErrorPath</key>
+ <string>/tmp/skhd.err</string>
+</dict>
+</plist>
diff --git a/examples/skhdrc b/examples/skhdrc
new file mode 100644
index 0000000..b9f2c09
--- /dev/null
+++ b/examples/skhdrc
@@ -0,0 +1,153 @@
+## NOTE(koekeishiya): A hotkey is written according to the following rules:
+#
+# hotkey = <keysym> ':' <command> |
+# <keysym> '->' ':' <command>
+#
+# keysym = <mod> '-' <key> | <key>
+#
+# mod = 'built-in mod keyword' | <mod> '+' <mod>
+#
+# key = <literal> | <keycode>
+#
+# literal = 'single letter or built-in keyword'
+#
+# keycode = 'apple keyboard kVK_<Key> values (0x3C)'
+#
+# -> = optional operator '->' marks this bind as passthrough.
+# this causes the key to be passed through to the system,
+# instead of being consumed.
+#
+# 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.
+
+
+# open terminal, blazingly fast compared to iTerm/Hyper
+cmd - return : open -na /Applications/Kitty.app
+
+# focus window
+alt - h : chunkc tiling::window --focus west
+alt - j : chunkc tiling::window --focus south
+alt - k : chunkc tiling::window --focus north
+alt - l : chunkc tiling::window --focus east
+
+cmd - j : chunkc tiling::window --focus prev
+cmd - k : chunkc tiling::window --focus next
+
+# equalize size of windows
+shift + alt - 0 : chunkc tiling::desktop --equalize
+
+# swap window
+shift + alt - h : chunkc tiling::window --swap west
+shift + alt - j : chunkc tiling::window --swap south
+shift + alt - k : chunkc tiling::window --swap north
+shift + alt - l : chunkc tiling::window --swap east
+
+# move window
+shift + cmd - h : chunkc tiling::window --warp west
+shift + cmd - j : chunkc tiling::window --warp south
+shift + cmd - k : chunkc tiling::window --warp north
+shift + cmd - l : chunkc tiling::window --warp east
+
+# move floating windows / windows on a floating space
+shift + alt - up : chunkc tiling::window --warp-floating fullscreen
+shift + alt - left : chunkc tiling::window --warp-floating left
+shift + alt - right : chunkc tiling::window --warp-floating right
+shift + cmd - left : chunkc tiling::window --warp-floating top-left
+shift + cmd - right : chunkc tiling::window --warp-floating top-right
+shift + ctrl - left : chunkc tiling::window --warp-floating bottom-left
+shift + ctrl - right : chunkc tiling::window --warp-floating bottom-right
+
+# send window to desktop
+shift + alt - x : chunkc tiling::window --send-to-desktop $(chunkc get _last_active_desktop)
+shift + alt - z : chunkc tiling::window --send-to-desktop prev
+shift + alt - c : chunkc tiling::window --send-to-desktop next
+shift + alt - 1 : chunkc tiling::window --send-to-desktop 1
+shift + alt - 2 : chunkc tiling::window --send-to-desktop 2
+shift + alt - 3 : chunkc tiling::window --send-to-desktop 3
+shift + alt - 4 : chunkc tiling::window --send-to-desktop 4
+shift + alt - 5 : chunkc tiling::window --send-to-desktop 5
+shift + alt - 6 : chunkc tiling::window --send-to-desktop 6
+
+# focus monitor
+ctrl + alt - z : chunkc tiling::monitor -f prev
+ctrl + alt - c : chunkc tiling::monitor -f next
+ctrl + alt - 1 : chunkc tiling::monitor -f 1
+ctrl + alt - 2 : chunkc tiling::monitor -f 2
+ctrl + alt - 3 : chunkc tiling::monitor -f 3
+
+# send window to monitor and follow focus
+ctrl + cmd - z : chunkc tiling::window --send-to-monitor prev; chunkc tiling::monitor -f prev
+ctrl + cmd - c : chunkc tiling::window --send-to-monitor next; chunkc tiling::monitor -f next
+ctrl + cmd - 1 : chunkc tiling::window --send-to-monitor 1; chunkc tiling::monitor -f 1
+ctrl + cmd - 2 : chunkc tiling::window --send-to-monitor 2; chunkc tiling::monitor -f 2
+ctrl + cmd - 3 : chunkc tiling::window --send-to-monitor 3; chunkc tiling::monitor -f 3
+
+# increase region size
+shift + alt - a : chunkc tiling::window --use-temporary-ratio 0.1 --adjust-window-edge west
+shift + alt - s : chunkc tiling::window --use-temporary-ratio 0.1 --adjust-window-edge south
+shift + alt - w : chunkc tiling::window --use-temporary-ratio 0.1 --adjust-window-edge north
+shift + alt - d : chunkc tiling::window --use-temporary-ratio 0.1 --adjust-window-edge east
+
+# decrease region size
+shift + cmd - a : chunkc tiling::window --use-temporary-ratio -0.1 --adjust-window-edge west
+shift + cmd - s : chunkc tiling::window --use-temporary-ratio -0.1 --adjust-window-edge south
+shift + cmd - w : chunkc tiling::window --use-temporary-ratio -0.1 --adjust-window-edge north
+shift + cmd - d : chunkc tiling::window --use-temporary-ratio -0.1 --adjust-window-edge east
+
+# set insertion point for focused container
+ctrl + alt - f : chunkc tiling::window --use-insertion-point cancel
+ctrl + alt - h : chunkc tiling::window --use-insertion-point west
+ctrl + alt - j : chunkc tiling::window --use-insertion-point south
+ctrl + alt - k : chunkc tiling::window --use-insertion-point north
+ctrl + alt - l : chunkc tiling::window --use-insertion-point east
+
+# rotate tree
+alt - r : chunkc tiling::desktop --rotate 90
+
+# mirror tree y-axis
+alt - y : chunkc tiling::desktop --mirror vertical
+
+# mirror tree x-axis
+alt - x : chunkc tiling::desktop --mirror horizontal
+
+# toggle desktop offset
+alt - a : chunkc tiling::desktop --toggle offset
+
+# toggle window fullscreen
+alt - f : chunkc tiling::window --toggle fullscreen
+
+# toggle window native fullscreen
+shift + alt - f : chunkc tiling::window --toggle native-fullscreen
+
+# toggle window parent zoom
+alt - d : chunkc tiling::window --toggle parent
+
+# toggle window split type
+alt - e : chunkc tiling::window --toggle split
+
+# float / unfloat window
+alt - t : chunkc tiling::window --toggle float
+
+# toggle sticky, float and resize to picture-in-picture size
+alt - s : chunkc tiling::window --toggle sticky;\
+ chunkc tiling::window --warp-floating pip-right
+
+# float next window to be tiled
+shift + alt - t : chunkc set window_float_next 1
+
+# change layout of desktop
+ctrl + alt - a : chunkc tiling::desktop --layout bsp
+ctrl + alt - s : chunkc tiling::desktop --layout monocle
+ctrl + alt - d : chunkc tiling::desktop --layout float
+
+ctrl + alt - w : chunkc tiling::desktop --deserialize ~/.chunkwm_layouts/dev_1
+
+aasdsa : say "hello"
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..e20de74
--- /dev/null
+++ b/makefile
@@ -0,0 +1,19 @@
+FRAMEWORKS = -framework Carbon
+BUILD_PATH = ./bin
+BUILD_FLAGS = -std=c99 -Wall -g
+SKHD_SRC = ./src/skhd.c
+BINS = $(BUILD_PATH)/skhd
+
+.PHONY: all clean install
+
+all: clean $(BINS)
+
+install: BUILD_FLAGS=-std=c99 -O2
+install: clean $(BINS)
+
+clean:
+ rm -rf $(BUILD_PATH)
+
+$(BUILD_PATH)/skhd: $(SKHD_SRC)
+ mkdir -p $(BUILD_PATH)
+ clang $^ $(BUILD_FLAGS) $(FRAMEWORKS) -o $@
diff --git a/src/event_tap.c b/src/event_tap.c
new file mode 100644
index 0000000..5f44ff4
--- /dev/null
+++ b/src/event_tap.c
@@ -0,0 +1,39 @@
+#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;
+ }
+}
diff --git a/src/event_tap.h b/src/event_tap.h
new file mode 100644
index 0000000..b126ce4
--- /dev/null
+++ b/src/event_tap.h
@@ -0,0 +1,25 @@
+#ifndef SKHD_EVENT_TAP_H
+#define SKHD_EVENT_TAP_H
+
+#include <stdbool.h>
+#include <Carbon/Carbon.h>
+
+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
diff --git a/src/hotkey.c b/src/hotkey.c
new file mode 100644
index 0000000..d34d9a0
--- /dev/null
+++ b/src/hotkey.c
@@ -0,0 +1,186 @@
+#include "hotkey.h"
+#include "locale.h"
+#include "parse.h"
+
+#include <string.h>
+#include <pthread.h>
+
+#define internal static
+#define local_persist static
+
+internal bool
+execute_hotkey(struct hotkey *hotkey)
+{
+ local_persist char arg[] = "-c";
+ local_persist char *shell = NULL;
+ if(!shell)
+ {
+ char *env_shell = getenv("SHELL");
+ shell = env_shell ? env_shell : "/bin/bash";
+ }
+
+ int cpid = fork();
+ if(cpid == 0)
+ {
+ char *exec[] = { shell, arg, hotkey->command, NULL};
+ int status_code = execvp(exec[0], exec);
+ exit(status_code);
+ }
+
+ return true;
+}
+
+internal bool
+compare_cmd(struct hotkey *a, struct hotkey *b)
+{
+ if(has_flags(a, Hotkey_Flag_Cmd)) {
+ return (has_flags(b, Hotkey_Flag_LCmd) ||
+ has_flags(b, Hotkey_Flag_RCmd) ||
+ has_flags(b, Hotkey_Flag_Cmd));
+ } else {
+ return ((has_flags(a, Hotkey_Flag_LCmd) == has_flags(b, Hotkey_Flag_LCmd)) &&
+ (has_flags(a, Hotkey_Flag_RCmd) == has_flags(b, Hotkey_Flag_RCmd)) &&
+ (has_flags(a, Hotkey_Flag_Cmd) == has_flags(b, Hotkey_Flag_Cmd)));
+ }
+}
+
+internal bool
+compare_shift(struct hotkey *a, struct hotkey *b)
+{
+ if(has_flags(a, Hotkey_Flag_Shift)) {
+ return (has_flags(b, Hotkey_Flag_LShift) ||
+ has_flags(b, Hotkey_Flag_RShift) ||
+ has_flags(b, Hotkey_Flag_Shift));
+ } else {
+ return ((has_flags(a, Hotkey_Flag_LShift) == has_flags(b, Hotkey_Flag_LShift)) &&
+ (has_flags(a, Hotkey_Flag_RShift) == has_flags(b, Hotkey_Flag_RShift)) &&
+ (has_flags(a, Hotkey_Flag_Shift) == has_flags(b, Hotkey_Flag_Shift)));
+ }
+}
+
+internal bool
+compare_alt(struct hotkey *a, struct hotkey *b)
+{
+ if(has_flags(a, Hotkey_Flag_Alt)) {
+ return (has_flags(b, Hotkey_Flag_LAlt) ||
+ has_flags(b, Hotkey_Flag_RAlt) ||
+ has_flags(b, Hotkey_Flag_Alt));
+ } else {
+ return ((has_flags(a, Hotkey_Flag_LAlt) == has_flags(b, Hotkey_Flag_LAlt)) &&
+ (has_flags(a, Hotkey_Flag_RAlt) == has_flags(b, Hotkey_Flag_RAlt)) &&
+ (has_flags(a, Hotkey_Flag_Alt) == has_flags(b, Hotkey_Flag_Alt)));
+ }
+}
+
+internal bool
+compare_ctrl(struct hotkey *a, struct hotkey *b)
+{
+ if(has_flags(a, Hotkey_Flag_Control)) {
+ return (has_flags(b, Hotkey_Flag_LControl) ||
+ has_flags(b, Hotkey_Flag_RControl) ||
+ has_flags(b, Hotkey_Flag_Control));
+ } else {
+ return ((has_flags(a, Hotkey_Flag_LControl) == has_flags(b, Hotkey_Flag_LControl)) &&
+ (has_flags(a, Hotkey_Flag_RControl) == has_flags(b, Hotkey_Flag_RControl)) &&
+ (has_flags(a, Hotkey_Flag_Control) == has_flags(b, Hotkey_Flag_Control)));
+ }
+}
+
+internal inline bool
+same_hotkey(struct hotkey *a, struct hotkey *b)
+{
+ if(a && b) {
+ return compare_cmd(a, b) &&
+ compare_shift(a, b) &&
+ compare_alt(a, b) &&
+ compare_ctrl(a, b) &&
+ a->key == b->key;
+ }
+
+ return false;
+}
+
+internal bool
+find_hotkey(struct hotkey *seek, struct hotkey **result, struct hotkey *hotkeys)
+{
+ struct hotkey *hotkey = hotkeys;
+ while(hotkey) {
+ if(same_hotkey(hotkey, seek)) {
+ *result = hotkey;
+ return true;
+ }
+
+ hotkey = hotkey->next;
+ }
+
+ return false;
+}
+
+void free_hotkeys(struct hotkey *hotkeys)
+{
+ struct hotkey *next, *hotkey = hotkeys;
+ while(hotkey) {
+ next = hotkey->next;
+ free(hotkey->command);
+ free(hotkey);
+ hotkey = next;
+ }
+}
+
+bool find_and_exec_hotkey(struct hotkey *eventkey, struct hotkey *hotkeys)
+{
+ bool result = false;
+ struct hotkey *hotkey = NULL;
+ if(find_hotkey(eventkey, &hotkey, hotkeys)) {
+ if(execute_hotkey(hotkey)) {
+ result = has_flags(hotkey, Hotkey_Flag_Passthrough) ? false : true;
+ }
+ }
+
+ return result;
+}
+
+struct hotkey
+cgevent_to_hotkey(CGEventFlags flags, uint32_t key)
+{
+ struct hotkey eventkey = {};
+ eventkey.key = key;
+
+ if((flags & Event_Mask_Cmd) == Event_Mask_Cmd) {
+ bool left = (flags & Event_Mask_LCmd) == Event_Mask_LCmd;
+ bool right = (flags & Event_Mask_RCmd) == Event_Mask_RCmd;
+
+ if(left) add_flags(&eventkey, Hotkey_Flag_LCmd);
+ if(right) add_flags(&eventkey, Hotkey_Flag_RCmd);
+ if(!left && !right) add_flags(&eventkey, Hotkey_Flag_Cmd);
+ }
+
+ if((flags & Event_Mask_Shift) == Event_Mask_Shift) {
+ bool left = (flags & Event_Mask_LShift) == Event_Mask_LShift;
+ bool right = (flags & Event_Mask_RShift) == Event_Mask_RShift;
+
+ if(left) add_flags(&eventkey, Hotkey_Flag_LShift);
+ if(right) add_flags(&eventkey, Hotkey_Flag_RShift);
+ if(!left && !right) add_flags(&eventkey, Hotkey_Flag_Shift);
+ }
+
+ if((flags & Event_Mask_Alt) == Event_Mask_Alt) {
+ bool left = (flags & Event_Mask_LAlt) == Event_Mask_LAlt;
+ bool right = (flags & Event_Mask_RAlt) == Event_Mask_RAlt;
+
+ if(left) add_flags(&eventkey, Hotkey_Flag_LAlt);
+ if(right) add_flags(&eventkey, Hotkey_Flag_RAlt);
+ if(!left && !right) add_flags(&eventkey, Hotkey_Flag_Alt);
+ }
+
+ if((flags & Event_Mask_Control) == Event_Mask_Control) {
+ bool left = (flags & Event_Mask_LControl) == Event_Mask_LControl;
+ bool right = (flags & Event_Mask_RControl) == Event_Mask_RControl;
+
+ if(left) add_flags(&eventkey, Hotkey_Flag_LControl);
+ if(right) add_flags(&eventkey, Hotkey_Flag_RControl);
+ if(!left && !right) add_flags(&eventkey, Hotkey_Flag_Control);
+ }
+
+ return eventkey;
+}
diff --git a/src/hotkey.h b/src/hotkey.h
new file mode 100644
index 0000000..293f04f
--- /dev/null
+++ b/src/hotkey.h
@@ -0,0 +1,81 @@
+#ifndef SKHD_HOTKEY_H
+#define SKHD_HOTKEY_H
+
+#include <Carbon/Carbon.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#define internal static
+
+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,
+};
+
+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_Passthrough = (1 << 12),
+};
+
+struct hotkey
+{
+ uint32_t flags;
+ uint32_t key;
+ char *command;
+ struct hotkey *next;
+};
+
+internal inline void
+add_flags(struct hotkey *hotkey, uint32_t flag)
+{
+ hotkey->flags |= flag;
+}
+
+internal inline bool
+has_flags(struct hotkey *hotkey, uint32_t flag)
+{
+ bool result = hotkey->flags & flag;
+ return result;
+}
+
+internal inline void
+clear_flags(struct hotkey *hotkey, uint32_t flag)
+{
+ hotkey->flags &= ~flag;
+}
+
+bool find_and_exec_hotkey(struct hotkey *eventkey, struct hotkey *hotkeys);
+struct hotkey cgevent_to_hotkey(CGEventFlags flags, uint32_t key);
+void free_hotkeys(struct hotkey *hotkeys);
+
+#endif
diff --git a/src/hotload.c b/src/hotload.c
new file mode 100644
index 0000000..8f39b15
--- /dev/null
+++ b/src/hotload.c
@@ -0,0 +1,96 @@
+#include "hotload.h"
+#include "hotkey.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#define internal static
+
+internal char *
+copy_string(const char *s)
+{
+ unsigned length = strlen(s);
+ char *result = malloc(length + 1);
+ memcpy(result, s, length);
+ result[length] = '\0';
+ return result;
+}
+
+char *file_directory(const char *file)
+{
+ char *last_slash = strrchr(file, '/');
+ *last_slash = '\0';
+ char *directory = copy_string(file);
+ *last_slash = '/';
+ return directory;
+}
+
+char *file_name(const char *file)
+{
+ char *last_slash = strrchr(file, '/');
+ char *name = copy_string(last_slash + 1);
+ return name;
+}
+
+void hotloader_add_file(struct hotloader *hotloader, const char *file)
+{
+ if(!hotloader->enabled) {
+ struct watched_file watch_info;
+ watch_info.directory = file_directory(file);
+ watch_info.filename = file_name(file);
+ hotloader->watch_list[hotloader->watch_count++] = watch_info;
+ printf("hotload: watching file '%s' in directory '%s'\n", watch_info.filename, watch_info.directory);
+ }
+}
+
+bool hotloader_begin(struct hotloader *hotloader, hotloader_callback *callback)
+{
+ if(!hotloader->enabled) {
+ if(hotloader->watch_count) {
+ CFStringRef string_refs[hotloader->watch_count];
+ for(unsigned index = 0; index < hotloader->watch_count; ++index) {
+ string_refs[index] = CFStringCreateWithCString(kCFAllocatorDefault,
+ hotloader->watch_list[index].directory,
+ kCFStringEncodingUTF8);
+ }
+
+ FSEventStreamContext context = {};
+ context.info = (void *) hotloader;
+
+ hotloader->enabled = true;
+ hotloader->path = (CFArrayRef) CFArrayCreate(NULL, (const void **) string_refs, hotloader->watch_count, &kCFTypeArrayCallBacks);
+ hotloader->flags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents;
+ hotloader->stream = FSEventStreamCreate(NULL,
+ callback,
+ &context,
+ hotloader->path,
+ kFSEventStreamEventIdSinceNow,
+ 0.5,
+ hotloader->flags);
+ FSEventStreamScheduleWithRunLoop(hotloader->stream, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
+ FSEventStreamStart(hotloader->stream);
+ return true;
+ }
+ }
+ return false;
+}
+
+void hotloader_end(struct hotloader *hotloader)
+{
+ if(hotloader->enabled) {
+ FSEventStreamStop(hotloader->stream);
+ FSEventStreamInvalidate(hotloader->stream);
+ FSEventStreamRelease(hotloader->stream);
+
+ CFIndex count = CFArrayGetCount(hotloader->path);
+ for(unsigned index = 0; index < count; ++index) {
+ CFStringRef string_ref = (CFStringRef) CFArrayGetValueAtIndex(hotloader->path, index);
+ free(hotloader->watch_list[index].directory);
+ free(hotloader->watch_list[index].filename);
+ CFRelease(string_ref);
+ }
+
+ CFRelease(hotloader->path);
+ memset(hotloader, 0, sizeof(struct hotloader));
+ }
+}
diff --git a/src/hotload.h b/src/hotload.h
new file mode 100644
index 0000000..9b2d666
--- /dev/null
+++ b/src/hotload.h
@@ -0,0 +1,39 @@
+#ifndef SKHD_HOTLOAD_H
+#define SKHD_HOTLOAD_H
+
+#include <stdbool.h>
+#include <Carbon/Carbon.h>
+
+#define HOTLOADER_CALLBACK(name) void name(ConstFSEventStreamRef stream,\
+ void *context,\
+ size_t count,\
+ void *paths,\
+ const FSEventStreamEventFlags *flags,\
+ const FSEventStreamEventId *ids)
+typedef HOTLOADER_CALLBACK(hotloader_callback);
+
+struct watched_file
+{
+ char *directory;
+ char *filename;
+};
+
+struct hotloader
+{
+ FSEventStreamEventFlags flags;
+ FSEventStreamRef stream;
+ CFArrayRef path;
+ bool enabled;
+
+ struct watched_file watch_list[32];
+ unsigned watch_count;
+};
+
+bool hotloader_begin(struct hotloader *hotloader, hotloader_callback *callback);
+void hotloader_end(struct hotloader *hotloader);
+void hotloader_add_file(struct hotloader *hotloader, const char *file);
+
+char *file_directory(const char *file);
+char *file_name(const char *file);
+
+#endif
diff --git a/src/locale.c b/src/locale.c
new file mode 100644
index 0000000..5b262c7
--- /dev/null
+++ b/src/locale.c
@@ -0,0 +1,117 @@
+#include "locale.h"
+
+#include <Carbon/Carbon.h>
+#include <IOKit/hidsystem/ev_keymap.h>
+
+#define internal static
+#define local_persist static
+
+bool same_string(char *text, unsigned length, const char *match)
+{
+ const char *at = match;
+ unsigned index = 0;
+ while(*at++ == text[index++] && index < length);
+ return (*at == '\0' && index == length) ? true : false;
+}
+
+internal CFStringRef
+cfstring_from_keycode(CGKeyCode keycode)
+{
+ TISInputSourceRef keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ CFDataRef uchr = (CFDataRef) TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData);
+ CFRelease(keyboard);
+
+ UCKeyboardLayout *keyboard_layout = (UCKeyboardLayout *) CFDataGetBytePtr(uchr);
+ if(keyboard_layout) {
+ UInt32 dead_key_state = 0;
+ UniCharCount max_string_length = 255;
+ UniCharCount string_length = 0;
+ UniChar unicode_string[max_string_length];
+
+ OSStatus status = UCKeyTranslate(keyboard_layout, keycode,
+ kUCKeyActionDown, 0,
+ LMGetKbdType(), 0,
+ &dead_key_state,
+ max_string_length,
+ &string_length,
+ unicode_string);
+
+ if(string_length == 0 && dead_key_state) {
+ status = UCKeyTranslate(keyboard_layout, kVK_Space,
+ kUCKeyActionDown, 0,
+ LMGetKbdType(), 0,
+ &dead_key_state,
+ max_string_length,
+ &string_length,
+ unicode_string);
+ }
+
+ if(string_length > 0 && status == noErr) {
+ return CFStringCreateWithCharacters(NULL, unicode_string, string_length);
+ }
+ }
+
+ return NULL;
+}
+
+uint32_t keycode_from_char(char key)
+{
+ uint32_t keycode = 0;
+ local_persist CFMutableDictionaryRef keycode_map = NULL;
+ if(!keycode_map) {
+ keycode_map = CFDictionaryCreateMutable(kCFAllocatorDefault, 128, &kCFCopyStringDictionaryKeyCallBacks, NULL);
+ for(unsigned index = 0; index < 128; ++index) {
+ CFStringRef key_string = cfstring_from_keycode(index);
+ if(key_string) {
+ CFDictionaryAddValue(keycode_map, key_string, (const void *)index);
+ CFRelease(key_string);
+ }
+ }
+ }
+
+ UniChar uni_char = key;
+ CFStringRef char_str = CFStringCreateWithCharacters(kCFAllocatorDefault, &uni_char, 1);
+ CFDictionaryGetValueIfPresent(keycode_map, char_str, (const void **)&keycode);
+ CFRelease(char_str);
+
+ return keycode;
+}
+
+uint32_t keycode_from_literal(char *key, unsigned length)
+{
+ if(same_string(key, length, "return")) {
+ return kVK_Return;
+ } else if(same_string(key, length, "tab")) {
+ return kVK_Tab;
+ } else if(same_string(key, length, "space")) {
+ return kVK_Space;
+ } else if(same_string(key, length, "backspace")) {
+ return kVK_Delete;
+ } else if(same_string(key, length, "delete")) {
+ return kVK_ForwardDelete;
+ } else if(same_string(key, length, "escape")) {
+ return kVK_Escape;
+ } else if(same_string(key, length, "home")) {
+ return kVK_Home;
+ } else if(same_string(key, length, "end")) {
+ return kVK_End;
+ } else if(same_string(key, length, "pageup")) {
+ return kVK_PageUp;
+ } else if(same_string(key, length, "pagedown")) {
+ return kVK_PageDown;
+ } else if(same_string(key, length, "help")) {
+ return kVK_Help;
+ } else if(same_string(key, length, "left")) {
+ return kVK_LeftArrow;
+ } else if(same_string(key, length, "right")) {
+ return kVK_RightArrow;
+ } else if(same_string(key, length, "up")) {
+ return kVK_UpArrow;
+ } else if(same_string(key, length, "down")) {
+ return kVK_DownArrow;
+ } else if(same_string(key, length, "f1")) {
+ return kVK_F1;
+ } else {
+ return 0;
+ }
+}
diff --git a/src/locale.h b/src/locale.h
new file mode 100644
index 0000000..2662cee
--- /dev/null
+++ b/src/locale.h
@@ -0,0 +1,11 @@
+#ifndef SKHD_LOCALE_H
+#define SKHD_LOCALE_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+uint32_t keycode_from_char(char key);
+uint32_t keycode_from_literal(char *key, unsigned length);
+bool same_string(char *text, unsigned length, const char *match);
+
+#endif
diff --git a/src/parse.c b/src/parse.c
new file mode 100644
index 0000000..bac0215
--- /dev/null
+++ b/src/parse.c
@@ -0,0 +1,270 @@
+#include "parse.h"
+#include "tokenize.h"
+#include "locale.h"
+#include "hotkey.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#define internal static
+
+internal 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;
+}
+
+internal char *
+copy_string_count(char *s, int length)
+{
+ char *result = malloc(length + 1);
+ memcpy(result, s, length);
+ result[length] = '\0';
+ return result;
+}
+
+internal uint32_t
+keycode_from_hex(char *hex)
+{
+ uint32_t result;
+ sscanf(hex, "%x", &result);
+ return result;
+}
+
+internal char *
+parse_command(struct parser *parser)
+{
+ struct token command = parser_previous(parser);
+ char *result = copy_string_count(command.text, command.length);
+ printf("\tcmd: '%s'\n", result);
+ return result;
+}
+
+internal 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);
+ printf("\tkey: '%.*s' (%d)\n", key.length, key.text, keycode);
+ return keycode;
+}
+
+internal uint32_t
+parse_key(struct parser *parser)
+{
+ uint32_t keycode;
+ struct token key = parser_previous(parser);
+ if(key.length == 1) {
+ keycode = keycode_from_char(*key.text);
+ } else {
+ keycode = keycode_from_literal(key.text, key.length);
+ }
+ printf("\tkey: '%.*s' (%d)\n", key.length, key.text, keycode);
+ return keycode;
+}
+
+internal const char *modifier_flags_map[] =
+{
+ "alt", "lalt", "ralt",
+ "shift", "lshift", "rshift",
+ "cmd", "lcmd", "rcmd",
+ "ctrl", "lctrl", "rctrl",
+};
+internal 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,
+};
+
+internal uint32_t
+parse_modifier(struct parser *parser)
+{
+ uint32_t flags = 0;
+ struct token modifier = parser_previous(parser);
+
+ for(int i = 0; i < array_count(modifier_flags_map); ++i) {
+ if(same_string(modifier.text, modifier.length, modifier_flags_map[i])) {
+ flags |= modifier_flags_value[i];
+ printf("\tmod: '%s'\n", modifier_flags_map[i]);
+ break;
+ }
+ }
+
+ if(parser_match(parser, Token_Plus)) {
+ if(parser_match(parser, Token_Modifier)) {
+ flags |= parse_modifier(parser);
+ } else {
+ fprintf(stderr, "(#%d:%d) expected token 'Token_Modifier', but got '%.*s'\n",
+ parser->current_token.line, parser->current_token.cursor,
+ parser->current_token.length, parser->current_token.text);
+ parser->error = true;
+ }
+ }
+
+ return flags;
+}
+
+internal struct hotkey *
+parse_hotkey(struct parser *parser)
+{
+ struct hotkey *hotkey = malloc(sizeof(struct hotkey));
+ int found_modifier;
+
+ printf("(#%d) hotkey :: {\n", parser->current_token.line);
+
+ if(parser_match(parser, Token_Modifier)) {
+ hotkey->flags = parse_modifier(parser);
+ if(parser->error) {
+ return NULL;
+ }
+ found_modifier = 1;
+ } else {
+ hotkey->flags = found_modifier = 0;
+ }
+
+ if(found_modifier) {
+ if(!parser_match(parser, Token_Dash)) {
+ fprintf(stderr, "(#%d:%d) expected token '-', but got '%.*s'\n",
+ parser->current_token.line, parser->current_token.cursor,
+ parser->current_token.length, parser->current_token.text);
+ parser->error = true;
+ return NULL;
+ }
+ }
+
+ 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 {
+ fprintf(stderr, "(#%d:%d) expected token 'Token_Key', but got '%.*s'\n",
+ parser->current_token.line, parser->current_token.cursor,
+ parser->current_token.length, parser->current_token.text);
+ parser->error = true;
+ return NULL;
+ }
+
+ if(parser_match(parser, Token_Arrow)) {
+ hotkey->flags |= Hotkey_Flag_Passthrough;
+ }
+
+ if(parser_match(parser, Token_Command)) {
+ hotkey->command = parse_command(parser);
+ } else {
+ fprintf(stderr, "(#%d:%d) expected token 'Token_Command', but got '%.*s'\n",
+ parser->current_token.line, parser->current_token.cursor,
+ parser->current_token.length, parser->current_token.text);
+ parser->error = true;
+ return NULL;
+ }
+
+ printf("}\n");
+
+ hotkey->next = NULL;
+ return hotkey;
+}
+
+struct hotkey *
+parse_config(struct parser *parser)
+{
+ struct hotkey hotkeys;
+ struct hotkey *current_hotkey = &hotkeys;
+
+ while(!parser_eof(parser)) {
+ if((parser_check(parser, Token_Modifier)) ||
+ (parser_check(parser, Token_Key_Hex)) ||
+ (parser_check(parser, Token_Key))) {
+ current_hotkey->next = parse_hotkey(parser);
+ current_hotkey = current_hotkey->next;
+ } else {
+ fprintf(stderr, "(#%d:%d) expected token 'Token_Modifier', 'Token_Key_Hex' or 'Token_Key', but got '%.*s'\n",
+ parser->current_token.line, parser->current_token.cursor,
+ parser->current_token.length, parser->current_token.text);
+ parser->error = true;
+ return NULL;
+ }
+ }
+
+ return hotkeys.next;
+}
+
+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 0;
+ 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;
+}
+
+bool parser_init(struct parser *parser, char *file)
+{
+ memset(parser, 0, sizeof(struct parser));
+ char *buffer = read_file(file);
+ if(buffer) {
+ tokenizer_init(&parser->tokenizer, buffer);
+ parser_advance(parser);
+ return true;
+ }
+ return false;
+}
+
+void parser_destroy(struct parser *parser)
+{
+ free(parser->tokenizer.buffer);
+}
diff --git a/src/parse.h b/src/parse.h
new file mode 100644
index 0000000..f0ea0f7
--- /dev/null
+++ b/src/parse.h
@@ -0,0 +1,26 @@
+#ifndef SKHD_PARSE_H
+#define SKHD_PARSE_H
+
+#include "tokenize.h"
+#include <stdbool.h>
+
+struct parser
+{
+ struct token previous_token;
+ struct token current_token;
+ struct tokenizer tokenizer;
+ bool error;
+};
+
+struct hotkey *parse_config(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, char *file);
+void parser_destroy(struct parser *parser);
+
+#endif
diff --git a/src/skhd.c b/src/skhd.c
new file mode 100644
index 0000000..b40e32c
--- /dev/null
+++ b/src/skhd.c
@@ -0,0 +1,216 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <getopt.h>
+#include <signal.h>
+#include <string.h>
+
+#include <Carbon/Carbon.h>
+
+#include "hotload.h"
+#include "event_tap.h"
+#include "locale.h"
+#include "tokenize.h"
+#include "parse.h"
+#include "hotkey.h"
+
+#include "hotload.c"
+#include "event_tap.c"
+#include "locale.c"
+#include "tokenize.c"
+#include "parse.c"
+#include "hotkey.c"
+
+#define internal static
+extern bool CGSIsSecureEventInputSet();
+#define secure_keyboard_entry_enabled CGSIsSecureEventInputSet
+
+internal unsigned major_version = 0;
+internal unsigned minor_version = 0;
+internal unsigned patch_version = 1;
+internal char *config_file;
+struct hotkey *hotkeys;
+
+internal void
+error(const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ exit(EXIT_FAILURE);
+}
+
+internal bool
+watched_io_file(struct hotloader *hotloader, char *absolutepath)
+{
+ bool success = false;
+ for(unsigned index = 0; success == 0 && index < hotloader->watch_count; ++index) {
+ struct watched_file *watch_info = &hotloader->watch_list[index];
+
+ char *directory = file_directory(absolutepath);
+ char *filename = file_name(absolutepath);
+
+ if(strcmp(watch_info->directory, directory) == 0) {
+ if(strcmp(watch_info->filename, filename) == 0) {
+ success = true;
+ }
+ }
+
+ free(filename);
+ free(directory);
+ }
+
+ return success;
+}
+
+internal HOTLOADER_CALLBACK(hotloader_handler)
+{
+ struct hotloader *hotloader = (struct hotloader *) context;
+
+ char **files = (char **) paths;
+ for(unsigned index = 0; index < count; ++index) {
+ char *absolutepath = files[index];
+ if(watched_io_file(hotloader, absolutepath)) {
+ /* TODO(koekeishiya): We sometimes get two events upon file save.
+ * Filter the duplicated event or something ?? */
+ struct parser parser;
+ if(parser_init(&parser, absolutepath)) {
+ free_hotkeys(hotkeys);
+ hotkeys = parse_config(&parser);
+ parser_destroy(&parser);
+ }
+ }
+ }
+}
+
+internal EVENT_TAP_CALLBACK(key_handler)
+{
+ switch(type)
+ {
+ case kCGEventTapDisabledByTimeout:
+ case kCGEventTapDisabledByUserInput:
+ {
+ printf("skhd: restarting event-tap\n");
+ struct event_tap *event_tap = (struct event_tap *) reference;
+ CGEventTapEnable(event_tap->handle, 1);
+ } break;
+ case kCGEventKeyDown:
+ {
+ uint32_t flags = CGEventGetFlags(event);
+ uint32_t key = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
+ struct hotkey eventkey = cgevent_to_hotkey(flags, key);
+ if(find_and_exec_hotkey(&eventkey, hotkeys)) {
+ return NULL;
+ }
+ } break;
+ default: {} break;
+ }
+
+ return event;
+}
+
+internal bool
+parse_arguments(int argc, char **argv)
+{
+ int option;
+ const char *short_option = "vc:";
+ struct option long_option[] =
+ {
+ { "version", no_argument, NULL, 'v' },
+ { "config", required_argument, NULL, 'c' },
+ { NULL, 0, NULL, 0 }
+ };
+
+ while((option = getopt_long(argc, argv, short_option, long_option, NULL)) != -1) {
+ switch(option)
+ {
+ case 'v':
+ {
+ printf("skhd version %d.%d.%d\n", major_version, minor_version, patch_version);
+ return true;
+ } break;
+ case 'c':
+ {
+ config_file = strdup(optarg);
+ } break;
+ }
+ }
+
+ return false;
+}
+
+internal bool
+check_privileges()
+{
+ bool result = false;
+ 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;
+}
+
+internal void
+set_config_path()
+{
+ if(!config_file) {
+ char *home = getenv("HOME");
+ if(home) {
+ int length = strlen(home) + strlen("/.skhdrc");
+ config_file = (char *) malloc(length + 1);
+ strcpy(config_file, home);
+ strcat(config_file, "/.skhdrc");
+ } else {
+ config_file = strdup(".skhdrc");
+ }
+ }
+}
+
+int main(int argc, char **argv)
+{
+ if(parse_arguments(argc, argv)) {
+ return EXIT_SUCCESS;
+ }
+
+ if(secure_keyboard_entry_enabled()) {
+ error("skhd: secure keyboard entry is enabled! abort..\n");
+ }
+
+ if(!check_privileges()) {
+ error("skhd: must be run with accessibility access.\n");
+ }
+
+ signal(SIGCHLD, SIG_IGN);
+ set_config_path();
+ printf("skhd: using config '%s'\n", config_file);
+
+ struct parser parser;
+ if(parser_init(&parser, config_file)) {
+ hotkeys = parse_config(&parser);
+ parser_destroy(&parser);
+ } else {
+ error("skhd: could not open file '%s'\n", config_file);
+ }
+
+ struct event_tap event_tap;
+ event_tap.mask = (1 << kCGEventKeyDown);
+ event_tap_begin(&event_tap, key_handler);
+
+ struct hotloader hotloader = {};
+ hotloader_add_file(&hotloader, config_file);
+ hotloader_begin(&hotloader, hotloader_handler);
+
+ CFRunLoopRun();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/tokenize.c b/src/tokenize.c
new file mode 100644
index 0000000..53f294a
--- /dev/null
+++ b/src/tokenize.c
@@ -0,0 +1,171 @@
+#include "tokenize.h"
+#define internal static
+
+internal int
+token_equals(struct token token, const char *match)
+{
+ const char *at = match;
+ unsigned index = 0;
+ while(*at++ == token.text[index++] && index < token.length);
+ return (*at == '\0' && index == token.length) ? 1 : 0;
+}
+
+internal void
+advance(struct tokenizer *tokenizer)
+{
+ if(*tokenizer->at == '\n') {
+ tokenizer->cursor = 0;
+ ++tokenizer->line;
+ }
+ ++tokenizer->cursor;
+ ++tokenizer->at;
+}
+
+internal void
+eat_whitespace(struct tokenizer *tokenizer)
+{
+ while(*tokenizer->at && isspace(*tokenizer->at)) {
+ advance(tokenizer);
+ }
+}
+
+internal void
+eat_comment(struct tokenizer *tokenizer)
+{
+ while(*tokenizer->at && *tokenizer->at != '\n') {
+ advance(tokenizer);
+ }
+}
+
+internal void
+eat_command(struct tokenizer *tokenizer)
+{
+ while(*tokenizer->at && *tokenizer->at != '\n') {
+ if(*tokenizer->at == '\\') {
+ advance(tokenizer);
+ }
+ advance(tokenizer);
+ }
+}
+
+internal void
+eat_hex(struct tokenizer *tokenizer)
+{
+ while((*tokenizer->at) &&
+ ((isdigit(*tokenizer->at)) ||
+ (*tokenizer->at >= 'A' && *tokenizer->at <= 'F'))) {
+ advance(tokenizer);
+ }
+}
+
+internal void
+eat_identifier(struct tokenizer *tokenizer)
+{
+ while(*tokenizer->at && isalpha(*tokenizer->at)) {
+ advance(tokenizer);
+ }
+}
+
+internal enum token_type
+resolve_identifier_type(struct token token)
+{
+ if(token.length == 1) {
+ return Token_Key;
+ }
+
+ for(int i = 0; i < array_count(token_modifier_map); ++i) {
+ if(token_equals(token, token_modifier_map[i])) {
+ return Token_Modifier;
+ }
+ }
+
+ for(int i = 0; i < array_count(token_key_map); ++i) {
+ if(token_equals(token, token_key_map[i])) {
+ return Token_Key;
+ }
+ }
+
+ return Token_Unknown;
+}
+
+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 '-':
+ {
+ 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_command(tokenizer);
+ token.length = tokenizer->at - token.text;
+ token.type = Token_Command;
+ } break;
+ case '#':
+ {
+ eat_comment(tokenizer);
+ token = get_token(tokenizer);
+ } 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;
+}
diff --git a/src/tokenize.h b/src/tokenize.h
new file mode 100644
index 0000000..8b8ecf9
--- /dev/null
+++ b/src/tokenize.h
@@ -0,0 +1,66 @@
+#ifndef SKHD_TOKENIZE_H
+#define SKHD_TOKENIZE_H
+
+#define array_count(a) (sizeof((a)) / sizeof(*(a)))
+static const char *token_modifier_map[] =
+{
+ "lctrl", "ctrl", "rctrl",
+ "lalt", "alt", "ralt",
+ "lshift", "shift", "rshift",
+ "lcmd", "cmd", "rcmd",
+};
+
+static const char *token_key_map[] =
+{
+ "return", "tab", "space",
+ "backspace", "delete", "escape",
+ "capslock", "home", "end",
+ "pageup", "pagedown", "help",
+ "left", "right", "up",
+ "down", "f1", "f2",
+ "f3", "f4", "f5",
+ "f6", "f7", "f8",
+ "f9", "f10", "f11",
+ "f12", "f13", "f14",
+ "f15", "f16", "f17",
+ "f18", "f19", "f20",
+};
+
+enum token_type
+{
+ Token_Command,
+ Token_Modifier,
+ Token_Key_Hex,
+ Token_Key,
+
+ Token_Plus,
+ Token_Dash,
+ Token_Arrow,
+
+ 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);
+
+#endif