summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHunt <lizhunt@amazon.com>2025-06-27 14:25:22 -0700
committerHunt <lizhunt@amazon.com>2025-06-27 14:44:54 -0700
commit0ba773cb8cb5162019d42c511f2580d601ab65d1 (patch)
tree7224f2e249df760213216a97ee73220e83a04b9f
parentdccdc3326cea3ad1e951438e6d38170d84d186d8 (diff)
downloaddotfiles-0ba773cb8cb5162019d42c511f2580d601ab65d1.tar.gz
dotfiles-0ba773cb8cb5162019d42c511f2580d601ab65d1.zip
Add sketchybar
-rw-r--r--context.json114
-rw-r--r--dots/aerospace/.aerospace.toml.j2 (renamed from dots/aerospace/.aerospace.toml)140
-rw-r--r--dots/sketchybar/.config/sketchybar/bar.lua15
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/.gitignore1
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/Makefile3
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/menus/Makefile5
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/menus/menus.c248
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/network_load/Makefile5
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.c39
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.h93
-rw-r--r--dots/sketchybar/.config/sketchybar/bridge/sketchybar.h124
-rw-r--r--dots/sketchybar/.config/sketchybar/config/colors.lua38
-rw-r--r--dots/sketchybar/.config/sketchybar/config/dimens.lua40
-rw-r--r--dots/sketchybar/.config/sketchybar/config/fonts.lua.j214
-rw-r--r--dots/sketchybar/.config/sketchybar/config/icons.lua331
-rw-r--r--dots/sketchybar/.config/sketchybar/config/init.lua1
-rw-r--r--dots/sketchybar/.config/sketchybar/config/settings.lua11
-rw-r--r--dots/sketchybar/.config/sketchybar/constants.lua34
-rw-r--r--dots/sketchybar/.config/sketchybar/default.lua66
-rw-r--r--dots/sketchybar/.config/sketchybar/init.lua15
-rw-r--r--dots/sketchybar/.config/sketchybar/install/init.lua1
-rw-r--r--dots/sketchybar/.config/sketchybar/install/sbar.lua.j225
-rw-r--r--dots/sketchybar/.config/sketchybar/items/apple.lua7
-rw-r--r--dots/sketchybar/.config/sketchybar/items/front_apps.lua71
-rw-r--r--dots/sketchybar/.config/sketchybar/items/init.lua12
-rw-r--r--dots/sketchybar/.config/sketchybar/items/media.lua118
-rw-r--r--dots/sketchybar/.config/sketchybar/items/menu_spaces_toggle.lua71
-rw-r--r--dots/sketchybar/.config/sketchybar/items/menus.lua74
-rwxr-xr-xdots/sketchybar/.config/sketchybar/items/menus/bin/menusbin0 -> 36920 bytes
-rw-r--r--dots/sketchybar/.config/sketchybar/items/message.lua55
-rw-r--r--dots/sketchybar/.config/sketchybar/items/spaces.lua.j299
-rw-r--r--dots/sketchybar/.config/sketchybar/items/widgets/battery.lua91
-rw-r--r--dots/sketchybar/.config/sketchybar/items/widgets/calendar.lua17
-rw-r--r--dots/sketchybar/.config/sketchybar/items/widgets/init.lua4
-rw-r--r--dots/sketchybar/.config/sketchybar/items/widgets/volume.lua128
-rw-r--r--dots/sketchybar/.config/sketchybar/items/widgets/wifi.lua261
-rwxr-xr-xdots/sketchybar/.config/sketchybar/sketchybarrc3
-rw-r--r--dots/sketchybar/.config/sketchybar/util/.gitkeep0
38 files changed, 2290 insertions, 84 deletions
diff --git a/context.json b/context.json
index 18a76d8..be0b89b 100644
--- a/context.json
+++ b/context.json
@@ -12,6 +12,47 @@
"XDG_DATA_HOME" : "$HOME/.local/share",
"XDG_STATE_HOME" : "$HOME/.local/state",
"XDG_CACHE_HOME" : "$HOME/.cache"
+ },
+ "window_manager": {
+ "workspaces": {
+ "http": {
+ "apps": ["org.mozilla.firefox", "org.mozilla.librewolf"]
+ },
+ "mesg": {
+ "apps": ["com.tinyspeck.slackmacgap", "com.hnc.Discord",
+ "com.apple.mail", "org.whispersystems.signal-desktop"]
+ },
+ "call": {
+ "apps": ["com.amazon.Amazon-Chime"]
+ },
+ "mgmt": {
+ "apps": ["com.microsoft.Outlook"]
+ },
+ "misc": {
+ "apps": []
+ },
+ "txt": {
+ "apps": ["dev.zed.Zed", "org.gnu.Emacs", "com.neovide.neovide"]
+ },
+ "term": {
+ "apps": ["org.alacritty"]
+ },
+ "ctrl": {
+ "apps": ["com.apple.finder", "com.apple.systempreferences", "io.tailscale.ipn.macos",
+ "com.amazon.ACME"]
+ },
+ "play": {
+ "apps": ["com.tidal.desktop"]
+ },
+ "brn": {
+ "apps": []
+ }
+ }
+ },
+ "theme": {
+ "sketchybar": {
+ "font": "Lekton Nerd Font Mono"
+ }
}
},
"bcd074b37296": {
@@ -85,7 +126,80 @@
"home_localbin": "$HOME/.local/bin"
}
},
+ "window_manager": {
+ "key": {
+ "mod": "alt",
+ "mov": "shift",
+ "ed": "ctrl"
+ },
+ "workspaces": {
+ "http": {
+ "order": 0,
+ "key": "q",
+ "monitor": "main",
+ "icon": "󰖟"
+ },
+ "mesg": {
+ "order": 1,
+ "key": "w",
+ "monitor": "main",
+ "icon": "󱋊"
+ },
+ "mgmt": {
+ "order": 2,
+ "key": "e",
+ "monitor": "main",
+ "icon": "󰃥"
+ },
+ "misc": {
+ "order": 3,
+ "key": "r",
+ "monitor": "main",
+ "icon": "󱞁"
+ },
+ "call": {
+ "order": 4,
+ "key": "t",
+ "monitor": "main",
+ "icon": ""
+ },
+ "txt": {
+ "order": 5,
+ "key": "y",
+ "monitor": "secondary",
+ "icon": "󰘦"
+ },
+ "term": {
+ "order": 6,
+ "key": "u",
+ "monitor": "secondary",
+ "icon": ""
+ },
+ "ctrl": {
+ "order": 7,
+ "key": "i",
+ "monitor": "secondary",
+ "icon": ""
+ },
+ "brn": {
+ "order": 8,
+ "key": "o",
+ "monitor": "secondary",
+ "icon": "󰧑"
+ },
+ "play": {
+ "order": 9,
+ "key": "p",
+ "monitor": "secondary",
+ "icon": ""
+ }
+ }
+ },
"theme": {
+ "borders": {
+ "active_color": "fbf1c7df",
+ "inactive_color": "a998a45f"
+ },
"tmux": {
"plugin": {
"line": "set -g @plugin 'egel/tmux-gruvbox'",
diff --git a/dots/aerospace/.aerospace.toml b/dots/aerospace/.aerospace.toml.j2
index f4199e6..b04ce6e 100644
--- a/dots/aerospace/.aerospace.toml
+++ b/dots/aerospace/.aerospace.toml.j2
@@ -1,6 +1,19 @@
after-login-command = []
after-startup-command = [
- 'exec-and-forget borders active_color=0xffe1e3e4 inactive_color=0xff494d64 width=4.0'
+ 'exec-and-forget borders active_color=0x{{ theme.borders.active_color }} inactive_color=0x{{ theme.borders.inactive_color }} width=4.0',
+ 'exec-and-forget /opt/homebrew/bin/sketchybar'
+]
+
+exec-on-workspace-change = [
+ '/bin/bash',
+ '-c',
+ 'sketchybar --trigger aerospace_workspace_changed FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE'
+]
+
+on-focus-changed = [
+ 'move-mouse window-lazy-center',
+ 'exec-and-forget /bin/bash -c /opt/homebrew/bin/sketchybar --trigger front_app_switched',
+ 'exec-and-forget sketchybar --trigger update_windows'
]
start-at-login = true
@@ -22,10 +35,8 @@ default-root-container-layout = 'tiles'
# tall monitor (anything higher than wide) gets vertical orientation
default-root-container-orientation = 'auto'
-on-focus-changed = ['move-mouse window-lazy-center']
-
# You can effectively turn off macOS "Hide application" (cmd-h) feature by toggling this flag
-# Useful if you don't use this macOS feature, but accidentally hit cmd-h or cmd-alt-h key
+# Useful if you don't use this macOS feature, but accidentally hit cmd-h or cmd-{{ window_manager.key.mod }}-h key
# Also see: https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app
automatically-unhide-macos-hidden-apps = true
@@ -47,99 +58,53 @@ automatically-unhide-macos-hidden-apps = true
inner.vertical = 8
outer.left = 8
outer.bottom = 8
- outer.top = 8
+ outer.top = 58
outer.right = 8
[mode.main.binding]
# All possible commands: https://nikitabobko.github.io/AeroSpace/commands
# See: https://nikitabobko.github.io/AeroSpace/commands#layout
- alt-slash = 'layout tiles horizontal vertical'
- alt-comma = 'layout accordion horizontal vertical'
-
- alt-ctrl-d = 'close'
- alt-h = 'focus left'
- alt-j = 'focus down'
- alt-k = 'focus up'
- alt-l = 'focus right'
-
- alt-shift-h = 'move left'
- alt-shift-j = 'move down'
- alt-shift-k = 'move up'
- alt-shift-l = 'move right'
-
- alt-ctrl-space = ['mode main']
- alt-ctrl-h = ['join-with left', 'mode main']
- alt-ctrl-j = ['join-with down', 'mode main']
- alt-ctrl-k = ['join-with up', 'mode main']
- alt-ctrl-l = ['join-with right', 'mode main']
-
- alt-minus = 'resize smart -50'
- alt-equal = 'resize smart +50'
-
- # Monitor One Workspaces
- alt-1 = 'workspace 1'
- alt-2 = 'workspace 2'
- alt-3 = 'workspace 3'
- alt-4 = 'workspace 4'
- alt-5 = 'workspace 5'
- alt-q = 'workspace 1'
- alt-w = 'workspace 2'
- alt-e = 'workspace 3'
- alt-r = 'workspace 4'
- alt-t = 'workspace 5'
- alt-shift-1 = 'move-node-to-workspace 1'
- alt-shift-2 = 'move-node-to-workspace 2'
- alt-shift-3 = 'move-node-to-workspace 3'
- alt-shift-4 = 'move-node-to-workspace 4'
- alt-shift-5 = 'move-node-to-workspace 5'
- alt-shift-q = 'move-node-to-workspace 1'
- alt-shift-w = 'move-node-to-workspace 2'
- alt-shift-e = 'move-node-to-workspace 3'
- alt-shift-r = 'move-node-to-workspace 4'
- alt-shift-t = 'move-node-to-workspace 5'
-
- # Monitor Two Workspaces
- alt-6 = 'workspace 6'
- alt-7 = 'workspace 7'
- alt-8 = 'workspace 8'
- alt-9 = 'workspace 9'
- alt-0 = 'workspace 10'
- alt-y = 'workspace 6'
- alt-u = 'workspace 7'
- alt-i = 'workspace 8'
- alt-o = 'workspace 9'
- alt-p = 'workspace 10'
- alt-shift-6 = 'move-node-to-workspace 6'
- alt-shift-7 = 'move-node-to-workspace 7'
- alt-shift-8 = 'move-node-to-workspace 8'
- alt-shift-9 = 'move-node-to-workspace 9'
- alt-shift-0 = 'move-node-to-workspace 10'
- alt-shift-y = 'move-node-to-workspace 6'
- alt-shift-u = 'move-node-to-workspace 7'
- alt-shift-i = 'move-node-to-workspace 8'
- alt-shift-o = 'move-node-to-workspace 9'
- alt-shift-p = 'move-node-to-workspace 10'
+ {{ window_manager.key.mod }}-slash = 'layout tiles horizontal vertical'
+ {{ window_manager.key.mod }}-comma = 'layout accordion horizontal vertical'
+
+ {{ window_manager.key.mod }}-{{ window_manager.key.ed }}-d = 'close'
+ {{ window_manager.key.mod }}-h = 'focus left'
+ {{ window_manager.key.mod }}-j = 'focus down'
+ {{ window_manager.key.mod }}-k = 'focus up'
+ {{ window_manager.key.mod }}-l = 'focus right'
+
+ {{ window_manager.key.mod }}-{{ window_manager.key.mov }}-h = 'move left'
+ {{ window_manager.key.mod }}-{{ window_manager.key.mov }}-j = 'move down'
+ {{ window_manager.key.mod }}-{{ window_manager.key.mov }}-k = 'move up'
+ {{ window_manager.key.mod }}-{{ window_manager.key.mov }}-l = 'move right'
+
+ {{ window_manager.key.mod }}-{{ window_manager.key.ed }}-space = ['mode main']
+ {{ window_manager.key.mod }}-{{ window_manager.key.ed }}-h = ['join-with left', 'mode main']
+ {{ window_manager.key.mod }}-{{ window_manager.key.ed }}-j = ['join-with down', 'mode main']
+ {{ window_manager.key.mod }}-{{ window_manager.key.ed }}-k = ['join-with up', 'mode main']
+ {{ window_manager.key.mod }}-{{ window_manager.key.ed }}-l = ['join-with right', 'mode main']
+
+ {{ window_manager.key.mod }}-minus = 'resize smart -50'
+ {{ window_manager.key.mod }}-equal = 'resize smart +50'
+
+{% for name, ws in window_manager.workspaces.items() %}
+ {{ window_manager.key.mod }}-{{ ws.key }} = 'workspace {{ name }}'
+ {{ window_manager.key.mod }}-{{ window_manager.key.mov }}-{{ ws.key }} = 'move-node-to-workspace {{ name }}'
+{% endfor %}
# See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth
- alt-tab = 'workspace-back-and-forth'
+ {{ window_manager.key.mod }}-tab = 'workspace-back-and-forth'
# See: https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor
- alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'
+ {{ window_manager.key.mod }}-shift-tab = 'move-workspace-to-monitor --wrap-around next'
# See: https://nikitabobko.github.io/AeroSpace/commands#mode
- alt-shift-semicolon = 'mode service'
+ {{ window_manager.key.mod }}-shift-semicolon = 'mode service'
[workspace-to-monitor-force-assignment]
- 1 = 'main'
- 2 = 'main'
- 3 = 'main'
- 4 = 'main'
- 5 = 'main'
- 6 = 'secondary'
- 7 = 'secondary'
- 8 = 'secondary'
- 9 = 'secondary'
- 10 = 'secondary'
+{% for name, ws in window_manager.workspaces.items() %}
+ {{ name }} = '{{ ws.monitor }}'
+{% endfor %}
# 'service' binding mode declaration.
# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes
@@ -156,3 +121,10 @@ automatically-unhide-macos-hidden-apps = true
up = 'volume up'
shift-down = ['volume set 0', 'mode main']
+{% for name, ws in window_manager.workspaces.items() %}
+{% for app_id in ws.apps %}
+[[on-window-detected]]
+ if.app-id = '{{ app_id }}'
+ run = ['move-node-to-workspace {{ name }}']
+{% endfor %}
+{% endfor %}
diff --git a/dots/sketchybar/.config/sketchybar/bar.lua b/dots/sketchybar/.config/sketchybar/bar.lua
new file mode 100644
index 0000000..2eea723
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bar.lua
@@ -0,0 +1,15 @@
+local settings = require("config.settings")
+
+sbar.bar({
+ topmost = "window",
+ height = settings.dimens.graphics.bar.height,
+ color = settings.colors.bar.transparent,
+ padding_right = settings.dimens.padding.right,
+ padding = settings.dimens.padding.bar,
+ padding_left = settings.dimens.padding.left,
+ margin = settings.dimens.padding.bar,
+ corner_radius = settings.dimens.graphics.background.corner_radius,
+ y_offset = settings.dimens.graphics.bar.offset,
+ -- blur_radius = settings.dimens.graphics.blur_radius,
+ border_width = 0,
+})
diff --git a/dots/sketchybar/.config/sketchybar/bridge/.gitignore b/dots/sketchybar/.config/sketchybar/bridge/.gitignore
new file mode 100644
index 0000000..ba077a4
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/.gitignore
@@ -0,0 +1 @@
+bin
diff --git a/dots/sketchybar/.config/sketchybar/bridge/Makefile b/dots/sketchybar/.config/sketchybar/bridge/Makefile
new file mode 100644
index 0000000..43a4e04
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/Makefile
@@ -0,0 +1,3 @@
+all:
+ (cd network_load && $(MAKE)) >/dev/null
+ (cd menus && $(MAKE)) >/dev/null
diff --git a/dots/sketchybar/.config/sketchybar/bridge/menus/Makefile b/dots/sketchybar/.config/sketchybar/bridge/menus/Makefile
new file mode 100644
index 0000000..0cb454e
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/menus/Makefile
@@ -0,0 +1,5 @@
+bin/menus: menus.c | bin
+ clang -std=c99 -O3 -F/System/Library/PrivateFrameworks/ -framework Carbon -framework SkyLight $< -o $@
+
+bin:
+ mkdir bin
diff --git a/dots/sketchybar/.config/sketchybar/bridge/menus/menus.c b/dots/sketchybar/.config/sketchybar/bridge/menus/menus.c
new file mode 100644
index 0000000..2e77822
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/menus/menus.c
@@ -0,0 +1,248 @@
+#include <Carbon/Carbon.h>
+
+void ax_init() {
+ const void *keys[] = { kAXTrustedCheckOptionPrompt };
+ const void *values[] = { kCFBooleanTrue };
+
+ CFDictionaryRef options;
+ options = CFDictionaryCreate(kCFAllocatorDefault,
+ keys,
+ values,
+ sizeof(keys) / sizeof(*keys),
+ &kCFCopyStringDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks );
+
+ bool trusted = AXIsProcessTrustedWithOptions(options);
+ CFRelease(options);
+ if (!trusted) exit(1);
+}
+
+void ax_perform_click(AXUIElementRef element) {
+ if (!element) return;
+ AXUIElementPerformAction(element, kAXCancelAction);
+ usleep(150000);
+ AXUIElementPerformAction(element, kAXPressAction);
+}
+
+CFStringRef ax_get_title(AXUIElementRef element) {
+ CFTypeRef title = NULL;
+ AXError error = AXUIElementCopyAttributeValue(element,
+ kAXTitleAttribute,
+ &title );
+
+ if (error != kAXErrorSuccess) return NULL;
+ return title;
+}
+
+void ax_select_menu_option(AXUIElementRef app, int id) {
+ AXUIElementRef menubars_ref = NULL;
+ CFArrayRef children_ref = NULL;
+
+ AXError error = AXUIElementCopyAttributeValue(app,
+ kAXMenuBarAttribute,
+ (CFTypeRef*)&menubars_ref);
+ if (error == kAXErrorSuccess) {
+ error = AXUIElementCopyAttributeValue(menubars_ref,
+ kAXVisibleChildrenAttribute,
+ (CFTypeRef*)&children_ref );
+
+ if (error == kAXErrorSuccess) {
+ uint32_t count = CFArrayGetCount(children_ref);
+ if (id < count) {
+ AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, id);
+ ax_perform_click(item);
+ }
+ if (children_ref) CFRelease(children_ref);
+ }
+ if (menubars_ref) CFRelease(menubars_ref);
+ }
+}
+
+void ax_print_menu_options(AXUIElementRef app) {
+ AXUIElementRef menubars_ref = NULL;
+ CFTypeRef menubar = NULL;
+ CFArrayRef children_ref = NULL;
+
+ AXError error = AXUIElementCopyAttributeValue(app,
+ kAXMenuBarAttribute,
+ (CFTypeRef*)&menubars_ref);
+ if (error == kAXErrorSuccess) {
+ error = AXUIElementCopyAttributeValue(menubars_ref,
+ kAXVisibleChildrenAttribute,
+ (CFTypeRef*)&children_ref );
+
+ if (error == kAXErrorSuccess) {
+ uint32_t count = CFArrayGetCount(children_ref);
+
+ for (int i = 1; i < count; i++) {
+ AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, i);
+ CFTypeRef title = ax_get_title(item);
+
+ if (title) {
+ uint32_t buffer_len = 2*CFStringGetLength(title);
+ char buffer[2*CFStringGetLength(title)];
+ CFStringGetCString(title, buffer, buffer_len, kCFStringEncodingUTF8);
+ printf("%s\n", buffer);
+ CFRelease(title);
+ }
+ }
+ }
+ if (menubars_ref) CFRelease(menubars_ref);
+ if (children_ref) CFRelease(children_ref);
+ }
+}
+
+AXUIElementRef ax_get_extra_menu_item(char* alias) {
+ pid_t pid = 0;
+ CGRect bounds = CGRectNull;
+ CFArrayRef window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll,
+ kCGNullWindowID );
+ char owner_buffer[256];
+ char name_buffer[256];
+ char buffer[512];
+ int window_count = CFArrayGetCount(window_list);
+ for (int i = 0; i < window_count; ++i) {
+ CFDictionaryRef dictionary = CFArrayGetValueAtIndex(window_list, i);
+ if (!dictionary) continue;
+
+ CFStringRef owner_ref = CFDictionaryGetValue(dictionary,
+ kCGWindowOwnerName);
+
+ CFNumberRef owner_pid_ref = CFDictionaryGetValue(dictionary,
+ kCGWindowOwnerPID);
+
+ CFStringRef name_ref = CFDictionaryGetValue(dictionary, kCGWindowName);
+ CFNumberRef layer_ref = CFDictionaryGetValue(dictionary, kCGWindowLayer);
+ CFDictionaryRef bounds_ref = CFDictionaryGetValue(dictionary,
+ kCGWindowBounds);
+
+ if (!name_ref || !owner_ref || !owner_pid_ref || !layer_ref || !bounds_ref)
+ continue;
+
+ long long int layer = 0;
+ CFNumberGetValue(layer_ref, CFNumberGetType(layer_ref), &layer);
+ uint64_t owner_pid = 0;
+ CFNumberGetValue(owner_pid_ref,
+ CFNumberGetType(owner_pid_ref),
+ &owner_pid );
+
+ if (layer != 0x19) continue;
+ bounds = CGRectNull;
+ if (!CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) continue;
+ CFStringGetCString(owner_ref,
+ owner_buffer,
+ sizeof(owner_buffer),
+ kCFStringEncodingUTF8);
+
+ CFStringGetCString(name_ref,
+ name_buffer,
+ sizeof(name_buffer),
+ kCFStringEncodingUTF8);
+ snprintf(buffer, sizeof(buffer), "%s,%s", owner_buffer, name_buffer);
+
+ if (strcmp(buffer, alias) == 0) {
+ pid = owner_pid;
+ break;
+ }
+ }
+ CFRelease(window_list);
+ if (!pid) return NULL;
+
+ AXUIElementRef app = AXUIElementCreateApplication(pid);
+ if (!app) return NULL;
+ AXUIElementRef result = NULL;
+ CFTypeRef extras = NULL;
+ CFArrayRef children_ref = NULL;
+ AXError error = AXUIElementCopyAttributeValue(app,
+ kAXExtrasMenuBarAttribute,
+ &extras );
+ if (error == kAXErrorSuccess) {
+ error = AXUIElementCopyAttributeValue(extras,
+ kAXVisibleChildrenAttribute,
+ (CFTypeRef*)&children_ref );
+
+ if (error == kAXErrorSuccess) {
+ uint32_t count = CFArrayGetCount(children_ref);
+ for (uint32_t i = 0; i < count; i++) {
+ AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, i);
+ CFTypeRef position_ref = NULL;
+ CFTypeRef size_ref = NULL;
+ AXUIElementCopyAttributeValue(item, kAXPositionAttribute,
+ &position_ref );
+ AXUIElementCopyAttributeValue(item, kAXSizeAttribute,
+ &size_ref );
+ if (!position_ref || !size_ref) continue;
+
+ CGPoint position = CGPointZero;
+ AXValueGetValue(position_ref, kAXValueCGPointType, &position);
+ CGSize size = CGSizeZero;
+ AXValueGetValue(size_ref, kAXValueCGSizeType, &size);
+ CFRelease(position_ref);
+ CFRelease(size_ref);
+ // The offset is exactly 8 on macOS Sonoma...
+ // printf("%f %f\n", position.x, bounds.origin.x);
+ if (error == kAXErrorSuccess
+ && fabs(position.x - bounds.origin.x) <= 10) {
+ result = item;
+ break;
+ }
+ }
+ }
+ }
+
+ CFRelease(app);
+ return result;
+}
+
+extern int SLSMainConnectionID();
+extern void SLSSetMenuBarVisibilityOverrideOnDisplay(int cid, int did, bool enabled);
+extern void SLSSetMenuBarVisibilityOverrideOnDisplay(int cid, int did, bool enabled);
+extern void SLSSetMenuBarInsetAndAlpha(int cid, double u1, double u2, float alpha);
+void ax_select_menu_extra(char* alias) {
+ AXUIElementRef item = ax_get_extra_menu_item(alias);
+ if (!item) return;
+ SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 0.0);
+ SLSSetMenuBarVisibilityOverrideOnDisplay(SLSMainConnectionID(), 0, true);
+ SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 0.0);
+ ax_perform_click(item);
+ SLSSetMenuBarVisibilityOverrideOnDisplay(SLSMainConnectionID(), 0, false);
+ SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 1.0);
+ CFRelease(item);
+}
+
+extern void _SLPSGetFrontProcess(ProcessSerialNumber* psn);
+extern void SLSGetConnectionIDForPSN(int cid, ProcessSerialNumber* psn, int* cid_out);
+extern void SLSConnectionGetPID(int cid, pid_t* pid_out);
+AXUIElementRef ax_get_front_app() {
+ ProcessSerialNumber psn;
+ _SLPSGetFrontProcess(&psn);
+ int target_cid;
+ SLSGetConnectionIDForPSN(SLSMainConnectionID(), &psn, &target_cid);
+
+ pid_t pid;
+ SLSConnectionGetPID(target_cid, &pid);
+ return AXUIElementCreateApplication(pid);
+}
+
+int main (int argc, char **argv) {
+ if (argc == 1) {
+ printf("Usage: %s [-l | -s id/alias ]\n", argv[0]);
+ exit(0);
+ }
+ ax_init();
+ if (strcmp(argv[1], "-l") == 0) {
+ AXUIElementRef app = ax_get_front_app();
+ if (!app) return 1;
+ ax_print_menu_options(app);
+ CFRelease(app);
+ } else if (argc == 3 && strcmp(argv[1], "-s") == 0) {
+ int id = 0;
+ if (sscanf(argv[2], "%d", &id) == 1) {
+ AXUIElementRef app = ax_get_front_app();
+ if (!app) return 1;
+ ax_select_menu_option(app, id);
+ CFRelease(app);
+ } else ax_select_menu_extra(argv[2]);
+ }
+ return 0;
+}
diff --git a/dots/sketchybar/.config/sketchybar/bridge/network_load/Makefile b/dots/sketchybar/.config/sketchybar/bridge/network_load/Makefile
new file mode 100644
index 0000000..3eeee83
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/network_load/Makefile
@@ -0,0 +1,5 @@
+bin/network_load: network_load.c network_load.h ../sketchybar.h | bin
+ clang -std=c99 -O3 $< -o $@
+
+bin:
+ mkdir bin
diff --git a/dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.c b/dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.c
new file mode 100644
index 0000000..1be53b0
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.c
@@ -0,0 +1,39 @@
+#include "network_load.h"
+#include "../sketchybar.h"
+#include <unistd.h>
+
+int main(int argc, char **argv) {
+ float update_freq;
+ if (argc < 4 || (sscanf(argv[3], "%f", &update_freq) != 1)) {
+ printf("Usage: %s \"<interface>\" \"<event-name>\" \"<event_freq>\"\n",
+ argv[0]);
+ exit(1);
+ }
+
+ alarm(0);
+ // Setup the event in sketchybar
+ char event_message[512];
+ snprintf(event_message, 512, "--add event '%s'", argv[2]);
+ sketchybar(event_message);
+
+ struct network network;
+ network_init(&network, argv[1]);
+ char trigger_message[512];
+ for (;;) {
+ // Acquire new info
+ network_update(&network);
+
+ // Prepare the event message
+ snprintf(trigger_message, 512,
+ "--trigger '%s' upload='%03d%s' download='%03d%s'", argv[2],
+ network.up, unit_str[network.up_unit], network.down,
+ unit_str[network.down_unit]);
+
+ // Trigger the event
+ sketchybar(trigger_message);
+
+ // Wait
+ usleep(update_freq * 1000000);
+ }
+ return 0;
+}
diff --git a/dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.h b/dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.h
new file mode 100644
index 0000000..e3eba26
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/network_load/network_load.h
@@ -0,0 +1,93 @@
+#include <math.h>
+#include <net/if.h>
+#include <net/if_mib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/select.h>
+#include <sys/sysctl.h>
+
+static char unit_str[3][6] = {
+ {" Bps"},
+ {"KBps"},
+ {"MBps"},
+};
+
+enum unit { UNIT_BPS, UNIT_KBPS, UNIT_MBPS };
+struct network {
+ uint32_t row;
+ struct ifmibdata data;
+ struct timeval tv_nm1, tv_n, tv_delta;
+
+ int up;
+ int down;
+ enum unit up_unit, down_unit;
+};
+
+static inline void ifdata(uint32_t net_row, struct ifmibdata *data) {
+ static size_t size = sizeof(struct ifmibdata);
+ static int32_t data_option[] = {CTL_NET, PF_LINK, NETLINK_GENERIC,
+ IFMIB_IFDATA, 0, IFDATA_GENERAL};
+ data_option[4] = net_row;
+ sysctl(data_option, 6, data, &size, NULL, 0);
+}
+
+static inline void network_init(struct network *net, char *ifname) {
+ memset(net, 0, sizeof(struct network));
+
+ static int count_option[] = {CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_SYSTEM,
+ IFMIB_IFCOUNT};
+ uint32_t interface_count = 0;
+ size_t size = sizeof(uint32_t);
+ sysctl(count_option, 5, &interface_count, &size, NULL, 0);
+
+ for (int i = 0; i < interface_count; i++) {
+ ifdata(i, &net->data);
+ if (strcmp(net->data.ifmd_name, ifname) == 0) {
+ net->row = i;
+ break;
+ }
+ }
+}
+
+static inline void network_update(struct network *net) {
+ gettimeofday(&net->tv_n, NULL);
+ timersub(&net->tv_n, &net->tv_nm1, &net->tv_delta);
+ net->tv_nm1 = net->tv_n;
+
+ uint64_t ibytes_nm1 = net->data.ifmd_data.ifi_ibytes;
+ uint64_t obytes_nm1 = net->data.ifmd_data.ifi_obytes;
+ ifdata(net->row, &net->data);
+
+ double time_scale = (net->tv_delta.tv_sec + 1e-6 * net->tv_delta.tv_usec);
+ if (time_scale < 1e-6 || time_scale > 1e2)
+ return;
+ double delta_ibytes =
+ (double)(net->data.ifmd_data.ifi_ibytes - ibytes_nm1) / time_scale;
+ double delta_obytes =
+ (double)(net->data.ifmd_data.ifi_obytes - obytes_nm1) / time_scale;
+
+ double exponent_ibytes = log10(delta_ibytes);
+ double exponent_obytes = log10(delta_obytes);
+
+ if (exponent_ibytes < 3) {
+ net->down_unit = UNIT_BPS;
+ net->down = delta_ibytes;
+ } else if (exponent_ibytes < 6) {
+ net->down_unit = UNIT_KBPS;
+ net->down = delta_ibytes / 1000.0;
+ } else if (exponent_ibytes < 9) {
+ net->down_unit = UNIT_MBPS;
+ net->down = delta_ibytes / 1000000.0;
+ }
+
+ if (exponent_obytes < 3) {
+ net->up_unit = UNIT_BPS;
+ net->up = delta_obytes;
+ } else if (exponent_obytes < 6) {
+ net->up_unit = UNIT_KBPS;
+ net->up = delta_obytes / 1000.0;
+ } else if (exponent_obytes < 9) {
+ net->up_unit = UNIT_MBPS;
+ net->up = delta_obytes / 1000000.0;
+ }
+}
diff --git a/dots/sketchybar/.config/sketchybar/bridge/sketchybar.h b/dots/sketchybar/.config/sketchybar/bridge/sketchybar.h
new file mode 100644
index 0000000..b194d8a
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/bridge/sketchybar.h
@@ -0,0 +1,124 @@
+#pragma once
+
+#include <bootstrap.h>
+#include <mach/arm/kern_return.h>
+#include <mach/mach.h>
+#include <mach/mach_port.h>
+#include <mach/message.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+typedef char *env;
+
+#define MACH_HANDLER(name) void name(env env)
+typedef MACH_HANDLER(mach_handler);
+
+struct mach_message {
+ mach_msg_header_t header;
+ mach_msg_size_t msgh_descriptor_count;
+ mach_msg_ool_descriptor_t descriptor;
+};
+
+struct mach_buffer {
+ struct mach_message message;
+ mach_msg_trailer_t trailer;
+};
+
+static mach_port_t g_mach_port = 0;
+
+static inline mach_port_t mach_get_bs_port() {
+ mach_port_name_t task = mach_task_self();
+
+ mach_port_t bs_port;
+ if (task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bs_port) !=
+ KERN_SUCCESS) {
+ return 0;
+ }
+
+ char *name = getenv("BAR_NAME");
+ if (!name)
+ name = "sketchybar";
+ uint32_t lookup_len = 16 + strlen(name);
+
+ char buffer[lookup_len];
+ snprintf(buffer, lookup_len, "git.felix.%s", name);
+
+ mach_port_t port;
+ if (bootstrap_look_up(bs_port, buffer, &port) != KERN_SUCCESS)
+ return 0;
+ return port;
+}
+
+static inline bool mach_send_message(mach_port_t port, char *message,
+ uint32_t len) {
+ if (!message || !port) {
+ return false;
+ }
+
+ struct mach_message msg = {0};
+ msg.header.msgh_remote_port = port;
+ msg.header.msgh_local_port = 0;
+ msg.header.msgh_id = 0;
+ msg.header.msgh_bits =
+ MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND, 0,
+ MACH_MSGH_BITS_COMPLEX);
+
+ msg.header.msgh_size = sizeof(struct mach_message);
+ msg.msgh_descriptor_count = 1;
+ msg.descriptor.address = message;
+ msg.descriptor.size = len * sizeof(char);
+ msg.descriptor.copy = MACH_MSG_VIRTUAL_COPY;
+ msg.descriptor.deallocate = false;
+ msg.descriptor.type = MACH_MSG_OOL_DESCRIPTOR;
+
+ kern_return_t err =
+ mach_msg(&msg.header, MACH_SEND_MSG, sizeof(struct mach_message), 0,
+ MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
+
+ return err == KERN_SUCCESS;
+}
+
+static inline uint32_t format_message(char *message, char *formatted_message) {
+ // This is not actually robust, switch to stack based messaging.
+ char outer_quote = 0;
+ uint32_t caret = 0;
+ uint32_t message_length = strlen(message) + 1;
+ for (int i = 0; i < message_length; ++i) {
+ if (message[i] == '"' || message[i] == '\'') {
+ if (outer_quote && outer_quote == message[i])
+ outer_quote = 0;
+ else if (!outer_quote)
+ outer_quote = message[i];
+ continue;
+ }
+ formatted_message[caret] = message[i];
+ if (message[i] == ' ' && !outer_quote)
+ formatted_message[caret] = '\0';
+ caret++;
+ }
+
+ if (caret > 0 && formatted_message[caret] == '\0' &&
+ formatted_message[caret - 1] == '\0') {
+ caret--;
+ }
+ formatted_message[caret] = '\0';
+ return caret + 1;
+}
+
+static inline void sketchybar(char *message) {
+ char formatted_message[strlen(message) + 2];
+ uint32_t length = format_message(message, formatted_message);
+ if (!length)
+ return;
+
+ if (!g_mach_port)
+ g_mach_port = mach_get_bs_port();
+ if (!mach_send_message(g_mach_port, formatted_message, length)) {
+ g_mach_port = mach_get_bs_port();
+ if (!mach_send_message(g_mach_port, formatted_message, length)) {
+ // No sketchybar instance running, exit.
+ exit(0);
+ }
+ }
+}
diff --git a/dots/sketchybar/.config/sketchybar/config/colors.lua b/dots/sketchybar/.config/sketchybar/config/colors.lua
new file mode 100644
index 0000000..9029d24
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/config/colors.lua
@@ -0,0 +1,38 @@
+local colors <const> = {
+ black = 0xff181819,
+ white = 0xfff8f8f2,
+ red = 0xf1FD6592,
+ green = 0xff007692,
+ blue = 0xff5199ba,
+ yellow = 0xffffff81,
+ orange = 0xfff4c07b,
+ magenta = 0xd3fc7ebd,
+ purple = 0xff796fa9,
+ other_purple = 0xff302c45,
+ cyan = 0xff7bf2de,
+ grey = 0xff7f8490,
+ dirty_white = 0xc8cad3f5,
+ dark_grey = 0xff2b2736,
+ transparent = 0x00000000,
+ bar = {
+ bg = 0xf1151320,
+ border = 0xff2c2e34,
+ },
+ popup = {
+ bg = 0xf1151320,
+ border = 0xff2c2e34,
+ },
+ slider = {
+ bg = 0xf1151320,
+ border = 0xff2c2e34,
+ },
+ bg1 = 0xd322212c,
+ bg2 = 0xff302c45,
+
+ with_alpha = function(color, alpha)
+ if alpha > 1.0 or alpha < 0.0 then return color end
+ return (color & 0x00ffffff) | (math.floor(alpha * 255.0) << 24)
+ end,
+}
+
+return colors
diff --git a/dots/sketchybar/.config/sketchybar/config/dimens.lua b/dots/sketchybar/.config/sketchybar/config/dimens.lua
new file mode 100644
index 0000000..be69bdc
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/config/dimens.lua
@@ -0,0 +1,40 @@
+local padding <const> = {
+ background = 8,
+ icon = 10,
+ label = 8,
+ bar = 39,
+ left = 12,
+ right = 12,
+ item = 18,
+ popup = 8,
+}
+
+local graphics <const> = {
+ bar = {
+ height = 36,
+ offset = 12,
+ },
+ background = {
+ height = 24,
+ corner_radius = 9,
+ },
+ slider = {
+ height = 20,
+ },
+ popup = {
+ width = 200,
+ large_width = 300,
+ },
+ blur_radius = 30,
+}
+
+local text <const> = {
+ icon = 16.0,
+ label = 14.0,
+}
+
+return {
+ padding = padding,
+ graphics = graphics,
+ text = text,
+}
diff --git a/dots/sketchybar/.config/sketchybar/config/fonts.lua.j2 b/dots/sketchybar/.config/sketchybar/config/fonts.lua.j2
new file mode 100644
index 0000000..84f3f2b
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/config/fonts.lua.j2
@@ -0,0 +1,14 @@
+local dimens <const> = require("config.dimens")
+
+return {
+ text = "{{ theme.sketchybar.font }}",
+ numbers = "{{ theme.sketchybar.font }}",
+ icons = function(size)
+ local font = "sketchybar-app-font:Regular"
+ return size and font .. ":" .. size or font .. ":" .. dimens.text.icon
+ end,
+ styles = {
+ regular = "Regular",
+ bold = "Bold",
+ }
+}
diff --git a/dots/sketchybar/.config/sketchybar/config/icons.lua b/dots/sketchybar/.config/sketchybar/config/icons.lua
new file mode 100644
index 0000000..287a2fd
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/config/icons.lua
@@ -0,0 +1,331 @@
+local apps <const> = {
+ ["Live"] = ":ableton:",
+ ["Adobe Bridge 2024"] = ":adobe_bridge:",
+ ["Affinity Designer"] = ":affinity_designer:",
+ ["Affinity Designer 2"] = ":affinity_designer_2:",
+ ["Affinity Photo"] = ":affinity_photo:",
+ ["Affinity Photo 2"] = ":affinity_photo_2:",
+ ["Affinity Publisher"] = ":affinity_publisher:",
+ ["Affinity Publisher 2"] = ":affinity_publisher_2:",
+ ["Airmail"] = ":airmail:",
+ ["Alacritty"] = ":alacritty:",
+ ["Alfred Preferences"] = ":alfred:",
+ ["Android Messages"] = ":android_messages:",
+ ["Android Studio"] = ":android_studio:",
+ ["Anytype"] = ":anytype:",
+ ["App Eraser"] = ":app_eraser:",
+ ["App Store"] = ":app_store:",
+ ["Arc"] = ":arc:",
+ ["Atom"] = ":atom:",
+ ["Audacity"] = ":audacity:",
+ ["Bambu Studio"] = ":bambu_studio:",
+ ["MoneyMoney"] = ":bank:",
+ ["Bear"] = ":bear:",
+ ["BetterTouchTool"] = ":bettertouchtool:",
+ ["Bilibili"] = ":bilibili:",
+ ["哔哩哔哩"] = ":bilibili:",
+ ["Bitwarden"] = ":bit_warden:",
+ ["Blender"] = ":blender:",
+ ["BluOS Controller"] = ":bluos_controller:",
+ ["Calibre"] = ":book:",
+ ["Brave Browser"] = ":brave_browser:",
+ ["Calculator"] = ":calculator:",
+ ["Soulver 3"] = ":calculator:",
+ ["Calculette"] = ":calculator:",
+ ["Calendar"] = ":calendar:",
+ ["日历"] = ":calendar:",
+ ["Fantastical"] = ":calendar:",
+ ["Cron"] = ":calendar:",
+ ["Amie"] = ":calendar:",
+ ["Calendrier"] = ":calendar:",
+ ["Notion Calendar"] = ":calendar:",
+ ["Caprine"] = ":caprine:",
+ ["Citrix Workspace"] = ":citrix:",
+ ["Citrix Viewer"] = ":citrix:",
+ ["ClickUp"] = ":click_up:",
+ ["Code"] = ":code:",
+ ["Code - Insiders"] = ":code:",
+ ["Color Picker"] = ":color_picker:",
+ ["数码测色计"] = ":color_picker:",
+ ["CotEditor"] = ":coteditor:",
+ ["Cypress"] = ":cypress:",
+ ["DataGrip"] = ":datagrip:",
+ ["DataSpell"] = ":dataspell:",
+ ["DaVinci Resolve"] = ":davinciresolve:",
+ ["Default"] = ":default:",
+ ["CleanMyMac X"] = ":desktop:",
+ ["DEVONthink 3"] = ":devonthink3:",
+ ["DingTalk"] = ":dingtalk:",
+ ["钉钉"] = ":dingtalk:",
+ ["阿里钉"] = ":dingtalk:",
+ ["Discord"] = ":discord:",
+ ["Discord Canary"] = ":discord:",
+ ["Discord PTB"] = ":discord:",
+ ["Docker"] = ":docker:",
+ ["Docker Desktop"] = ":docker:",
+ ["GrandTotal"] = ":dollar:",
+ ["Receipts"] = ":dollar:",
+ ["Double Commander"] = ":doublecmd:",
+ ["Drafts"] = ":drafts:",
+ ["Dropbox"] = ":dropbox:",
+ ["Element"] = ":element:",
+ ["Emacs"] = ":emacs:",
+ ["Evernote Legacy"] = ":evernote_legacy:",
+ ["FaceTime"] = ":face_time:",
+ ["FaceTime 通话"] = ":face_time:",
+ ["Figma"] = ":figma:",
+ ["Final Cut Pro"] = ":final_cut_pro:",
+ ["Finder"] = ":finder:",
+ ["访达"] = ":finder:",
+ ["Firefox"] = ":firefox:",
+ ["Firefox Developer Edition"] = ":firefox_developer_edition:",
+ ["Firefox Nightly"] = ":firefox_developer_edition:",
+ ["Folx"] = ":folx:",
+ ["Fusion"] = ":fusion:",
+ ["System Preferences"] = ":gear:",
+ ["System Settings"] = ":gear:",
+ ["Systemeinstellungen"] = ":gear:",
+ ["系统设置"] = ":gear:",
+ ["Réglages Système"] = ":gear:",
+ ["GitHub Desktop"] = ":git_hub:",
+ ["Godot"] = ":godot:",
+ ["GoLand"] = ":goland:",
+ ["Chromium"] = ":google_chrome:",
+ ["Google Chrome"] = ":google_chrome:",
+ ["Google Chrome Canary"] = ":google_chrome:",
+ ["Grammarly Editor"] = ":grammarly:",
+ ["Home Assistant"] = ":home_assistant:",
+ ["Hyper"] = ":hyper:",
+ ["IntelliJ IDEA"] = ":idea:",
+ ["Inkdrop"] = ":inkdrop:",
+ ["Inkscape"] = ":inkscape:",
+ ["Insomnia"] = ":insomnia:",
+ ["Iris"] = ":iris:",
+ ["iTerm"] = ":iterm:",
+ ["iTerm2"] = ":iterm:",
+ ["Jellyfin Media Player"] = ":jellyfin:",
+ ["Joplin"] = ":joplin:",
+ ["카카오톡"] = ":kakaotalk:",
+ ["KakaoTalk"] = ":kakaotalk:",
+ ["Kakoune"] = ":kakoune:",
+ ["KeePassXC"] = ":kee_pass_x_c:",
+ ["Secrets"] = ":one_password:",
+ ["Keyboard Maestro"] = ":keyboard_maestro:",
+ ["Keynote"] = ":keynote:",
+ ["Keynote 讲演"] = ":keynote:",
+ ["kitty"] = ":kitty:",
+ ["League of Legends"] = ":league_of_legends:",
+ ["LibreWolf"] = ":libre_wolf:",
+ ["Adobe Lightroom"] = ":lightroom:",
+ ["Lightroom Classic"] = ":lightroomclassic:",
+ ["LINE"] = ":line:",
+ ["Linear"] = ":linear:",
+ ["LM Studio"] = ":lm_studio:",
+ ["LocalSend"] = ":localsend:",
+ ["Logic Pro"] = ":logicpro:",
+ ["Logseq"] = ":logseq:",
+ ["Canary Mail"] = ":mail:",
+ ["HEY"] = ":mail:",
+ ["Mail"] = ":mail:",
+ ["Mailspring"] = ":mail:",
+ ["MailMate"] = ":mail:",
+ ["Superhuman"] = ":mail:",
+ ["邮件"] = ":mail:",
+ ["MAMP"] = ":mamp:",
+ ["MAMP PRO"] = ":mamp:",
+ ["Maps"] = ":maps:",
+ ["Google Maps"] = ":maps:",
+ ["Matlab"] = ":matlab:",
+ ["Mattermost"] = ":mattermost:",
+ ["Messages"] = ":messages:",
+ ["信息"] = ":messages:",
+ ["Nachrichten"] = ":messages:",
+ ["Messenger"] = ":messenger:",
+ ["Microsoft Edge"] = ":microsoft_edge:",
+ ["Microsoft Excel"] = ":microsoft_excel:",
+ ["Microsoft Outlook"] = ":microsoft_outlook:",
+ ["Microsoft PowerPoint"] = ":microsoft_power_point:",
+ ["Microsoft Remote Desktop"] = ":microsoft_remote_desktop:",
+ ["Microsoft Teams"] = ":microsoft_teams:",
+ ["Microsoft Teams (work or school)"] = ":microsoft_teams:",
+ ["Microsoft Word"] = ":microsoft_word:",
+ ["Min"] = ":min_browser:",
+ ["Miro"] = ":miro:",
+ ["MongoDB Compass"] = ":mongodb:",
+ ["mpv"] = ":mpv:",
+ ["Mullvad Browser"] = ":mullvad_browser:",
+ ["Music"] = ":music:",
+ ["音乐"] = ":music:",
+ ["Musique"] = ":music:",
+ ["Neovide"] = ":neovide:",
+ ["neovide"] = ":neovide:",
+ ["Neovim"] = ":neovim:",
+ ["neovim"] = ":neovim:",
+ ["nvim"] = ":neovim:",
+ ["网易云音乐"] = ":netease_music:",
+ ["Noodl"] = ":noodl:",
+ ["Noodl Editor"] = ":noodl:",
+ ["NordVPN"] = ":nord_vpn:",
+ ["Notability"] = ":notability:",
+ ["Notes"] = ":notes:",
+ ["Notizen"] = ":notes:",
+ ["备忘录"] = ":notes:",
+ ["Notion"] = ":notion:",
+ ["Nova"] = ":nova:",
+ ["Numbers"] = ":numbers:",
+ ["Numbers 表格"] = ":numbers:",
+ ["Obsidian"] = ":obsidian:",
+ ["OBS"] = ":obsstudio:",
+ ["OmniFocus"] = ":omni_focus:",
+ ["1Password"] = ":one_password:",
+ ["ChatGPT"] = ":openai:",
+ ["OpenVPN Connect"] = ":openvpn_connect:",
+ ["Opera"] = ":opera:",
+ ["OrcaSlicer"] = ":orcaslicer:",
+ ["Orion"] = ":orion:",
+ ["Orion RC"] = ":orion:",
+ ["Pages"] = ":pages:",
+ ["Pages 文稿"] = ":pages:",
+ ["Parallels Desktop"] = ":parallels:",
+ ["Parsec"] = ":parsec:",
+ ["Preview"] = ":pdf:",
+ ["预览"] = ":pdf:",
+ ["Skim"] = ":pdf:",
+ ["zathura"] = ":pdf:",
+ ["Aperçu"] = ":pdf:",
+ ["PDF Expert"] = ":pdf_expert:",
+ ["Adobe Photoshop"] = ":photoshop:",
+ ["Pi-hole Remote"] = ":pihole:",
+ ["Pine"] = ":pine:",
+ ["Podcasts"] = ":podcasts:",
+ ["播客"] = ":podcasts:",
+ ["PomoDone App"] = ":pomodone:",
+ ["Postman"] = ":postman:",
+ ["PrusaSlicer"] = ":prusaslicer:",
+ ["SuperSlicer"] = ":prusaslicer:",
+ ["PyCharm"] = ":pycharm:",
+ ["QQ"] = ":qq:",
+ ["QQ音乐"] = ":qqmusic:",
+ ["QQMusic"] = ":qqmusic:",
+ ["Quantumult X"] = ":quantumult_x:",
+ ["qutebrowser"] = ":qute_browser:",
+ ["Raindrop.io"] = ":raindrop_io:",
+ ["Reeder"] = ":reeder5:",
+ ["Reminders"] = ":reminders:",
+ ["提醒事项"] = ":reminders:",
+ ["Rappels"] = ":reminders:",
+ ["Replit"] = ":replit:",
+ ["Rider"] = ":rider:",
+ ["JetBrains Rider"] = ":rider:",
+ ["Safari"] = ":safari:",
+ ["Safari浏览器"] = ":safari:",
+ ["Safari Technology Preview"] = ":safari:",
+ ["Sequel Ace"] = ":sequel_ace:",
+ ["Sequel Pro"] = ":sequel_pro:",
+ ["Setapp"] = ":setapp:",
+ ["SF Symbols"] = ":sf_symbols:",
+ ["Signal"] = ":signal:",
+ ["Sketch"] = ":sketch:",
+ ["Skype"] = ":skype:",
+ ["Slack"] = ":slack:",
+ ["Spark"] = ":spark:",
+ ["Spotify"] = ":spotify:",
+ ["Spotlight"] = ":spotlight:",
+ ["Sublime Text"] = ":sublime_text:",
+ ["Tana"] = ":tana:",
+ ["TeamSpeak 3"] = ":team_speak:",
+ ["Telegram"] = ":telegram:",
+ ["Terminal"] = ":terminal:",
+ ["终端"] = ":terminal:",
+ ["Typora"] = ":text:",
+ ["Microsoft To Do"] = ":things:",
+ ["Things"] = ":things:",
+ ["Thunderbird"] = ":thunderbird:",
+ ["TickTick"] = ":tick_tick:",
+ ["TIDAL"] = ":tidal:",
+ ["Tiny RDM"] = ":tinyrdm:",
+ ["Todoist"] = ":todoist:",
+ ["Toggl Track"] = ":toggl_track:",
+ ["Tor Browser"] = ":tor_browser:",
+ ["Tower"] = ":tower:",
+ ["Transmit"] = ":transmit:",
+ ["Trello"] = ":trello:",
+ ["Tweetbot"] = ":twitter:",
+ ["Twitter"] = ":twitter:",
+ ["MacVim"] = ":vim:",
+ ["Vim"] = ":vim:",
+ ["VimR"] = ":vim:",
+ ["Vivaldi"] = ":vivaldi:",
+ ["VLC"] = ":vlc:",
+ ["VMware Fusion"] = ":vmware_fusion:",
+ ["VSCodium"] = ":vscodium:",
+ ["Warp"] = ":warp:",
+ ["WebStorm"] = ":web_storm:",
+ ["微信"] = ":wechat:",
+ ["WeChat"] = ":wechat:",
+ ["企业微信"] = ":wecom:",
+ ["WeCom"] = ":wecom:",
+ ["WezTerm"] = ":wezterm:",
+ ["WhatsApp"] = ":whats_app:",
+ ["‎WhatsApp"] = ":whats_app:",
+ ["Xcode"] = ":xcode:",
+ ["Яндекс Музыка"] = ":yandex_music:",
+ ["Yuque"] = ":yuque:",
+ ["语雀"] = ":yuque:",
+ ["Zed"] = ":zed:",
+ ["Zeplin"] = ":zeplin:",
+ ["zoom.us"] = ":zoom:",
+ ["Zotero"] = ":zotero:",
+ ["Zulip"] = ":zulip:",
+ ["default"] = ":default:",
+}
+
+local text <const> = {
+ nerdfont = {
+ plus = "",
+ loading = "",
+ apple = "",
+ gear = "",
+ cpu = "",
+ clipboard = "󰅇",
+ switch = {
+ on = "󱨥",
+ off = "󱨦",
+ },
+ volume = {
+ _100 = "",
+ _66 = "",
+ _33 = "",
+ _10 = "",
+ _0 = "",
+ },
+ battery = {
+ _100 = "",
+ _75 = "",
+ _50 = "",
+ _25 = "",
+ _0 = "",
+ charging = "",
+ },
+ wifi = {
+ upload = "",
+ download = "",
+ connected = "󰖩",
+ disconnected = "󰖪",
+ router = "󰑩",
+ },
+ media = {
+ back = "",
+ forward = "",
+ play_pause = "",
+ },
+ slider = {
+ knob = ""
+ }
+ },
+}
+
+return {
+ text = text.nerdfont,
+ apps = apps,
+}
diff --git a/dots/sketchybar/.config/sketchybar/config/init.lua b/dots/sketchybar/.config/sketchybar/config/init.lua
new file mode 100644
index 0000000..255488f
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/config/init.lua
@@ -0,0 +1 @@
+return require("config.settings")
diff --git a/dots/sketchybar/.config/sketchybar/config/settings.lua b/dots/sketchybar/.config/sketchybar/config/settings.lua
new file mode 100644
index 0000000..d03458c
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/config/settings.lua
@@ -0,0 +1,11 @@
+local colors <const> = require("config.colors")
+local fonts <const> = require("config.fonts")
+local icons <const> = require("config.icons")
+local dimens <const> = require("config.dimens")
+
+return {
+ fonts = fonts,
+ dimens = dimens,
+ colors = colors,
+ icons = icons,
+}
diff --git a/dots/sketchybar/.config/sketchybar/constants.lua b/dots/sketchybar/.config/sketchybar/constants.lua
new file mode 100644
index 0000000..ed52f12
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/constants.lua
@@ -0,0 +1,34 @@
+local events <const> = {
+ AEROSPACE_WORKSPACE_CHANGED = "aerospace_workspace_changed",
+ AEROSPACE_SWITCH = "aerospace_switch",
+ SWAP_MENU_AND_SPACES = "swap_menu_and_spaces",
+ FRONT_APP_SWITCHED = "front_app_switched",
+ UPDATE_WINDOWS = "update_windows",
+ SEND_MESSAGE = "send_message",
+ HIDE_MESSAGE = "hide_message",
+}
+
+local items <const> = {
+ SPACES = "workspaces",
+ MENU = "menu",
+ MENU_TOGGLE = "menu_toggle",
+ FRONT_APPS = "front_apps",
+ MESSAGE = "message",
+ VOLUME = "widgets.volume",
+ WIFI = "widgets.wifi",
+ BATTERY = "widgets.battery",
+ CALENDAR = "widgets.calendar",
+}
+
+local aerospace <const> = {
+ LIST_ALL_WORKSPACES = "aerospace list-workspaces --all",
+ GET_CURRENT_WORKSPACE = "aerospace list-workspaces --focused",
+ LIST_WINDOWS = "aerospace list-windows --workspace focused --format \"id=%{window-id}, name=%{app-name}\"",
+ GET_CURRENT_WINDOW = "aerospace list-windows --focused --format %{app-name}",
+}
+
+return {
+ items = items,
+ events = events,
+ aerospace = aerospace,
+}
diff --git a/dots/sketchybar/.config/sketchybar/default.lua b/dots/sketchybar/.config/sketchybar/default.lua
new file mode 100644
index 0000000..fbee54b
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/default.lua
@@ -0,0 +1,66 @@
+local settings = require("config.settings")
+
+sbar.default({
+ updates = "when_shown",
+ icon = {
+ font = {
+ family = settings.fonts.text,
+ style = settings.fonts.styles.regular,
+ size = settings.dimens.text.icon,
+ },
+ color = settings.colors.white,
+ padding_left = settings.dimens.padding.icon,
+ padding_right = settings.dimens.padding.icon,
+ },
+ label = {
+ font = {
+ family = settings.fonts.text,
+ style = settings.fonts.styles.regular,
+ size = settings.dimens.text.label,
+ },
+ color = settings.colors.white,
+ padding_left = settings.dimens.padding.label,
+ padding_right = settings.dimens.padding.label,
+ },
+ background = {
+ height = settings.dimens.graphics.background.height,
+ corner_radius = settings.dimens.graphics.background.corner_radius,
+ border_width = 0,
+ image = {
+ corner_radius = settings.dimens.graphics.background.corner_radius
+ }
+ },
+ popup = {
+ y_offset = settings.dimens.padding.popup,
+ align = "center",
+ background = {
+ border_width = 0,
+ corner_radius = settings.dimens.graphics.background.corner_radius,
+ color = settings.colors.popup.bg,
+ shadow = { drawing = true },
+ padding_left = settings.dimens.padding.icon,
+ padding_right = settings.dimens.padding.icon,
+ },
+ blur_radius = settings.dimens.graphics.blur_radius,
+ },
+ slider = {
+ highlight_color = settings.colors.orange,
+ background = {
+ height = settings.dimens.graphics.slider.height,
+ corner_radius = settings.dimens.graphics.background.corner_radius,
+ color = settings.colors.slider.bg,
+ border_color = settings.colors.slider.border,
+ border_width = 1,
+ },
+ knob = {
+ font = {
+ family = settings.fonts.text,
+ style = settings.fonts.styles.regular,
+ size = 32,
+ },
+ string = settings.icons.text.slider.knob,
+ drawing = false,
+ },
+ },
+ scroll_texts = true,
+})
diff --git a/dots/sketchybar/.config/sketchybar/init.lua b/dots/sketchybar/.config/sketchybar/init.lua
new file mode 100644
index 0000000..5dfa643
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/init.lua
@@ -0,0 +1,15 @@
+require("install.sbar")
+
+sbar = require("sketchybar")
+
+sbar.begin_config()
+sbar.hotload(true)
+
+require("constants")
+require("config")
+require("bar")
+require("default")
+require("items")
+
+sbar.end_config()
+sbar.event_loop()
diff --git a/dots/sketchybar/.config/sketchybar/install/init.lua b/dots/sketchybar/.config/sketchybar/install/init.lua
new file mode 100644
index 0000000..bde1a41
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/install/init.lua
@@ -0,0 +1 @@
+require("install.sbar")
diff --git a/dots/sketchybar/.config/sketchybar/install/sbar.lua.j2 b/dots/sketchybar/.config/sketchybar/install/sbar.lua.j2
new file mode 100644
index 0000000..38a1e47
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/install/sbar.lua.j2
@@ -0,0 +1,25 @@
+local sbarpath = "{{ user.home }}" .. "/.local/share/sketchybar_lua/"
+
+local function exists(file)
+ local ok, err, code = os.rename(file, file)
+ if not ok then
+ if code == 13 then
+ return true
+ end
+ end
+ return ok, err
+end
+
+local function isdir(path)
+ return exists(path .. "/")
+end
+
+if not isdir(sbarpath) then
+ os.execute(
+ "git clone https://github.com/FelixKratz/SbarLua.git /tmp/SbarLua && cd /tmp/SbarLua && make install && rm -rf /tmp/SbarLua/"
+ )
+end
+
+package.cpath = package.cpath .. ";" .. sbarpath .. "?.so"
+
+os.execute("(cd bridge && make)")
diff --git a/dots/sketchybar/.config/sketchybar/items/apple.lua b/dots/sketchybar/.config/sketchybar/items/apple.lua
new file mode 100644
index 0000000..001f348
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/apple.lua
@@ -0,0 +1,7 @@
+local settings = require("config.settings")
+
+local apple = sbar.add("item", "apple", {
+ icon = { string = settings.icons.text.apple },
+ label = { drawing = false },
+ click_script = "$CONFIG_DIR/items/menus/bin/menus -s 0"
+})
diff --git a/dots/sketchybar/.config/sketchybar/items/front_apps.lua b/dots/sketchybar/.config/sketchybar/items/front_apps.lua
new file mode 100644
index 0000000..dc0d606
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/front_apps.lua
@@ -0,0 +1,71 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local frontApps = {}
+
+sbar.add("bracket", constants.items.FRONT_APPS, {}, { position = "left" })
+
+local frontAppWatcher = sbar.add("item", {
+ drawing = false,
+ updates = true,
+})
+
+local function selectFocusedWindow(frontAppName)
+ for appName, app in pairs(frontApps) do
+ local isSelected = appName == frontAppName
+ local color = isSelected and settings.colors.orange or settings.colors.white
+ app:set(
+ {
+ label = { color = color },
+ icon = { color = color },
+ }
+ )
+ end
+end
+
+local function updateWindows(windows)
+ sbar.remove("/" .. constants.items.FRONT_APPS .. "\\.*/")
+
+ frontApps = {}
+ local foundWindows = string.gmatch(windows, "[^\n]+")
+ for window in foundWindows do
+ local parsedWindow = {}
+ for key, value in string.gmatch(window, "(%w+)=([%w%s]+)") do
+ parsedWindow[key] = value
+ end
+
+ local windowId = parsedWindow["id"]
+ local windowName = parsedWindow["name"]
+ local icon = settings.icons.apps[windowName] or settings.icons.apps["default"]
+
+ frontApps[windowName] = sbar.add("item", constants.items.FRONT_APPS .. "." .. windowName, {
+ label = {
+ padding_left = 0,
+ string = windowName,
+ },
+ icon = {
+ string = icon,
+ font = settings.fonts.icons(),
+ },
+ click_script = "aerospace focus --window-id " .. windowId,
+ })
+
+ frontApps[windowName]:subscribe(constants.events.FRONT_APP_SWITCHED, function(env)
+ selectFocusedWindow(env.INFO)
+ end)
+ end
+
+ sbar.exec(constants.aerospace.GET_CURRENT_WINDOW, function(frontAppName)
+ selectFocusedWindow(frontAppName:gsub("[\n\r]", ""))
+ end)
+end
+
+local function getWindows()
+ sbar.exec(constants.aerospace.LIST_WINDOWS, updateWindows)
+end
+
+frontAppWatcher:subscribe(constants.events.UPDATE_WINDOWS, function()
+ getWindows()
+end)
+
+getWindows()
diff --git a/dots/sketchybar/.config/sketchybar/items/init.lua b/dots/sketchybar/.config/sketchybar/items/init.lua
new file mode 100644
index 0000000..30625e3
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/init.lua
@@ -0,0 +1,12 @@
+-- Left items
+require("items.apple")
+require("items.menu_spaces_toggle")
+require("items.menus")
+require("items.spaces")
+require("items.front_apps")
+
+-- Right items
+require("items.message")
+require("items.widgets")
+
+-- require("items.media")
diff --git a/dots/sketchybar/.config/sketchybar/items/media.lua b/dots/sketchybar/.config/sketchybar/items/media.lua
new file mode 100644
index 0000000..1d34084
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/media.lua
@@ -0,0 +1,118 @@
+local colors = require("config.colors")
+
+local whitelist = {
+ ["Psst"] = true,
+};
+
+local media_cover = sbar.add("item", {
+ position = "left",
+ background = {
+ image = {
+ string = "media.artwork",
+ scale = 0.80,
+ },
+ color = colors.transparent,
+ },
+ label = { drawing = false },
+ icon = { drawing = false },
+ drawing = false,
+ updates = true,
+ popup = {
+ align = "center",
+ horizontal = true,
+ }
+})
+
+local media_artist = sbar.add("item", {
+ position = "left",
+ drawing = false,
+ padding_left = 3,
+ padding_right = 0,
+ width = 0,
+ icon = { drawing = false },
+ label = {
+ width = 0,
+ font = { size = 9 },
+ color = colors.with_alpha(colors.white, 0.6),
+ max_chars = 24,
+ y_offset = 6,
+ },
+})
+
+local media_title = sbar.add("item", {
+ position = "left",
+ drawing = false,
+ padding_left = 3,
+ padding_right = 0,
+ icon = { drawing = false },
+ label = {
+ font = { size = 11 },
+ width = 0,
+ max_chars = 35,
+ y_offset = -5,
+ },
+})
+
+sbar.add("item", {
+ position = "popup." .. media_cover.name,
+ icon = { string = icons.media.back },
+ label = { drawing = false },
+ click_script = "nowplaying-cli previous",
+})
+sbar.add("item", {
+ position = "popup." .. media_cover.name,
+ icon = { string = icons.media.play_pause },
+ label = { drawing = false },
+ click_script = "nowplaying-cli togglePlayPause",
+})
+sbar.add("item", {
+ position = "popup." .. media_cover.name,
+ icon = { string = icons.media.forward },
+ label = { drawing = false },
+ click_script = "nowplaying-cli next",
+})
+
+local interrupt = 0
+local function animate_detail(detail)
+ if (not detail) then interrupt = interrupt - 1 end
+ if interrupt > 0 and (not detail) then return end
+
+ sbar.animate("tanh", 30, function()
+ media_artist:set({ label = { width = detail and "dynamic" or 0 } })
+ media_title:set({ label = { width = detail and "dynamic" or 0 } })
+ end)
+end
+
+media_cover:subscribe("media_change", function(env)
+ if whitelist[env.INFO.app] then
+ local drawing = (env.INFO.state == "playing")
+ media_artist:set({ drawing = drawing, label = env.INFO.artist, })
+ media_title:set({ drawing = drawing, label = env.INFO.title, })
+ media_cover:set({ drawing = drawing })
+
+ if drawing then
+ animate_detail(true)
+ interrupt = interrupt + 1
+ sbar.delay(5, animate_detail)
+ else
+ media_cover:set({ popup = { drawing = false } })
+ end
+ end
+end)
+
+media_cover:subscribe("mouse.entered", function(env)
+ interrupt = interrupt + 1
+ animate_detail(true)
+end)
+
+media_cover:subscribe("mouse.exited", function(env)
+ animate_detail(false)
+end)
+
+media_cover:subscribe("mouse.clicked", function(env)
+ media_cover:set({ popup = { drawing = "toggle" } })
+end)
+
+media_title:subscribe("mouse.exited.global", function(env)
+ media_cover:set({ popup = { drawing = false } })
+end)
diff --git a/dots/sketchybar/.config/sketchybar/items/menu_spaces_toggle.lua b/dots/sketchybar/.config/sketchybar/items/menu_spaces_toggle.lua
new file mode 100644
index 0000000..bed73d5
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/menu_spaces_toggle.lua
@@ -0,0 +1,71 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+sbar.add("event", constants.events.SWAP_MENU_AND_SPACES)
+
+local function switchToggle(menuToggle)
+ local isShowingMenu = menuToggle:query().icon.value == settings.icons.text.switch.on
+
+ menuToggle:set({
+ icon = isShowingMenu and settings.icons.text.switch.off or settings.icons.text.switch.on,
+ label = isShowingMenu and "Menus" or "Spaces",
+ })
+
+ sbar.trigger(constants.events.SWAP_MENU_AND_SPACES, { isShowingMenu = isShowingMenu })
+end
+
+local function addToggle()
+ local menuToggle = sbar.add("item", constants.items.MENU_TOGGLE, {
+ icon = {
+ string = settings.icons.text.switch.on
+ },
+ label = {
+ width = 0,
+ color = settings.colors.bg1,
+ string = "Spaces",
+ },
+ background = {
+ color = settings.colors.with_alpha(settings.colors.dirty_white, 0.0),
+ }
+ })
+
+ sbar.add("item", constants.items.MENU_TOGGLE .. ".padding", {
+ width = settings.dimens.padding.label
+ })
+
+ menuToggle:subscribe("mouse.entered", function(env)
+ sbar.animate("tanh", 30, function()
+ menuToggle:set({
+ background = {
+ color = { alpha = 1.0 },
+ border_color = { alpha = 0.5 },
+ },
+ icon = { color = settings.colors.bg1 },
+ label = { width = "dynamic" }
+ })
+ end)
+ end)
+
+ menuToggle:subscribe("mouse.exited", function(env)
+ sbar.animate("tanh", 30, function()
+ menuToggle:set({
+ background = {
+ color = { alpha = 0.0 },
+ border_color = { alpha = 0.0 },
+ },
+ icon = { color = settings.colors.white },
+ label = { width = 0 }
+ })
+ end)
+ end)
+
+ menuToggle:subscribe("mouse.clicked", function(env)
+ switchToggle(menuToggle)
+ end)
+
+ menuToggle:subscribe(constants.events.AEROSPACE_SWITCH, function(env)
+ switchToggle(menuToggle)
+ end)
+end
+
+addToggle()
diff --git a/dots/sketchybar/.config/sketchybar/items/menus.lua b/dots/sketchybar/.config/sketchybar/items/menus.lua
new file mode 100644
index 0000000..674963a
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/menus.lua
@@ -0,0 +1,74 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local maxItems <const> = 15
+local menuItems = {}
+local isShowingMenu = false
+
+local frontAppWatcher = sbar.add("item", {
+ drawing = false,
+ updates = true,
+})
+
+local swapWatcher = sbar.add("item", {
+ drawing = false,
+ updates = true,
+})
+
+local function createPlaceholders()
+ for index = 1, maxItems, 1 do
+ local menu = sbar.add("item", constants.items.MENU .. "." .. index, {
+ drawing = false,
+ icon = { drawing = false },
+ width = "dynamic",
+ label = {
+ font = {
+ style = index == 1 and settings.fonts.styles.bold or settings.fonts.styles.regular,
+ },
+ },
+ click_script = "$CONFIG_DIR/bridge/menus/bin/menus -s " .. index,
+ })
+ menuItems[index] = menu
+ end
+
+ sbar.add("bracket", { "/" .. constants.items.MENU .. "\\..*/" }, {
+ background = {
+ color = settings.colors.bg1,
+ padding_left = settings.dimens.padding.item,
+ padding_right = settings.dimens.padding.item,
+ },
+ })
+end
+
+local function updateMenus()
+ sbar.set("/" .. constants.items.MENU .. "\\..*/", { drawing = false })
+
+ sbar.exec("$CONFIG_DIR/bridge/menus/bin/menus -l", function(menus)
+ local index = 1
+ for menu in string.gmatch(menus, '[^\r\n]+') do
+ if index < maxItems then
+ menuItems[index]:set(
+ {
+ width = "dynamic",
+ label = menu,
+ drawing = isShowingMenu
+ }
+ )
+ else
+ break
+ end
+ index = index + 1
+ end
+ end)
+
+ sbar.set(constants.items.MENU .. ".padding", { drawing = isShowingMenu })
+end
+
+frontAppWatcher:subscribe(constants.events.FRONT_APP_SWITCHED, updateMenus)
+
+swapWatcher:subscribe(constants.events.SWAP_MENU_AND_SPACES, function(env)
+ isShowingMenu = env.isShowingMenu == "on"
+ updateMenus()
+end)
+
+createPlaceholders()
diff --git a/dots/sketchybar/.config/sketchybar/items/menus/bin/menus b/dots/sketchybar/.config/sketchybar/items/menus/bin/menus
new file mode 100755
index 0000000..4d92744
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/menus/bin/menus
Binary files differ
diff --git a/dots/sketchybar/.config/sketchybar/items/message.lua b/dots/sketchybar/.config/sketchybar/items/message.lua
new file mode 100644
index 0000000..1be586a
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/message.lua
@@ -0,0 +1,55 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local message = sbar.add("item", constants.items.MESSAGE, {
+ width = 0,
+ position = "center",
+ popup = { align = "center" },
+ label = {
+ padding_left = 0,
+ padding_right = 0,
+ },
+ background = {
+ padding_left = 0,
+ padding_right = 0,
+ }
+})
+
+local messagePopup = sbar.add("item", {
+ position = "popup." .. message.name,
+ width = "dynamic",
+ label = {
+ padding_right = settings.dimens.padding.label,
+ padding_left = settings.dimens.padding.label,
+ },
+ icon = {
+ padding_left = 0,
+ padding_right = 0,
+ },
+})
+
+local function hideMessage()
+ message:set({ popup = { drawing = false } })
+end
+
+local function showMessage(content, hold)
+ hideMessage()
+
+ message:set({ popup = { drawing = true } })
+ messagePopup:set({ label = { string = content } })
+
+ if hold == false then
+ sbar.delay(5, function()
+ if hold then return end
+ hideMessage()
+ end)
+ end
+end
+
+message:subscribe(constants.events.SEND_MESSAGE, function(env)
+ local content = env.MESSAGE
+ local hold = env.HOLD ~= nil and env.HOLD == "true" or false
+ showMessage(content, hold)
+end)
+
+message:subscribe(constants.events.HIDE_MESSAGE, hideMessage)
diff --git a/dots/sketchybar/.config/sketchybar/items/spaces.lua.j2 b/dots/sketchybar/.config/sketchybar/items/spaces.lua.j2
new file mode 100644
index 0000000..6d1d6a9
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/spaces.lua.j2
@@ -0,0 +1,99 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local spaces = {}
+
+local swapWatcher = sbar.add("item", {
+ drawing = false,
+ updates = true,
+})
+
+local currentWorkspaceWatcher = sbar.add("item", {
+ drawing = false,
+ updates = true,
+})
+
+local spaceConfigs <const> = {
+{% for name, ws in window_manager.workspaces.items() %}
+ ["{{ name }}"] = { icon = "{{ ws.icon }}", name = "{{ name }}" },
+{% endfor %}
+}
+
+local function selectCurrentWorkspace(focusedWorkspaceName)
+ for sid, item in pairs(spaces) do
+ if item ~= nil then
+ local isSelected = sid == constants.items.SPACES .. "." .. focusedWorkspaceName
+ item:set({
+ icon = { color = isSelected and settings.colors.bg1 or settings.colors.white },
+ label = { color = isSelected and settings.colors.bg1 or settings.colors.white },
+ background = { color = isSelected and settings.colors.white or settings.colors.bg1 },
+ })
+ end
+ end
+
+ sbar.trigger(constants.events.UPDATE_WINDOWS)
+end
+
+local function findAndSelectCurrentWorkspace()
+ sbar.exec(constants.aerospace.GET_CURRENT_WORKSPACE, function(focusedWorkspaceOutput)
+ local focusedWorkspaceName = focusedWorkspaceOutput:match("[^\r\n]+")
+ selectCurrentWorkspace(focusedWorkspaceName)
+ end)
+end
+
+local function addWorkspaceItem(workspaceName)
+ local spaceName = constants.items.SPACES .. "." .. workspaceName
+ local spaceConfig = spaceConfigs[workspaceName]
+
+ spaces[spaceName] = sbar.add("item", spaceName, {
+ label = {
+ width = 0,
+ padding_left = 0,
+ string = spaceConfig.name,
+ },
+ icon = {
+ string = spaceConfig.icon or settings.icons.apps["default"],
+ color = settings.colors.white,
+ },
+ background = {
+ color = settings.colors.bg1,
+ },
+ click_script = "aerospace workspace " .. workspaceName,
+ })
+
+ spaces[spaceName]:subscribe("mouse.entered", function(env)
+ sbar.animate("tanh", 30, function()
+ spaces[spaceName]:set({ label = { width = "dynamic" } })
+ end)
+ end)
+
+ spaces[spaceName]:subscribe("mouse.exited", function(env)
+ sbar.animate("tanh", 30, function()
+ spaces[spaceName]:set({ label = { width = 0 } })
+ end)
+ end)
+
+ sbar.add("item", spaceName .. ".padding", {
+ width = settings.dimens.padding.label
+ })
+end
+
+local function createWorkspaces()
+ {% for name, ws in window_manager.workspaces.items() | sort(attribute='1.order') %}
+ addWorkspaceItem("{{ name }}")
+ {% endfor %}
+
+ findAndSelectCurrentWorkspace()
+end
+
+swapWatcher:subscribe(constants.events.SWAP_MENU_AND_SPACES, function(env)
+ local isShowingSpaces = env.isShowingMenu == "off" and true or false
+ sbar.set("/" .. constants.items.SPACES .. "\\..*/", { drawing = isShowingSpaces })
+end)
+
+currentWorkspaceWatcher:subscribe(constants.events.AEROSPACE_WORKSPACE_CHANGED, function(env)
+ selectCurrentWorkspace(env.FOCUSED_WORKSPACE)
+ sbar.trigger(constants.events.UPDATE_WINDOWS)
+end)
+
+createWorkspaces()
diff --git a/dots/sketchybar/.config/sketchybar/items/widgets/battery.lua b/dots/sketchybar/.config/sketchybar/items/widgets/battery.lua
new file mode 100644
index 0000000..32546f0
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/widgets/battery.lua
@@ -0,0 +1,91 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local isCharging = false
+
+local battery = sbar.add("item", constants.items.battery, {
+ position = "right",
+ update_freq = 60,
+})
+
+local batteryPopup = sbar.add("item", {
+ position = "popup." .. battery.name,
+ width = "dynamic",
+ label = {
+ padding_right = settings.dimens.padding.label,
+ padding_left = settings.dimens.padding.label,
+ },
+ icon = {
+ padding_left = 0,
+ padding_right = 0,
+ },
+})
+
+battery:subscribe({ "routine", "power_source_change", "system_woke" }, function()
+ sbar.exec("pmset -g batt", function(batteryInfo)
+ local icon = "!"
+ local label = "?"
+
+ local found, _, charge = batteryInfo:find("(%d+)%%")
+ if found then
+ charge = tonumber(charge)
+ label = charge .. "%"
+ end
+
+ local color = settings.colors.green
+ local charging, _, _ = batteryInfo:find("AC Power")
+
+ isCharging = charging
+
+ if charging then
+ icon = settings.icons.text.battery.charging
+ else
+ if found and charge > 80 then
+ icon = settings.icons.text.battery._100
+ elseif found and charge > 60 then
+ icon = settings.icons.text.battery._75
+ elseif found and charge > 40 then
+ icon = settings.icons.text.battery._50
+ elseif found and charge > 30 then
+ icon = settings.icons.text.battery._50
+ color = settings.colors.yellow
+ elseif found and charge > 20 then
+ icon = settings.icons.text.battery._25
+ color = settings.colors.orange
+ else
+ icon = settings.icons.text.battery._0
+ color = settings.colors.red
+ end
+ end
+
+ local lead = ""
+ if found and charge < 10 then
+ lead = "0"
+ end
+
+ battery:set({
+ icon = {
+ string = icon,
+ color = color
+ },
+ label = {
+ string = lead .. label,
+ padding_left = 0,
+ },
+ })
+ end)
+end)
+
+battery:subscribe("mouse.clicked", function(env)
+ local drawing = battery:query().popup.drawing
+
+ battery:set({ popup = { drawing = "toggle" } })
+
+ if drawing == "off" then
+ sbar.exec("pmset -g batt", function(batteryInfo)
+ local found, _, remaining = batteryInfo:find("(%d+:%d+) remaining")
+ local label = found and ("Time remaining: " .. remaining .. "h") or (isCharging and "Charging" or "No estimate")
+ batteryPopup:set({ label = label })
+ end)
+ end
+end)
diff --git a/dots/sketchybar/.config/sketchybar/items/widgets/calendar.lua b/dots/sketchybar/.config/sketchybar/items/widgets/calendar.lua
new file mode 100644
index 0000000..436591d
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/widgets/calendar.lua
@@ -0,0 +1,17 @@
+local constants = require("constants")
+
+local calendar = sbar.add("item", constants.items.CALENDAR, {
+ position = "right",
+ update_freq = 1,
+ icon = { padding_left = 0, padding_right = 0 }
+})
+
+calendar:subscribe({ "forced", "routine", "system_woke" }, function(env)
+ calendar:set({
+ label = os.date("%a %d %b, %H:%M"),
+ })
+end)
+
+calendar:subscribe("mouse.clicked", function(env)
+ sbar.exec("open -a 'Calendar'")
+end)
diff --git a/dots/sketchybar/.config/sketchybar/items/widgets/init.lua b/dots/sketchybar/.config/sketchybar/items/widgets/init.lua
new file mode 100644
index 0000000..8e9631c
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/widgets/init.lua
@@ -0,0 +1,4 @@
+require("items.widgets.calendar")
+require("items.widgets.battery")
+require("items.widgets.volume")
+require("items.widgets.wifi")
diff --git a/dots/sketchybar/.config/sketchybar/items/widgets/volume.lua b/dots/sketchybar/.config/sketchybar/items/widgets/volume.lua
new file mode 100644
index 0000000..7a24847
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/widgets/volume.lua
@@ -0,0 +1,128 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local currentAudioDevice = "None"
+
+local volumeValue = sbar.add("item", constants.items.VOLUME .. ".value", {
+ position = "right",
+ label = {
+ string = "??%",
+ padding_left = 0,
+ },
+})
+
+local volumeBracket = sbar.add("bracket", constants.items.VOLUME .. ".bracket", { volumeValue.name }, {
+ popup = {
+ align = "center"
+ },
+})
+
+local volumeSlider = sbar.add("slider", constants.items.VOLUME .. ".slider", settings.dimens.graphics.popup.width, {
+ position = "popup." .. volumeBracket.name,
+ click_script = 'osascript -e "set volume output volume $PERCENTAGE"'
+})
+
+volumeValue:subscribe("volume_change", function(env)
+ local icon = settings.icons.text.volume._0
+ local volume = tonumber(env.INFO)
+
+ sbar.exec("SwitchAudioSource -t output -c", function(result)
+ -- local currentOutputDevice = result:sub(1, -2)
+ -- if currentOutputDevice == "AirPods Max" then
+ -- icon = "􀺹"
+ -- elseif currentOutputDevice == "AirPods von Longdong Silver" or currentOutputDevice == "AirPods von Anna" then
+ -- icon = "􀟥"
+ -- elseif currentOutputDevice == "Arctis Nova Pro Wireless" then
+ -- icon = "􀑈"
+ -- elseif currentOutputDevice == "Ear (2)" then
+ -- icon = "􀪷"
+ -- elseif currentOutputDevice == "iD4" then
+ -- icon = "􀝎"
+ -- else
+ if volume > 60 then
+ icon = settings.icons.text.volume._100
+ elseif volume > 30 then
+ icon = settings.icons.text.volume._66
+ elseif volume > 10 then
+ icon = settings.icons.text.volume._33
+ elseif volume > 0 then
+ icon = settings.icons.text.volume._10
+ end
+ -- end
+
+ local lead = ""
+ if volume < 10 then
+ lead = "0"
+ end
+
+ -- volumeIcon:set({ label = icon })
+ volumeSlider:set({ slider = { percentage = volume } })
+
+ local hasVolume = volume ~= 0
+ volumeValue:set({
+ icon = icon,
+ label = {
+ string = hasVolume and lead .. volume .. "%" or "",
+ padding_right = hasVolume and 8 or 0,
+ },
+ })
+ end)
+end)
+
+local function hideVolumeDetails()
+ local drawing = volumeBracket:query().popup.drawing == "on"
+ if not drawing then return end
+ volumeBracket:set({ popup = { drawing = false } })
+ sbar.remove("/" .. constants.items.VOLUME .. ".device\\.*/")
+end
+
+local function toggleVolumeDetails(env)
+ if env.BUTTON == "right" then
+ sbar.exec("open /System/Library/PreferencePanes/Sound.prefpane")
+ return
+ end
+
+ local shouldDraw = volumeBracket:query().popup.drawing == "off"
+ if shouldDraw then
+ volumeBracket:set({ popup = { drawing = true } })
+
+ sbar.exec("SwitchAudioSource -t output -c", function(result)
+ currentAudioDevice = result:sub(1, -2)
+
+ sbar.exec("SwitchAudioSource -a -t output", function(available)
+ local current = currentAudioDevice
+ local counter = 0
+
+ for device in string.gmatch(available, '[^\r\n]+') do
+ local color = settings.colors.grey
+ if current == device then
+ color = settings.colors.white
+ end
+
+ sbar.add("item", constants.items.VOLUME .. ".device." .. counter, {
+ position = "popup." .. volumeBracket.name,
+ align = "center",
+ label = { string = device, color = color },
+ click_script = 'SwitchAudioSource -s "' ..
+ device ..
+ '" && sketchybar --set /' .. constants.items.VOLUME .. '.device\\.*/ label.color=' ..
+ settings.colors.grey .. ' --set $NAME label.color=' .. settings.colors.white
+
+ })
+ counter = counter + 1
+ end
+ end)
+ end)
+ else
+ hideVolumeDetails()
+ end
+end
+
+local function changeVolume(env)
+ local delta = env.SCROLL_DELTA
+ sbar.exec('osascript -e "set volume output volume (output volume of (get volume settings) + ' .. delta .. ')"')
+end
+
+volumeValue:subscribe("mouse.clicked", toggleVolumeDetails)
+volumeValue:subscribe("mouse.scrolled", changeVolume)
+-- volumeValue:subscribe("mouse.exited.global", hideVolumeDetails)
diff --git a/dots/sketchybar/.config/sketchybar/items/widgets/wifi.lua b/dots/sketchybar/.config/sketchybar/items/widgets/wifi.lua
new file mode 100644
index 0000000..551a4da
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/items/widgets/wifi.lua
@@ -0,0 +1,261 @@
+local constants = require("constants")
+local settings = require("config.settings")
+
+local popupWidth <const> = settings.dimens.graphics.popup.width + 20
+
+sbar.exec(
+ "killall network_load >/dev/null; $CONFIG_DIR/bridge/network_load/bin/network_load en0 network_update 2.0"
+)
+
+local wifiUp = sbar.add("item", constants.items.WIFI .. ".up", {
+ position = "right",
+ width = 0,
+ icon = {
+ padding_left = 0,
+ padding_right = 0,
+ font = {
+ style = settings.fonts.styles.bold,
+ size = 10.0,
+ },
+ string = settings.icons.text.wifi.upload,
+ },
+ label = {
+ font = {
+ family = settings.fonts.numbers,
+ style = settings.fonts.styles.bold,
+ size = 10.0,
+ },
+ color = settings.colors.orange,
+ string = "??? Bps",
+ },
+ y_offset = 4,
+})
+
+local wifiDown = sbar.add("item", constants.items.WIFI .. ".down", {
+ position = "right",
+ icon = {
+ padding_left = 0,
+ padding_right = 0,
+ font = {
+ style = settings.fonts.styles.bold,
+ size = 10.0,
+ },
+ string = settings.icons.text.wifi.download,
+ },
+ label = {
+ font = {
+ family = settings.fonts.numbers,
+ style = settings.fonts.styles.bold,
+ size = 10,
+ },
+ color = settings.colors.blue,
+ string = "??? Bps",
+ },
+ y_offset = -4,
+})
+
+local wifi = sbar.add("item", constants.items.WIFI .. ".padding", {
+ position = "right",
+ label = { drawing = false },
+ padding_right = 0,
+})
+
+local wifiBracket = sbar.add("bracket", constants.items.WIFI .. ".bracket", {
+ wifi.name,
+ wifiUp.name,
+ wifiDown.name
+}, {
+ popup = { align = "center" }
+})
+
+local ssid = sbar.add("item", {
+ align = "center",
+ position = "popup." .. wifiBracket.name,
+ width = popupWidth,
+ height = 16,
+ icon = {
+ string = settings.icons.text.wifi.router,
+ font = {
+ style = settings.fonts.styles.bold
+ },
+ },
+ label = {
+ font = {
+ style = settings.fonts.styles.bold,
+ size = settings.dimens.text.label,
+ },
+ max_chars = 18,
+ string = "????????????",
+ },
+})
+
+local hostname = sbar.add("item", {
+ position = "popup." .. wifiBracket.name,
+ background = {
+ height = 16,
+ },
+ icon = {
+ align = "left",
+ string = "Hostname:",
+ width = popupWidth / 2,
+ font = {
+ size = settings.dimens.text.label
+ },
+ },
+ label = {
+ max_chars = 20,
+ string = "????????????",
+ width = popupWidth / 2,
+ align = "right",
+ }
+})
+
+local ip = sbar.add("item", {
+ position = "popup." .. wifiBracket.name,
+ background = {
+ height = 16,
+ },
+ icon = {
+ align = "left",
+ string = "IP:",
+ width = popupWidth / 2,
+ font = {
+ size = settings.dimens.text.label
+ },
+ },
+ label = {
+ align = "right",
+ string = "???.???.???.???",
+ width = popupWidth / 2,
+ }
+})
+
+local router = sbar.add("item", {
+ position = "popup." .. wifiBracket.name,
+ background = {
+ height = 16,
+ },
+ icon = {
+ align = "left",
+ string = "Router:",
+ width = popupWidth / 2,
+ font = {
+ size = settings.dimens.text.label
+ },
+ },
+ label = {
+ align = "right",
+ string = "???.???.???.???",
+ width = popupWidth / 2,
+ },
+})
+
+sbar.add("item", { position = "right", width = settings.dimens.padding.item })
+
+wifiUp:subscribe("network_update", function(env)
+ local upColor = (env.upload == "000 Bps") and settings.colors.grey or settings.colors.orange
+ local downColor = (env.download == "000 Bps") and settings.colors.grey or settings.colors.blue
+
+ wifiUp:set({
+ icon = { color = upColor },
+ label = {
+ string = env.upload,
+ color = upColor
+ }
+ })
+ wifiDown:set({
+ icon = { color = downColor },
+ label = {
+ string = env.download,
+ color = downColor
+ }
+ })
+end)
+
+wifi:subscribe({ "wifi_change", "system_woke", "forced" }, function(env)
+ wifi:set({
+ icon = {
+ string = settings.icons.text.wifi.disconnected,
+ color = settings.colors.magenta,
+ }
+ })
+
+ sbar.exec([[ipconfig getifaddr en0]], function(ip)
+ local ipConnected = not (ip == "")
+
+ local wifiIcon
+ local wifiColor
+
+ if ipConnected then
+ wifiIcon = settings.icons.text.wifi.connected
+ wifiColor = settings.colors.white
+ end
+
+ wifi:set({
+ icon = {
+ string = wifiIcon,
+ color = wifiColor,
+ }
+ })
+
+ sbar.exec([[sleep 2; scutil --nwi | grep -m1 'utun' | awk '{ print $1 }']], function(vpn)
+ local isVPNConnected = not (vpn == "")
+
+ if isVPNConnected then
+ wifiIcon = settings.icons.text.wifi.vpn
+ wifiColor = settings.colors.green
+ end
+
+ wifi:set({
+ icon = {
+ string = wifiIcon,
+ color = wifiColor,
+ }
+ })
+ end)
+ end)
+end)
+
+local function hideDetails()
+ wifiBracket:set({ popup = { drawing = false } })
+end
+
+local function toggleDetails()
+ local shouldDrawDetails = wifiBracket:query().popup.drawing == "off"
+
+ if shouldDrawDetails then
+ wifiBracket:set({ popup = { drawing = true } })
+ sbar.exec("networksetup -getcomputername", function(result)
+ hostname:set({ label = result })
+ end)
+ sbar.exec("ipconfig getifaddr en0", function(result)
+ ip:set({ label = result })
+ end)
+ sbar.exec("ipconfig getsummary en0 | awk -F ' SSID : ' '/ SSID : / {print $2}'", function(result)
+ ssid:set({ label = result })
+ end)
+ sbar.exec("networksetup -getinfo Wi-Fi | awk -F 'Router: ' '/^Router: / {print $2}'", function(result)
+ router:set({ label = result })
+ end)
+ else
+ hideDetails()
+ end
+end
+
+local function copyLabelToClipboard(env)
+ local label = sbar.query(env.NAME).label.value
+ sbar.exec("echo \"" .. label .. "\" | pbcopy")
+ sbar.set(env.NAME, { label = { string = settings.icons.text.clipboard, align = "center" } })
+ sbar.delay(1, function()
+ sbar.set(env.NAME, { label = { string = label, align = "right" } })
+ end)
+end
+
+wifiUp:subscribe("mouse.clicked", toggleDetails)
+wifiDown:subscribe("mouse.clicked", toggleDetails)
+wifi:subscribe("mouse.clicked", toggleDetails)
+
+ssid:subscribe("mouse.clicked", copyLabelToClipboard)
+hostname:subscribe("mouse.clicked", copyLabelToClipboard)
+ip:subscribe("mouse.clicked", copyLabelToClipboard)
+router:subscribe("mouse.clicked", copyLabelToClipboard)
diff --git a/dots/sketchybar/.config/sketchybar/sketchybarrc b/dots/sketchybar/.config/sketchybar/sketchybarrc
new file mode 100755
index 0000000..d9f6e17
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/sketchybarrc
@@ -0,0 +1,3 @@
+#!/usr/bin/env lua
+
+require("init")
diff --git a/dots/sketchybar/.config/sketchybar/util/.gitkeep b/dots/sketchybar/.config/sketchybar/util/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dots/sketchybar/.config/sketchybar/util/.gitkeep