Compare

27117202739f092d837af2752e4b9801c47b8ddb .. d2b62c9d6f30a0ea88b2a235cde120cd10ba332b · 1 commits

Changed files

Commits

HashSubjectAuthorDate
d2b62c9d Sketchybar now, got tired of the battery issue Ryan Schanzenbacher
diff --git a/hosts/RyanMac/configuration.nix b/hosts/RyanMac/configuration.nix
index f72c6aa..e5b38ca 100644
--- a/hosts/RyanMac/configuration.nix
+++ b/hosts/RyanMac/configuration.nix
@@ -45,7 +45,7 @@ in {
# Modules
imports = [
inputs.nix-homebrew.darwinModules.nix-homebrew
- ../../modules/darwin/random-wallpaper/module.nix
+ ../../modules/darwin/random-wallpaper
];
# Define the system's user and home dir location
diff --git a/modules/darwin/random-wallpaper/module.nix b/modules/darwin/random-wallpaper/default.nix
similarity index 100%
rename from modules/darwin/random-wallpaper/module.nix
rename to modules/darwin/random-wallpaper/default.nix
diff --git a/users/ryan/home.nix b/users/ryan/home.nix
index 2aaacb3..1c77723 100644
--- a/users/ryan/home.nix
+++ b/users/ryan/home.nix
@@ -15,9 +15,7 @@
# My own modules
ryan.neovim.enable = true;
- ryan.simple-bar.enable = pkgs.stdenv.isDarwin;
- ryan.taskwarrior-widget.enable = pkgs.stdenv.isDarwin;
- ryan.ubersicht.enable = pkgs.stdenv.isDarwin;
+ ryan.sketchybar.enable = true;
programs.starship = {
enable = true;
@@ -148,6 +146,7 @@
audacity
ffmpeg
blender
+ nerd-fonts.anonymice
];
# Home Manager is pretty good at managing dotfiles. The primary way to manage
diff --git a/users/ryan/modules/sketchybar/bar-config/README.md b/users/ryan/modules/sketchybar/bar-config/README.md
new file mode 100644
index 0000000..7b1bf97
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/README.md
@@ -0,0 +1,63 @@
+# SketchyBar config (translated from Waybar)
+
+This is a translation of a Hyprland/Waybar setup (`config` + `style.css`) into an
+equivalent [SketchyBar](https://github.com/FelixKratz/SketchyBar) config for macOS.
+Layout, fonts, and colours mirror the originals as closely as macOS allows.
+
+## Install
+
+```sh
+# 1. SketchyBar + the font your CSS references
+brew tap FelixKratz/formulae
+brew install sketchybar
+brew install --cask font-anonymice-nerd-font # "AnonymicePro Nerd Font"
+
+# 2. Drop these files in place
+cp -r .config/sketchybar ~/.config/ # (or move this folder there)
+chmod +x ~/.config/sketchybar/sketchybarrc ~/.config/sketchybar/plugins/*.sh
+
+# 3. Start it
+brew services start sketchybar
+```
+
+## Optional dependencies (items self-hide / degrade gracefully without them)
+
+| Item | Needs | Install |
+|-------------|--------------------------------|---------------------------------|
+| workspaces | yabai (tiling WM) for `space` | `brew install koekeishiya/formulae/yabai` |
+| bluetooth | `blueutil` | `brew install blueutil` |
+| brightness | `brightness` CLI | `brew install brightness` |
+| weather | `curl` (built in) | — |
+
+> **Spaces / yabai:** the `space` component needs yabai, which needs SIP partially
+> disabled. If you'd rather not touch SIP, [AeroSpace](https://github.com/nikitabobko/AeroSpace)
+> is a SIP-free tiling WM — swap the `space` loop for its `aerospace_workspace_change`
+> event and point `click_script` at `aerospace workspace $sid`.
+
+## Module mapping
+
+| Waybar | SketchyBar | Notes |
+|-----------------------|-----------------------------------|-------|
+| hyprland/workspaces | `space` items (yabai) | number icon, dim→white for active (Waybar's 3px top-border → `icon.highlight`) |
+| hyprland/window | `front_app` | shows app name; window *title* needs a yabai query (noted in plugin) |
+| clock | `clock` | same `%a %d %b %H:%M` format |
+| battery (BAT0) | `battery` | `pmset`; same icons, warning@25 / critical@10, charging bolt |
+| bluetooth | `bluetooth` | `blueutil`; on/off/connected glyphs, click toggles power |
+| network | `network` | ip + wifi/ethernet/off glyph (cidr & RSSI dropped — not exposed on macOS) |
+| pulseaudio | `volume` | native `volume_change`; volume% + icon, mute glyph |
+| backlight | `brightness` | `brightness` CLI; scroll to adjust (flaky on Apple Silicon) |
+| custom/storage | `storage` | `df /`; warning@80 / critical@90 |
+| custom/weather | `weather` | `wttr.in` in °F, click opens wttr.in |
+| custom/mail | `mail` | Mail.app unread count (swap for your provider) |
+| custom/spotify | `spotify` | native `media_change`; title–artist, click play/pause |
+| idle_inhibitor | `caffeinate` | click toggles `caffeinate`; lock/unlock glyphs |
+
+## Omitted (no macOS/SketchyBar equivalent)
+
+- **tray** — the macOS menu bar already renders status/menu-extra icons.
+- **privacy** (screenshare/mic) — macOS shows camera/mic indicators natively.
+- **hyprland/submap** — yabai has no submap concept.
+- **battery#num2** (BAT1) — Macs have a single battery.
+
+Colours in `colors.sh` and glyphs in `icons.sh` are taken verbatim from your
+`style.css` and `config` (glyphs are byte-for-byte identical).
diff --git a/users/ryan/modules/sketchybar/bar-config/colors.sh b/users/ryan/modules/sketchybar/bar-config/colors.sh
new file mode 100644
index 0000000..1ef1264
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/colors.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# Colors translated from style.css (0xAARRGGBB)
+
+export WHITE=0xffffffff
+export TEXT=0xffd9d8d8 # rgba(217,216,216,1) default text / visible+active workspace
+export TEXT_DIM=0x66d9d8d8 # rgba(217,216,216,0.4) inactive workspace
+export RED=0xffee2e24 # rgba(238,46,36,1) urgent / battery+storage critical
+export YELLOW=0xffffd204 # rgba(255,210,4,1) battery+storage warning
+export BAR_COLOR=0x80000000 # rgba(0,0,0,0.25) #waybar background
+export POPUP_BG=0xd9231f20 # rgba(35,31,32,0.85) window#waybar.solo background
+export TRANSPARENT=0x00000000
diff --git a/users/ryan/modules/sketchybar/bar-config/icons.sh b/users/ryan/modules/sketchybar/bar-config/icons.sh
new file mode 100644
index 0000000..39de9cd
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/icons.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# Nerd Font glyphs, byte-for-byte identical to the original Waybar config.
+# Font: AnonymicePro Nerd Font
+
+# battery format-icons (low -> high) + charging
+export BAT_0="󰁺"; export BAT_1="󰁻"; export BAT_2="󰁾"
+export BAT_3="󰂀"; export BAT_4="󰁹"; export BAT_CHARGING="󰂄"
+
+# bluetooth
+export BT_ON="󰂯"; export BT_OFF="󰂲"; export BT_CONNECTED="󰂱"
+
+# network (wifi weak -> strong, ethernet, disconnected)
+export WIFI_1="󰤟"; export WIFI_2="󰤢"; export WIFI_3="󰤥"
+export WIFI_4="󰤨"; export ETHERNET=""; export NET_OFF="󰤭"
+
+# pulseaudio / volume (default set) + muted
+export VOL_0=""; export VOL_1=""; export VOL_2=""
+export VOL_3=""; export VOL_MUTE=""
+
+# backlight / brightness (dim, bright)
+export BL_LOW=""; export BL_HIGH=""
+
+# idle_inhibitor (activated=lock, deactivated=unlock)
+export IDLE_ON=""; export IDLE_OFF=""
+
+# misc
+export DISK=""; export MAIL=""; export SPOTIFY=""
+
+# weather (nf-weather-*, wttr.in weatherCode -> glyph)
+export WEA_SUNNY=""; export WEA_NIGHT=""
+export WEA_DAY_CLOUDY=""; export WEA_NIGHT_CLOUDY=""
+export WEA_CLOUDY=""; export WEA_FOG=""
+export WEA_SHOWERS=""; export WEA_RAIN=""
+export WEA_SNOW=""; export WEA_SLEET=""
+export WEA_THUNDER=""
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/battery.sh b/users/ryan/modules/sketchybar/bar-config/plugins/battery.sh
new file mode 100755
index 0000000..3d01d63
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/battery.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+CACHE="/tmp/sketchybar_${NAME}.txt"
+case "$SENDER" in
+ mouse.entered)
+ [ -f "$CACHE" ] && sketchybar --set "$NAME.details" label="$(cat "$CACHE")"
+ sketchybar --set "$NAME" popup.drawing=on; exit 0 ;;
+ mouse.exited)
+ sketchybar --set "$NAME" popup.drawing=off; exit 0 ;;
+esac
+
+RAW="$(pmset -g batt)"
+PCT="$(printf '%s' "$RAW" | grep -Eo '[0-9]+%' | head -1 | tr -d '%')"
+CHARGING="$(printf '%s' "$RAW" | grep -c 'AC Power')"
+[ -z "$PCT" ] && exit 0
+
+if [ "$PCT" -ge 80 ]; then ICON=$BAT_4
+elif [ "$PCT" -ge 60 ]; then ICON=$BAT_3
+elif [ "$PCT" -ge 40 ]; then ICON=$BAT_2
+elif [ "$PCT" -ge 20 ]; then ICON=$BAT_1
+else ICON=$BAT_0
+fi
+
+COLOR=$TEXT
+if [ "$CHARGING" -ge 1 ]; then ICON=$BAT_CHARGING; COLOR=$TEXT
+elif [ "$PCT" -le 10 ]; then COLOR=$RED
+elif [ "$PCT" -le 25 ]; then COLOR=$YELLOW
+fi
+
+sketchybar --set "$NAME" label="${PCT}% ${ICON}" label.color=$COLOR
+
+# popup: state + time remaining (Waybar battery format-alt "{time}")
+STATE="$(printf '%s' "$RAW" | grep -Eo 'discharging|charging|charged|AC attached|finishing charge' | head -1)"
+TIME="$(printf '%s' "$RAW" | grep -Eo '[0-9]+:[0-9]+' | head -1)"
+LINE="${PCT}%"
+[ -n "$STATE" ] && LINE="$LINE · $STATE"
+if [ -n "$TIME" ] && [ "$TIME" != "0:00" ]; then
+ if [ "$CHARGING" -ge 1 ]; then LINE="$LINE · $TIME to full"; else LINE="$LINE · $TIME remaining"; fi
+fi
+printf '%s' "$LINE" > "$CACHE"
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/bluetooth.sh b/users/ryan/modules/sketchybar/bar-config/plugins/bluetooth.sh
new file mode 100755
index 0000000..80185c0
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/bluetooth.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+CACHE="/tmp/sketchybar_${NAME}.txt"
+case "$SENDER" in
+ mouse.entered)
+ [ -f "$CACHE" ] && sketchybar --set "$NAME.details" label="$(cat "$CACHE")"
+ sketchybar --set "$NAME" popup.drawing=on; exit 0 ;;
+ mouse.exited)
+ sketchybar --set "$NAME" popup.drawing=off; exit 0 ;;
+esac
+
+if ! command -v blueutil >/dev/null 2>&1; then
+ sketchybar --set "$NAME" label="$BT_OFF" label.color=$TEXT_DIM
+ printf '%s' "blueutil not installed" > "$CACHE"
+ exit 0
+fi
+
+if [ "$1" = "toggle" ]; then
+ if [ "$(blueutil -p)" = "1" ]; then blueutil -p 0; else blueutil -p 1; fi
+fi
+
+if [ "$(blueutil -p)" = "1" ]; then
+ NAMES="$(blueutil --connected 2>/dev/null | grep -o 'name: "[^"]*"' | sed 's/name: "//; s/"$//' | paste -sd , - | sed 's/,/, /g')"
+ COUNT="$(blueutil --connected 2>/dev/null | grep -c 'name:')"
+ if [ "$COUNT" -gt 0 ]; then
+ sketchybar --set "$NAME" label="$BT_CONNECTED" label.color=$TEXT
+ printf '%s' "Bluetooth on · $COUNT connected: $NAMES" > "$CACHE"
+ else
+ sketchybar --set "$NAME" label="$BT_ON" label.color=$TEXT
+ printf '%s' "Bluetooth on · no devices connected" > "$CACHE"
+ fi
+else
+ sketchybar --set "$NAME" label="$BT_OFF" label.color=$TEXT_DIM
+ printf '%s' "Bluetooth off" > "$CACHE"
+fi
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/brightness.sh b/users/ryan/modules/sketchybar/bar-config/plugins/brightness.sh
new file mode 100755
index 0000000..87a245f
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/brightness.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# backlight -> brightness. Requires the `brightness` CLI (brew install brightness).
+# NOTE: reading/writing internal-display brightness is unreliable on Apple Silicon;
+# the item hides itself if the tool or a readable value is unavailable.
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+command -v brightness >/dev/null 2>&1 || { sketchybar --set "$NAME" drawing=off; exit 0; }
+
+read_val() { brightness -l 2>/dev/null | awk '/display 0: brightness/{print $NF}'; }
+
+if [ "$SENDER" = "mouse.scrolled" ] && command -v bc >/dev/null 2>&1; then
+ CUR="$(read_val)"
+ if [ -n "$CUR" ]; then
+ case "$INFO" in
+ -*) brightness "$(echo "$CUR - 0.05" | bc)" 2>/dev/null ;; # scroll down
+ *) brightness "$(echo "$CUR + 0.05" | bc)" 2>/dev/null ;; # scroll up
+ esac
+ fi
+fi
+
+CUR="$(read_val)"
+[ -z "$CUR" ] && { sketchybar --set "$NAME" drawing=off; exit 0; }
+PCT="$(printf '%.0f' "$(echo "$CUR * 100" | bc -l 2>/dev/null)" 2>/dev/null)"
+[ -z "$PCT" ] && PCT=0
+
+if [ "$PCT" -ge 50 ]; then ICON=$BL_HIGH; else ICON=$BL_LOW; fi
+# Waybar: {percent}% {icon}
+sketchybar --set "$NAME" drawing=on label="${PCT}% ${ICON}"
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/caffeinate.sh b/users/ryan/modules/sketchybar/bar-config/plugins/caffeinate.sh
new file mode 100755
index 0000000..ce4c4d2
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/caffeinate.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# idle_inhibitor -> caffeinate (keeps display + system awake while "activated")
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+PIDFILE="/tmp/sketchybar_caffeinate.pid"
+is_on() { [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; }
+
+if [ "$1" = "toggle" ]; then
+ if is_on; then
+ kill "$(cat "$PIDFILE")" 2>/dev/null; rm -f "$PIDFILE"
+ else
+ caffeinate -d -i -s & echo $! > "$PIDFILE"
+ fi
+fi
+
+if is_on; then
+ sketchybar --set "$NAME" label="$IDLE_ON" label.color=$TEXT # activated
+else
+ sketchybar --set "$NAME" label="$IDLE_OFF" label.color=$TEXT_DIM # deactivated
+fi
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/clock.sh b/users/ryan/modules/sketchybar/bar-config/plugins/clock.sh
new file mode 100755
index 0000000..e78ee89
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/clock.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+# Waybar: {:%a %d %b %H:%M:%S}
+sketchybar --set "$NAME" label="$(date '+%a %d %b %H:%M:%S')"
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/front_app.sh b/users/ryan/modules/sketchybar/bar-config/plugins/front_app.sh
new file mode 100755
index 0000000..c5d8ed0
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/front_app.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+# hyprland/window -> focused application name.
+# (For the literal window *title* instead of the app name, query yabai:
+# yabai -m query --windows --window | jq -r '.title')
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+if [ "$SENDER" = "front_app_switched" ]; then
+ sketchybar --set "$NAME" label="$INFO"
+fi
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/mail.sh b/users/ryan/modules/sketchybar/bar-config/plugins/mail.sh
new file mode 100755
index 0000000..7ce2fe6
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/mail.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# Waybar used a custom mail.py; here we read Mail.app's unread inbox count.
+# (First run may prompt for Automation permission. Swap this for your provider.)
+source "$HOME/.config/sketchybar/icons.sh"
+
+UNREAD="$(osascript -e 'tell application "Mail" to get unread count of inbox' 2>/dev/null)"
+
+if [ -n "$UNREAD" ] && [ "$UNREAD" -gt 0 ] 2>/dev/null; then
+ sketchybar --set "$NAME" label="$MAIL $UNREAD"
+else
+ sketchybar --set "$NAME" label="$MAIL"
+fi
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/network.sh b/users/ryan/modules/sketchybar/bar-config/plugins/network.sh
new file mode 100755
index 0000000..b6b003a
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/network.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+CACHE="/tmp/sketchybar_${NAME}.txt"
+case "$SENDER" in
+ mouse.entered)
+ [ -f "$CACHE" ] && sketchybar --set "$NAME.details" label="$(cat "$CACHE")"
+ sketchybar --set "$NAME" popup.drawing=on; exit 0 ;;
+ mouse.exited)
+ sketchybar --set "$NAME" popup.drawing=off; exit 0 ;;
+esac
+
+SERVICE="$(route -n get default 2>/dev/null | awk '/interface:/{print $2}')"
+IP="$(ipconfig getifaddr "$SERVICE" 2>/dev/null)"
+
+if [ -z "$IP" ]; then
+ sketchybar --set "$NAME" label="$NET_OFF"
+ printf '%s' "Disconnected" > "$CACHE"
+ exit 0
+fi
+
+WIFI_DEV="$(networksetup -listallhardwareports 2>/dev/null | awk '/Wi-Fi/{getline; print $2}')"
+if [ "$SERVICE" = "$WIFI_DEV" ]; then ICON=$WIFI_4; TYPE="Wi-Fi"; else ICON=$ETHERNET; TYPE="Ethernet"; fi
+
+sketchybar --set "$NAME" label="$IP $ICON"
+
+# ---- popup details: interface, ip/cidr, gateway, SSID ----
+MASK="$(ifconfig "$SERVICE" 2>/dev/null | awk '/inet /{print $4; exit}')"
+CIDR=""
+if [ -n "$MASK" ]; then
+ n=$(( MASK )); c=0
+ while [ "$n" -gt 0 ]; do c=$(( c + (n & 1) )); n=$(( n >> 1 )); done
+ CIDR="$c"
+fi
+GW="$(route -n get default 2>/dev/null | awk '/gateway:/{print $2}')"
+SSID=""
+if [ "$TYPE" = "Wi-Fi" ]; then
+ SSID="$(networksetup -getairportnetwork "$SERVICE" 2>/dev/null | sed -n 's/^Current Wi-Fi Network: //p')"
+fi
+
+LINE="$SERVICE · $TYPE · ${IP}${CIDR:+/$CIDR}"
+[ -n "$GW" ] && LINE="$LINE · gw $GW"
+[ -n "$SSID" ] && LINE="$LINE · $SSID"
+printf '%s' "$LINE" > "$CACHE"
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/space.sh b/users/ryan/modules/sketchybar/bar-config/plugins/space.sh
new file mode 100755
index 0000000..f15ecca
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/space.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# hyprland/workspaces active/visible -> highlighted (white) icon.
+# Waybar's 3px white top-border on the active button maps to icon.highlight here.
+if [ "$SELECTED" = "true" ]; then
+ sketchybar --set "$NAME" icon.highlight=on
+else
+ sketchybar --set "$NAME" icon.highlight=off
+fi
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/spotify.sh b/users/ryan/modules/sketchybar/bar-config/plugins/spotify.sh
new file mode 100755
index 0000000..8d91297
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/spotify.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# custom/spotify -> now playing, driven by the native media_change event.
+source "$HOME/.config/sketchybar/icons.sh"
+
+# Waybar on-click: playerctl play-pause
+if [ "$1" = "toggle" ]; then
+ osascript -e 'tell application "Spotify" to playpause' 2>/dev/null
+ exit 0
+fi
+
+STATE="$(printf '%s' "$INFO" | sed -nE 's/.*"state":"([^"]*)".*/\1/p')"
+TITLE="$(printf '%s' "$INFO" | sed -nE 's/.*"title":"([^"]*)".*/\1/p')"
+ARTIST="$(printf '%s' "$INFO" | sed -nE 's/.*"artist":"([^"]*)".*/\1/p')"
+
+# exec-if check-for-music -> only draw while something is playing
+if [ "$STATE" = "playing" ] && [ -n "$TITLE" ]; then
+ sketchybar --set "$NAME" drawing=on label="$TITLE - $ARTIST"
+else
+ sketchybar --set "$NAME" drawing=off
+fi
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/storage.sh b/users/ryan/modules/sketchybar/bar-config/plugins/storage.sh
new file mode 100755
index 0000000..b844bb5
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/storage.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+CACHE="/tmp/sketchybar_${NAME}.txt"
+case "$SENDER" in
+ mouse.entered)
+ [ -f "$CACHE" ] && sketchybar --set "$NAME.details" label="$(cat "$CACHE")"
+ sketchybar --set "$NAME" popup.drawing=on; exit 0 ;;
+ mouse.exited)
+ sketchybar --set "$NAME" popup.drawing=off; exit 0 ;;
+esac
+
+# On APFS the real user usage lives on the Data volume; fall back to /
+VOL="/System/Volumes/Data"; [ -d "$VOL" ] || VOL="/"
+
+DF="$(df -h "$VOL" | awk 'NR==2{gsub("%","",$5); print $2, $3, $4, $5}')"
+set -- $DF; SIZE="$1"; USED="$2"; AVAIL="$3"; PCT="$4"
+[ -z "$PCT" ] && exit 0
+
+COLOR=$TEXT
+if [ "$PCT" -ge 90 ]; then COLOR=$RED # custom-storage.critical
+elif [ "$PCT" -ge 80 ]; then COLOR=$YELLOW # custom-storage.warning
+fi
+
+sketchybar --set "$NAME" label="${PCT}% ${DISK}" label.color=$COLOR
+printf '%s' "Disk · ${USED} used of ${SIZE} (${PCT}%) · ${AVAIL} free" > "$CACHE"
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/volume.sh b/users/ryan/modules/sketchybar/bar-config/plugins/volume.sh
new file mode 100755
index 0000000..667d0e8
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/volume.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+STEP=5 # % per scroll tick (Waybar used scroll-step: 1 — bump this down to taste)
+
+if [ "$SENDER" = "mouse.scrolled" ]; then
+ CUR="$(osascript -e 'output volume of (get volume settings)')"
+ case "$SCROLL_DELTA" in
+ -*) VOL=$(( CUR - STEP )) ;; # scroll down
+ *) VOL=$(( CUR + STEP )) ;; # scroll up
+ esac
+ [ "$VOL" -lt 0 ] && VOL=0
+ [ "$VOL" -gt 100 ] && VOL=100
+ osascript -e "set volume output volume $VOL"
+elif [ "$SENDER" = "volume_change" ]; then
+ VOL="$INFO"
+else
+ VOL="$(osascript -e 'output volume of (get volume settings)')"
+fi
+
+MUTED="$(osascript -e 'output muted of (get volume settings)')"
+
+if [ "$MUTED" = "true" ] || { [ "$VOL" -eq 0 ] 2>/dev/null; }; then
+ sketchybar --set "$NAME" label="$VOL_MUTE" # format-muted
+ exit 0
+fi
+
+if [ "$VOL" -ge 66 ]; then ICON=$VOL_3
+elif [ "$VOL" -ge 33 ]; then ICON=$VOL_2
+elif [ "$VOL" -ge 1 ]; then ICON=$VOL_1
+else ICON=$VOL_0
+fi
+
+# Waybar: {volume} {icon}
+sketchybar --set "$NAME" label="$VOL $ICON"
diff --git a/users/ryan/modules/sketchybar/bar-config/plugins/weather.sh b/users/ryan/modules/sketchybar/bar-config/plugins/weather.sh
new file mode 100755
index 0000000..abf8103
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/plugins/weather.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# custom/weather -> condition icon + temp, with a hover popup ("tooltip").
+# Uses wttr.in JSON (format=j2, the smaller no-hourly variant). Needs jq + curl.
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+CACHE="/tmp/sketchybar_weather.txt"
+
+# --- hover: show/hide the popup, populated from the cached line ---
+case "$SENDER" in
+ mouse.entered)
+ [ -f "$CACHE" ] && sketchybar --set weather.details label="$(cat "$CACHE")"
+ sketchybar --set "$NAME" popup.drawing=on
+ exit 0 ;;
+ mouse.exited)
+ sketchybar --set "$NAME" popup.drawing=off
+ exit 0 ;;
+esac
+
+command -v jq >/dev/null 2>&1 || { sketchybar --set "$NAME" label="wttr?"; exit 0; }
+
+JSON="$(curl -sf --max-time 6 'wttr.in/?format=j2')" \
+ || JSON="$(curl -sf --max-time 6 'wttr.in/?format=j1')" \
+ || { sketchybar --set "$NAME" label="--"; exit 0; }
+
+cc() { printf '%s' "$JSON" | jq -r ".current_condition[0].$1 // empty"; }
+CODE="$(cc weatherCode)"; TEMP="$(cc temp_F)"; FEELS="$(cc FeelsLikeF)"
+HUM="$(cc humidity)"; WSPD="$(cc windspeedMiles)"; WDIR="$(cc winddir16Point)"
+DESC="$(printf '%s' "$JSON" | jq -r '.current_condition[0].weatherDesc[0].value // empty')"
+AREA="$(printf '%s' "$JSON" | jq -r '.nearest_area[0].areaName[0].value // empty')"
+
+# daytime? simple local-hour heuristic (06:00–18:59) for clear/partly-cloudy variants
+HOUR=$(date +%H); HOUR=${HOUR#0}; : "${HOUR:=0}"
+if [ "$HOUR" -ge 6 ] && [ "$HOUR" -lt 19 ]; then DAY=1; else DAY=0; fi
+
+# WWO weatherCode -> nf-weather glyph
+case "$CODE" in
+ 113) [ "$DAY" = 1 ] && ICON="$WEA_SUNNY" || ICON="$WEA_NIGHT" ;;
+ 116) [ "$DAY" = 1 ] && ICON="$WEA_DAY_CLOUDY" || ICON="$WEA_NIGHT_CLOUDY" ;;
+ 119|122) ICON="$WEA_CLOUDY" ;;
+ 143|248|260) ICON="$WEA_FOG" ;;
+ 200|386|389|392|395) ICON="$WEA_THUNDER" ;;
+ 176|263|266|293|296|353) ICON="$WEA_SHOWERS" ;;
+ 299|302|305|308|356|359) ICON="$WEA_RAIN" ;;
+ 179|227|230|323|326|329|332|335|338|368|371) ICON="$WEA_SNOW" ;;
+ 182|185|281|284|311|314|317|320|350|362|365|374|377) ICON="$WEA_SLEET" ;;
+ *) ICON="$WEA_CLOUDY" ;;
+esac
+
+[ -z "$TEMP" ] && exit 0
+
+# bar: <icon> <temp>°F (drop " ${TEMP}°F" if you want the icon only)
+sketchybar --set "$NAME" label="$ICON ${TEMP}°F"
+
+# cache the single-line hover tooltip
+printf '%s' "${AREA:+$AREA · }${DESC} · feels ${FEELS}°F · ${HUM}% humidity · ${WDIR} ${WSPD}mph" > "$CACHE"
diff --git a/users/ryan/modules/sketchybar/bar-config/sketchybarrc b/users/ryan/modules/sketchybar/bar-config/sketchybarrc
new file mode 100755
index 0000000..446da25
--- /dev/null
+++ b/users/ryan/modules/sketchybar/bar-config/sketchybarrc
@@ -0,0 +1,172 @@
+#!/usr/bin/env bash
+# ============================================================================
+# sketchybarrc — translated from a Hyprland/Waybar config
+# Layout, fonts and colours mirror the original Waybar config + style.css.
+# ============================================================================
+CONFIG_DIR="$HOME/.config/sketchybar"
+PLUGIN_DIR="$CONFIG_DIR/plugins"
+source "$CONFIG_DIR/colors.sh"
+source "$CONFIG_DIR/icons.sh"
+
+env
+
+FONT="AnonymicePro Nerd Font"
+
+# ---- bar --------------------------------------------------------------------
+# From #waybar { background: rgba(0,0,0,0.25) } and Waybar height:30, position:top
+sketchybar --bar \
+ height=32 \
+ position=top \
+ color=$BAR_COLOR \
+ padding_left=8 \
+ padding_right=8 \
+ corner_radius=0 \
+ border_width=0 \
+ shadow=off
+
+# ---- defaults ---------------------------------------------------------------
+# From * { color:white; font: AnonymicePro Nerd Font 15px; no radius/shadow }
+sketchybar --default \
+ updates=when_shown \
+ icon.font="$FONT:Regular:15.0" \
+ icon.color=$TEXT \
+ icon.padding_left=4 \
+ icon.padding_right=4 \
+ label.font="$FONT:Regular:15.0" \
+ label.color=$TEXT \
+ label.padding_left=4 \
+ label.padding_right=4 \
+ background.corner_radius=0 \
+ background.height=32
+
+# ---- popup helper -----------------------------------------------------------
+# add_popup <item>: attaches a hover "tooltip" popup + a <item>.details child
+# (styled with POPUP_BG from window#waybar.solo) and subscribes to hover events.
+add_popup() {
+ sketchybar --set "$1" \
+ popup.background.color=$POPUP_BG \
+ popup.background.corner_radius=0 \
+ popup.background.border_width=0 \
+ popup.horizontal=off \
+ popup.y_offset=-1 \
+ --subscribe "$1" mouse.entered mouse.exited
+ sketchybar --add item "$1.details" "popup.$1" \
+ --set "$1.details" icon.drawing=off \
+ label.font="$FONT:Regular:13.0" \
+ label.padding_left=12 label.padding_right=12 \
+ background.height=24
+}
+
+# ============================================================================
+# LEFT — hyprland/workspaces -> yabai spaces
+# (hyprland/submap has no yabai/macOS analog -> omitted)
+# ============================================================================
+for sid in 1 2 3 4 5 6 7 8 9 10; do
+ sketchybar --add space space.$sid left \
+ --set space.$sid \
+ space=$sid \
+ icon=$sid \
+ icon.color=$TEXT_DIM \
+ icon.highlight_color=$TEXT \
+ label.drawing=off \
+ background.drawing=off \
+ padding_left=5 padding_right=5 \
+ script="$PLUGIN_DIR/space.sh" \
+ click_script="yabai -m space --focus $sid 2>/dev/null"
+done
+
+# ============================================================================
+# CENTER — hyprland/window -> front_app
+# ============================================================================
+sketchybar --add item front_app center \
+ --set front_app \
+ icon.drawing=off \
+ label.max_chars=80 \
+ label.padding_left=10 label.padding_right=10 \
+ script="$PLUGIN_DIR/front_app.sh" \
+ --subscribe front_app front_app_switched
+
+# ============================================================================
+# RIGHT — added right-to-left so the visual order matches Waybar's list:
+# spotify weather mail storage brightness bluetooth volume network
+# caffeinate battery clock
+# ============================================================================
+
+# clock (rightmost) — Waybar {:%a %d %b %H:%M}
+sketchybar --add item clock right \
+ --set clock update_freq=1 icon.drawing=off \
+ label.padding_left=10 label.padding_right=16 \
+ script="$PLUGIN_DIR/clock.sh"
+
+# battery (BAT0). Waybar battery#num2 (BAT1) omitted — Macs have one battery.
+sketchybar --add item battery right \
+ --set battery update_freq=60 icon.drawing=off \
+ script="$PLUGIN_DIR/battery.sh" \
+ --subscribe battery power_source_change system_woke
+add_popup battery
+
+# idle_inhibitor -> caffeinate
+sketchybar --add item caffeinate right \
+ --set caffeinate update_freq=15 \
+ script="$PLUGIN_DIR/caffeinate.sh" \
+ click_script="$PLUGIN_DIR/caffeinate.sh toggle"
+
+# network
+sketchybar --add item network right \
+ --set network update_freq=5 icon.drawing=off \
+ click_script="open -a 'System Settings' 2>/dev/null" \
+ script="$PLUGIN_DIR/network.sh"
+add_popup network
+
+# pulseaudio -> volume (native volume_change event)
+sketchybar --add item volume right \
+ --set volume icon.drawing=off script="$PLUGIN_DIR/volume.sh" \
+ --subscribe volume volume_change mouse.scrolled
+
+# bluetooth (needs blueutil)
+sketchybar --add item bluetooth right \
+ --set bluetooth update_freq=30 icon.drawing=off \
+ click_script="$PLUGIN_DIR/bluetooth.sh toggle" \
+ script="$PLUGIN_DIR/bluetooth.sh"
+add_popup bluetooth
+
+# backlight -> brightness (needs the `brightness` CLI; hides itself if absent)
+sketchybar --add item brightness right \
+ --set brightness update_freq=10 icon.drawing=off \
+ script="$PLUGIN_DIR/brightness.sh" \
+ --subscribe brightness mouse.scrolled
+
+# custom/storage
+sketchybar --add item storage right \
+ --set storage update_freq=60 icon.drawing=off \
+ script="$PLUGIN_DIR/storage.sh"
+add_popup storage
+
+# custom/mail (Mail.app unread)
+# sketchybar --add item mail right \
+# --set mail update_freq=60 icon.drawing=off \
+# click_script="open -a Mail" \
+# script="$PLUGIN_DIR/mail.sh"
+
+# custom/weather (wttr.in j2 -> condition icon + temp, hover popup as a "tooltip")
+sketchybar --add item weather right \
+ --set weather update_freq=1800 icon.drawing=off \
+ click_script="open https://wttr.in" \
+ script="$PLUGIN_DIR/weather.sh"
+add_popup weather
+
+# custom/spotify (now playing via native media_change)
+# sketchybar --add item spotify right \
+# --set spotify icon=$SPOTIFY icon.color=$TEXT drawing=off \
+# label.max_chars=30 scroll_texts=on \
+# click_script="$PLUGIN_DIR/spotify.sh toggle" \
+# script="$PLUGIN_DIR/spotify.sh" \
+# --subscribe spotify media_change
+
+# ----------------------------------------------------------------------------
+# Omitted (no SketchyBar / macOS analog):
+# tray -> the macOS menu bar already renders status/menu-extra items
+# privacy -> macOS shows camera/mic indicators natively in the menu bar
+# ----------------------------------------------------------------------------
+
+sketchybar --update
diff --git a/users/ryan/modules/sketchybar/default.nix b/users/ryan/modules/sketchybar/default.nix
new file mode 100644
index 0000000..8179f18
--- /dev/null
+++ b/users/ryan/modules/sketchybar/default.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.ryan.sketchybar;
+in
+{
+ options.ryan.sketchybar.enable = mkEnableOption "sketchybar";
+
+ config = mkIf cfg.enable {
+ # Enable the service
+ programs.sketchybar = {
+ enable = true;
+ extraPackages = [ pkgs.jq ];
+ config = { source = ./bar-config; recursive = true; };
+ };
+ };
+}