diff --git a/.gitignore b/.gitignore index 1832962..e8fa921 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tmux-client-*.log .opencode/package-lock.json .opencode/bun.lock .opencode/opencode-loop/ +/sketchybar-plan.html diff --git a/README.md b/README.md index 989f713..0268711 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,101 @@ # Collie's Dotfiles -[![CI](https://github.com/CollieIsCute/dotfiles/actions/workflows/test-distros.yaml/badge.svg?branch=main)](https://github.com/CollieIsCute/dotfiles/actions/workflows/test-distros.yaml) -![macOS](https://img.shields.io/badge/macOS-Homebrew-000?logo=apple&logoColor=white) -![Arch](https://img.shields.io/badge/Arch-pacman%20%2B%20paru-1793D1?logo=archlinux&logoColor=white) -![Ubuntu](https://img.shields.io/badge/Ubuntu-apt-E95420?logo=ubuntu&logoColor=white) -![Debian](https://img.shields.io/badge/Debian-apt-A81D33?logo=debian&logoColor=white) -![Mint](https://img.shields.io/badge/Linux%20Mint-apt-87CF3E?logo=linuxmint&logoColor=white) -![Windows](https://img.shields.io/badge/Windows-Scoop-0078D4?logo=windows&logoColor=white) - -Personal dotfiles managed with [chezmoi](https://chezmoi.io). One repo, several machines. - ## Install +On macOS, Arch Linux, Ubuntu / Debian / Linux Mint, or Windows: + ```bash chezmoi init --apply collieiscute -v ``` -## Supported platforms +Apply local configuration changes: + +```bash +chezmoi apply -v +``` + +## Theme + +Change the wallpaper and colors on Linux / macOS, or generate colors on Windows: + +```bash +chezmoi theme IMAGE +``` + +## SketchyBar (macOS) + +After the first `chezmoi apply`: + +1. Open CodexBar and Stats. In CodexBar, enable Codex with OAuth and Claude with CLI, and select an icon-and-percentage style. Relaunch CodexBar if it was already open during apply. In Stats, enable the items you want displayed. +2. Under **System Settings → Privacy & Security**, allow AeroSpace in **Screen & System Audio Recording** and **Accessibility**. If launching SketchyBar from a terminal, allow that terminal too. +3. Set **System Settings → Menu Bar → Automatically hide and show the menu bar → Always**. +4. After granting permissions, quit SketchyBar and restart AeroSpace. Reloading SketchyBar alone does not apply new permissions. + +Daily use: + +- **Workspaces:** click a number to switch. Only occupied workspaces and the current workspace are shown; keyboard shortcuts still reach hidden workspaces. +- **GPT / Claude:** hover for both providers' usage and reset countdowns; left-click for that provider's details; right-click to refresh. +- **Stats:** hover for the module name; click for its detailed panel. +- **Input source:** click to open the input-method menu. +- **Volume:** click to toggle mute; scroll to adjust by 3%. +- **Control Center:** press `Fn+C`. +- **App menus:** move the pointer to the top edge or press `Fn+Control+F2`. Move the pointer away after dismissing the menu to restore SketchyBar. + +If icons disappear after restarting an app or rearranging native menu-bar items: + +```bash +sketchybar --reload +``` + +If they are still missing, check the permissions above and inspect the available items: -| OS | Package manager | Status | -|---|---|---| -| macOS | Homebrew | daily-driven | -| Arch | pacman + paru | daily-driven | -| Ubuntu / Debian / Linux Mint | apt | CI-tested only | -| Windows | Scoop | CI-tested only | +```bash +sb-status-items list +sketchybar --query default_menu_items +``` -## Custom touches worth knowing +## Dropbox (Linux) -### Theme +- On a new machine with `dropbox-cli` installed, run `env -u DISPLAY -u WAYLAND_DISPLAY dropbox-cli start` and open the printed URL. +- Keep `dropbox.service` and `dropbox@USER.service` disabled when using Hyprland's Dropbox startup. -- Linux uses Noctalia's wallpaper-derived **light** palette; macOS and Windows use Matugen with the same per-app theme paths. -- Generated app themes stay outside chezmoi. [`.chezmoiexternal.toml`](home/.chezmoiexternal.toml) only pins the Matugen template inputs. -- Fish inherits the terminal ANSI palette; cursor themes stay independent. -- Font: **JetBrainsMono Nerd Font** across every terminal / bar / lock screen. +## Noctalia (Linux) -### chezmoi quirks I keep tripping over (that this repo handles) +- Run `chezmoi apply` after changing the monitor layout. +- If widget edits in Noctalia's GUI override your dotfiles, remove `[desktop_widgets]` and `[lockscreen_widgets]` from `~/.local/state/noctalia/settings.toml`, or copy those choices into your template before applying. +- Enable live wallpapers through Noctalia's **W Engine** bar widget. -- `run_onchange_*` scripts only re-run when their **rendered** content changes. Manifest files (`fish_plugins`, `Brewfile`) that aren't templated into the script bodies don't trigger reruns. Both are pinned via embedded sha256 hash comments — see `run_onchange_after_1-setup-fish-and-its-plugins.sh.tmpl` and `install-packages_darwin.tmpl`. -- All apt-based distros share `.packages.ubuntu.apt` and the lazygit-from-GitHub fallback (lazygit isn't in Ubuntu apt). -- Fonts use the Nerd Font patched family (`JetBrainsMono Nerd Font`), not the un-patched JetBrains Mono — drop that distinction and bar icons disappear. +### Display manager recovery (Arch) -### Dropbox +- Select **SDDM** during `archinstall`; apply the dotfiles to set up Noctalia Greeter. +- If Noctalia Greeter shows a black screen, switch to a TTY with `Ctrl+Alt+F2` and run: -- Hyprland starts `dropbox-cli` headlessly when the command is installed (currently via AUR on Arch); other Linux hosts skip it. On a new machine, run `env -u DISPLAY -u WAYLAND_DISPLAY dropbox-cli start` in a terminal and open the printed URL. -- Keep `dropbox.service` and `dropbox@USER.service` disabled; Hyprland is the only startup path. + ```bash + sudo systemctl disable --now greetd.service && sudo systemctl enable --now sddm.service + ``` -### Tmux +- If TTY switching does not work, boot with `systemd.unit=multi-user.target` from GRUB, then run the same command. -- Prefix is `C-z` (so `C-b` stays free for vim). -- Splits: `prefix |` (horizontal), `prefix -` (vertical), inheriting the current pane's path. -- `set-clipboard on` + `allow-passthrough` → OSC 52 yank works over SSH without X11 forwarding. -- Sessions auto-restore via tmux-resurrect + tmux-continuum on tmux start. +## Voice dictation -### Kitty +On Arch Linux x86_64, macOS, or Windows x64 with AVX2: -- Used on macOS specifically because [AeroSpace](https://github.com/nikitabobko/AeroSpace) tiles each Ghostty native tab as a separate window — Kitty's custom tab bar appears as a single AXWindow. -- `cmd+option`/Alt key bindings deliberately avoided (macOS 26 Tahoe intercepts them). -- Kitty, Alacritty, and Ghostty point at the fixed `noctalia` theme path; Noctalia or Matugen owns the generated colors. +1. Launch OpenWhispr (`openwhispr` on Linux / Windows). +2. Run `asr-mode sensevoice` for CPU-only recognition or `asr-mode qwen-1.7b` for GPU-backed recognition. Missing models download when selected. +3. In OpenWhispr, set Self-Hosted to `http://127.0.0.1:8080/v1` and paste the printed `OpenWhispr Model ID`. +4. Restart OpenWhispr after `chezmoi apply` to load its shortcuts. -### Hyprland +- **macOS:** grant Microphone and Accessibility permissions. +- **Windows:** reopen the terminal after applying; install the [Visual C++ x64 runtime](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist), allow microphone access, and install a Vulkan GPU driver for Qwen. +- **Linux:** if OpenWhispr 1.9.2 shows a ydotool setup warning, do not add the `input` group or daemon just to dismiss it; `wtype` is already selected. +- Run `asr-mode off` before gaming to stop recognition and release GPU memory. Quit OpenWhispr separately if you also want to disable its UI and shortcut. -- Desc-keyed Lua `hl.monitor(...)` overrides plus a fallback that selects the highest resolution and refresh rate available at that resolution. -- Cursor: Catppuccin Mocha Teal (Hyprcursor) with Catppuccin Mocha Green as XCursor fallback. -- Electron / fcitx5 / Qt integration env vars set centrally. Noctalia applies GTK light mode and generated GTK/Qt colors through its built-in templates. -- Noctalia v5 owns the desktop shell layer (bar, launcher, notifications, wallpaper, lock screen, idle, screenshots, clipboard). -- Noctalia Shell is installed on Arch and Ubuntu. Noctalia Greeter stays Arch-only; Ubuntu keeps SDDM. -- Wallpapers are deployed by chezmoi to `~/.config/wallpapers`; Noctalia reads that path directly. -- Noctalia is the Linux wallpaper/theme owner for apps with built-in adapters. App integrations write generated theme files and reload apps; chezmoi keeps their main configs pre-aligned so post-hooks do not cause drift. -- Noctalia desktop/lockscreen widget placement is generated from monitor roles and ratios in `20-widgets.generated.toml.tmpl`; run `chezmoi apply` after changing the monitor layout. -- If widgets are edited in Noctalia's GUI, remove `[desktop_widgets]` and `[lockscreen_widgets]` from `~/.local/state/noctalia/settings.toml` or fold the new ratios back into the template; state overrides win over declarative config. -- Wallpaper Engine is opt-in through Noctalia's W Engine bar widget; palettes sync through Noctalia and Steam Workshop selections stay machine-local. +## AI extensions + +- Add plugins and marketplaces in [`home/.chezmoidata/ai.yaml`](home/.chezmoidata/ai.yaml), then run `chezmoi apply` and restart the affected app. +- Place shared user skills in `~/.agents/skills`. +- Review and trust new Codex hooks manually with `/hooks`. +- For OpenCode's zh-TW linting, install [zhtw-mcp](https://github.com/sysprog21/zhtw-mcp) from source with `make install`; its binary must be at `~/.local/bin/zhtw-mcp`. ## Keymappings @@ -106,37 +132,6 @@ chezmoi init --apply collieiscute -v | `XF86Audio*` | volume / mute / mic mute | | `XF86MonBrightness*` | screen brightness | -Voice dictation uses OpenWhispr on Arch Linux x86_64, macOS and Windows x86_64. Select -`asr-mode sensevoice` for the CPU-only SenseVoice model or `asr-mode qwen-1.7b` -for the GPU-backed Qwen3-ASR model; missing models download only when selected. -Models use `${XDG_CACHE_HOME:-$HOME/.cache}/crispasr` on Linux and -`${XDG_CACHE_HOME:-$HOME/Library/Caches}/crispasr` on macOS, and -`%LOCALAPPDATA%\crispasr` on Windows. -Run `asr-mode off` before gaming to stop CrispASR, close port 8080, and release -its GPU memory. OpenWhispr remains in the tray; quit it separately when its UI -and shortcut are not needed. - -On Linux, launch `openwhispr` from your terminal or the application menu. -OpenWhispr follows the latest official release; do not pin its version. -Linux patches the known Hold startup bug only when the source matches; -changed upstream code is left untouched and does not block updates. - -For first-time OpenWhispr setup, run `asr-mode qwen-1.7b`, then set Self-Hosted -to `http://127.0.0.1:8080/v1` and paste the printed `OpenWhispr Model ID`. Each -successful model switch prints its current absolute path for later connection -tests. Chezmoi keeps OpenWhispr in push mode with `SHIFT+F13` for dictation and -`Meta+F13` for the local voice agent. Linux uses its native Hyprland binding -and `wtype`; macOS requires Microphone and Accessibility access. OpenWhispr -loads these shortcuts at startup, so restart it after `chezmoi apply`. Version -1.9.2 may still show a ydotool setup warning on Linux; `wtype` is already -preferred, so do not add the `input` group or daemon just to dismiss it. - -On Windows x64 with AVX2, reopen your terminal after applying, then run `openwhispr` and -`asr-mode qwen-1.7b` (GPU) or `asr-mode sensevoice` (CPU). -Install the [Visual C++ x64 runtime](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist), -allow microphone access, configure Self-Hosted as above, and install a Vulkan -GPU driver for Qwen. - ### AeroSpace (macOS) | Bind | Action | @@ -147,8 +142,6 @@ GPU driver for Qwen. | `Cmd+Option+S` | toggle the dedicated `magic` workspace | | `Cmd+Option+Shift+S` | move window to the `magic` workspace | -AeroSpace restores the swapped root layouts and window states where possible; its CLI cannot reconstruct nested container geometry. - ### Tmux (prefix = `C-z`) | Bind | Action | @@ -187,100 +180,3 @@ AeroSpace restores the swapped root layouts and window states where possible; it |---|---| | `vi` | `nvim` (when nvim is installed) | | `buu` | `brew update && brew upgrade -y && fisher update` (macOS only) | - -## Tools - -### Shell / multiplexer - -- [`fish`](https://fishshell.com) — primary shell. -- [`tmux`](https://github.com/tmux/tmux) — primary multiplexer. -- [`zellij`](https://zellij.dev) — secondary multiplexer using the generated Noctalia-compatible theme. - -### Editor - -- [`neovim`](https://github.com/neovim/neovim) — LazyVim distro on top. - -### Terminals - -- [`kitty`](https://sw.kovidgoyal.net/kitty/) — macOS daily driver (AeroSpace-friendly tabs). -- [`wezterm`](https://wezterm.org) — cross-platform fallback. -- [`ghostty`](https://ghostty.org) — newer GPU terminal using the generated Noctalia-compatible theme. -- [`alacritty`](https://github.com/alacritty/alacritty) — minimal GPU terminal. - -### Wayland stack (Hyprland) - -- [`hyprland`](https://hyprland.org) — Wayland compositor. -- [`noctalia`](https://github.com/noctalia-dev/noctalia) — desktop shell: bar, launcher, notifications, wallpaper, lock screen, idle behavior, screenshots, clipboard, and control center. -- [`linux-wallpaperengine`](https://github.com/Almamu/linux-wallpaperengine) — opt-in live wallpapers from Steam Wallpaper Engine assets on Hyprland. -- [`sddm`](https://github.com/sddm/sddm) — bootstrap and fallback display manager for fresh Arch installs. -- [`greetd`](https://git.sr.ht/~kennylevinsen/greetd) + [`Noctalia Greeter`](https://github.com/noctalia-dev/noctalia-greeter) — final Wayland login greeter after AUR packages are installed. -- [`fcitx5`](https://github.com/fcitx/fcitx5) + [McBopomofo](https://github.com/openvanilla/fcitx5-mcbopomofo) + chewing — Chinese input with Hsu keyboard support. - -### Display manager bootstrap and recovery - -- Fresh Arch installs should use `SDDM` from `archinstall` first; chezmoi switches to `greetd` only after `noctalia-greeter-session` is installed and setup succeeds. -- `SDDM` stays installed as the fallback display manager. -- The switch script only enables/disables services; it does not stop/start the current graphical session. -- If Noctalia Greeter shows a black screen, switch to a TTY with `Ctrl+Alt+F2` and run `sudo systemctl disable --now greetd.service && sudo systemctl enable --now sddm.service`. -- If TTY switching does not work, boot with `systemd.unit=multi-user.target` from GRUB, then run the same service switch. - -### macOS extras - -- [`aerospace`](https://github.com/nikitabobko/AeroSpace) — tiling WM. -- [`karabiner-elements`](https://karabiner-elements.pqrs.org) — keyboard remapper. -- Desktop wallpaper is deployed by chezmoi to `~/.config/wallpapers`; `run_onchange_after_6-apply-theme.sh.tmpl` applies the initial wallpaper and Matugen palette, and `chezmoi theme [IMAGE]` changes both later. - -### CLI tooling - -- File / dir: [`eza`](https://github.com/eza-community/eza), [`fd`](https://github.com/sharkdp/fd), [`ripgrep`](https://github.com/BurntSushi/ripgrep), [`bat`](https://github.com/sharkdp/bat), [`zoxide`](https://github.com/ajeetdsouza/zoxide), [`fzf`](https://github.com/junegunn/fzf). -- System info: [`btop`](https://github.com/aristocratos/btop), [`fastfetch`](https://github.com/fastfetch-cli/fastfetch). -- Git: [`lazygit`](https://github.com/jesseduffield/lazygit), [`tig`](https://github.com/jonas/tig), [`gh`](https://cli.github.com), [`glab`](https://gitlab.com/gitlab-org/cli), [`onefetch`](https://github.com/o2sh/onefetch). -- Build / dev: [`gnu-tar`](https://www.gnu.org/software/tar/), [`bear`](https://github.com/rizsotto/Bear), [`cmake`](https://cmake.org), [`mold`](https://github.com/rui314/mold), [`ninja`](https://ninja-build.org), [`llvm`](https://llvm.org), [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html), [`cppcheck`](https://cppcheck.sourceforge.io), [`uv`](https://github.com/astral-sh/uv). -- Containers: macOS Apple silicon [`container`](https://github.com/apple/container) + third-party [`container-compose`](https://github.com/Mcrich23/Container-Compose); Linux [`podman`](https://podman.io). -- Docs: [`typst`](https://typst.app), [`tldr`](https://tldr.sh). -- OpenCode zh-TW linting: [`zhtw-mcp`](https://github.com/sysprog21/zhtw-mcp) is configured as a local MCP server at `~/.local/bin/zhtw-mcp`. Until upstream publishes releases, install it from source with `make install` so OpenCode can use the fixed binary path. -- OpenCode Claude Code plugin: [`@khalilgharbaoui/opencode-claude-code-plugin`](https://github.com/khalilgharbaoui/opencode-claude-code-plugin) is loaded through OpenCode's native npm plugin support; after changing the plugin list, run `chezmoi apply /home/collie/.config/opencode/opencode.json` and restart OpenCode. - -### AI extensions - -- [`home/.chezmoidata/ai.yaml`](home/.chezmoidata/ai.yaml) is the single inventory for plugins and marketplaces; add entries there without editing templates. -- Claude Code and Codex update configured marketplace plugins with their native startup updaters. -- OpenCode installs configured npm plugins when its generated cache is missing; remove that cache before startup to fetch newer versions. -- Shared user skills live in `~/.agents/skills`, which Codex and OpenCode discover natively. -- `run_after_5-sync-ai-extensions.sh.tmpl` bootstraps missing Codex plugins. -- Review and trust new Codex hooks manually with `/hooks`. - -### Fish plugins (managed by [`fisher`](https://github.com/jorgebucaran/fisher)) - -- `edc/bass` — run bash scripts in fish. -- `jorgebucaran/nvm.fish` — Node version manager. -- `patrickf1/fzf.fish` — fzf integrations. -- `pure-fish/pure` — minimal prompt. - -### Tmux plugins (managed by [`TPM`](https://github.com/tmux-plugins/tpm)) - -- `mrjones2014/smart-splits.nvim` — `Ctrl+Arrow` resize, plays nice with neovim. -- `tmux-plugins/tmux-sensible` — sensible defaults. -- `tmux-plugins/tmux-continuum` — auto-save/restore on start. -- `tmux-plugins/tmux-resurrect` — manual save/restore + nvim session capture. - -### Neovim — LazyVim core extras - -- `zbirenbaum/copilot.lua` — Copilot. -- `giuxtaposition/blink-cmp-copilot` — Copilot source for blink.cmp. -- `neovim/nvim-lspconfig` + `p00f/clangd_extensions.nvim` — LSP, with extra clangd polish. -- `nvim-treesitter/nvim-treesitter` — syntax. -- `ibhagwan/fzf-lua` — fuzzy finder. -- `sphamba/smear-cursor.nvim` — animated cursor. -- `folke/snacks.nvim` — utility collection. - -## Layout - -``` -home/ # chezmoi source root (.chezmoiroot=home) -├── .chezmoidata/packages.yaml # canonical package list (paru + apt) -├── .chezmoiexternal.toml # pinned external template inputs -├── .chezmoiscripts/ # run_once / run_onchange bootstrap -├── .chezmoitemplates/ # macOS install template (Brewfile pass-thru) -└── dot_config/ # → ~/.config/... -``` diff --git a/home/.chezmoiignore b/home/.chezmoiignore index 17b8c3d..309d584 100644 --- a/home/.chezmoiignore +++ b/home/.chezmoiignore @@ -6,6 +6,8 @@ .config/brew/** .config/karabiner .config/karabiner/** +.config/sketchybar +.config/sketchybar/** Library Library/** {{- end }} diff --git a/home/.chezmoiscripts/run_onchange_after_2-codexbar-icons.sh.tmpl b/home/.chezmoiscripts/run_onchange_after_2-codexbar-icons.sh.tmpl new file mode 100644 index 0000000..42ce586 --- /dev/null +++ b/home/.chezmoiscripts/run_onchange_after_2-codexbar-icons.sh.tmpl @@ -0,0 +1,7 @@ +{{- if eq .chezmoi.os "darwin" -}} +#!/bin/sh +set -eu +# Two upstream meters share one SketchyBar group and hover summary. +# CodexBar reads this setting on launch; no account preferences are changed. +defaults write com.steipete.codexbar mergeIcons -bool false +{{- end }} diff --git a/home/.chezmoiscripts/run_onchange_after_3-build-sketchybar-menu.sh.tmpl b/home/.chezmoiscripts/run_onchange_after_3-build-sketchybar-menu.sh.tmpl new file mode 100644 index 0000000..34065a2 --- /dev/null +++ b/home/.chezmoiscripts/run_onchange_after_3-build-sketchybar-menu.sh.tmpl @@ -0,0 +1,22 @@ +{{- if eq .chezmoi.os "darwin" -}} +#!/bin/sh +set -eu +# helper: {{ include "dot_config/sketchybar/menu-bar-events.swift" | sha256sum }} +# status items: {{ include "dot_config/sketchybar/status-items.swift" | sha256sum }} + +mkdir -p "$HOME/.local/bin" "$HOME/.cache/sketchybar" +menu_build=$(mktemp "$HOME/.local/bin/.sb-menu-events.XXXXXX") +trap 'rm -f "$menu_build"' EXIT HUP INT TERM +xcrun swiftc -O "$HOME/.config/sketchybar/menu-bar-events.swift" -o "$menu_build" +mv "$menu_build" "$HOME/.local/bin/sb-menu-events" +menu_build=$(mktemp "$HOME/.local/bin/.sb-status-items.XXXXXX") +xcrun swiftc -O "$HOME/.config/sketchybar/status-items.swift" -o "$menu_build" +mv "$menu_build" "$HOME/.local/bin/sb-status-items" + +# Restart only our helper; a running bar reloads after its supervisor exits. +pkill -x -u "$(id -u)" sb-menu-events || : +lockf -kst 5 "$HOME/.cache/sketchybar/menu-events.lock" true +if pgrep -x -u "$(id -u)" sketchybar >/dev/null; then + sketchybar --reload +fi +{{- end }} diff --git a/home/.chezmoiscripts/run_onchange_after_6-apply-theme.sh.tmpl b/home/.chezmoiscripts/run_onchange_after_6-apply-theme.sh.tmpl index 79bc486..d2b0d14 100644 --- a/home/.chezmoiscripts/run_onchange_after_6-apply-theme.sh.tmpl +++ b/home/.chezmoiscripts/run_onchange_after_6-apply-theme.sh.tmpl @@ -4,6 +4,7 @@ set -eu # wallpaper: {{ include "dot_config/wallpapers/xp_wallpaper.jpg" | sha256sum }} # config: {{ include "dot_config/matugen/config.toml.tmpl" | sha256sum }} +# sketchybar: {{ include "dot_config/sketchybar/colors.sh.tera" | sha256sum }} # inputs: {{ include ".chezmoiexternal.toml" | sha256sum }} # dispatcher: {{ include "dot_local/bin/executable_chezmoi-theme.tmpl" | sha256sum }} diff --git a/home/dot_config/aerospace/aerospace.toml b/home/dot_config/aerospace/aerospace.toml index 0f81206..0c861d8 100644 --- a/home/dot_config/aerospace/aerospace.toml +++ b/home/dot_config/aerospace/aerospace.toml @@ -17,6 +17,18 @@ config-version = 2 start-at-login = true # auto-reload-config = true # NOTE: requires AeroSpace > v0.20.2-Beta (not yet released) +after-startup-command = [ + 'exec-and-forget open -gja CodexBar', + 'exec-and-forget open -gja Stats', + # ponytail: one delayed reload covers login-item startup; add an app event helper only if three seconds proves insufficient. + # --reload returns success even without a running bar; check the process first. + 'exec-and-forget /bin/sh -c "sleep 3; if pgrep -x sketchybar >/dev/null; then sketchybar --reload; else sketchybar; fi"', +] + +exec-on-workspace-change = ['/bin/bash', '-c', + 'sketchybar --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE' +] + # Neutralize macOS "Hide Application" (Cmd+H) since we use Cmd+H for focus # AeroSpace intercepts Cmd+H before macOS, but if somehow an app hides, this unhides it automatically-unhide-macos-hidden-apps = true @@ -41,7 +53,7 @@ on-focused-monitor-changed = ['move-mouse monitor-lazy-center'] inner.vertical = 0 outer.left = 0 outer.bottom = 0 - outer.top = 0 + outer.top = 36 outer.right = 0 # ──────────────────────────────────────── diff --git a/home/dot_config/brew/Brewfile b/home/dot_config/brew/Brewfile index 802c53b..ea647e6 100644 --- a/home/dot_config/brew/Brewfile +++ b/home/dot_config/brew/Brewfile @@ -1,5 +1,3 @@ -tap "cxwx/cxbrew", trusted: { formula: "matugen" } - brew "bat" brew "bear" brew "btop" @@ -15,6 +13,7 @@ brew "dos2unix" brew "eza" brew "fastfetch" brew "fd" +brew "FelixKratz/formulae/sketchybar", trusted: true brew "fish" brew "gh" brew "git" @@ -22,7 +21,7 @@ brew "glab" brew "gnu-tar" brew "lazygit" brew "llvm" -brew "cxwx/cxbrew/matugen" +brew "cxwx/cxbrew/matugen", trusted: true brew "mold" brew "neovim" brew "ninja" @@ -42,6 +41,7 @@ brew "zellij" brew "zoxide" cask "brave-browser" cask "claude-code@latest" +cask "codexbar" cask "flutter" cask "font-jetbrains-mono-nerd-font" cask "ghostty" diff --git a/home/dot_config/matugen/config.toml.tmpl b/home/dot_config/matugen/config.toml.tmpl index b7751da..78e8245 100644 --- a/home/dot_config/matugen/config.toml.tmpl +++ b/home/dot_config/matugen/config.toml.tmpl @@ -19,6 +19,11 @@ output_path = "~/.config/ghostty/themes/noctalia" input_path = "~/.config/matugen/templates/tmux/tmux.conf" output_path = "~/.config/tmux/themes/noctalia.conf" post_hook = 'bash "$HOME/.config/matugen/templates/tmux/apply.sh"' + +[templates.sketchybar] +input_path = "~/.config/sketchybar/colors.sh.tera" +output_path = "~/.config/sketchybar/colors.sh" +post_hook = 'command -v sketchybar >/dev/null 2>&1 && sketchybar --reload || true' {{ else -}} [templates.alacritty] input_path = "~/.config/matugen/templates/matugen-themes/alacritty.toml" diff --git a/home/dot_config/sketchybar/colors.sh.tera b/home/dot_config/sketchybar/colors.sh.tera new file mode 100644 index 0000000..02f908c --- /dev/null +++ b/home/dot_config/sketchybar/colors.sh.tera @@ -0,0 +1,6 @@ +# Generated by Matugen. Edit colors.sh.tera, not colors.sh. +TRANSPARENT=0x00000000 +CAPSULE=0x4d{{colors.surface_variant.default.hex_stripped}} +ON_SURFACE=0xff{{colors.on_surface.default.hex_stripped}} +PRIMARY=0xff{{colors.primary.default.hex_stripped}} +ON_PRIMARY=0xff{{colors.on_primary.default.hex_stripped}} diff --git a/home/dot_config/sketchybar/executable_sketchybarrc b/home/dot_config/sketchybar/executable_sketchybarrc new file mode 100755 index 0000000..c12179c --- /dev/null +++ b/home/dot_config/sketchybar/executable_sketchybarrc @@ -0,0 +1,176 @@ +#!/bin/sh + +CONFIG_DIR=${CONFIG_DIR:-"$HOME/.config/sketchybar"} +PLUGIN_DIR="$CONFIG_DIR/plugins" +# Brackets absorb member padding; normal capsules own the full gap on the left. +# https://github.com/FelixKratz/SketchyBar/blob/v2.24.0/src/group.c +CAPSULE_GAP=8 + +# Safe first-run palette; Matugen replaces it and reloads the bar. +TRANSPARENT=0x00000000 +CAPSULE=0x4d303034 +ON_SURFACE=0xfff2f0f4 +PRIMARY=0xffd0bcff +ON_PRIMARY=0xff381e72 +test -r "$CONFIG_DIR/colors.sh" && . "$CONFIG_DIR/colors.sh" + +sketchybar --bar \ + position=top \ + height=36 \ + notch_width=200 \ + y_offset=0 \ + topmost=off \ + color="$TRANSPARENT" \ + shadow=off \ + corner_radius=0 \ + margin=0 \ + padding_left="$CAPSULE_GAP" \ + padding_right="$CAPSULE_GAP" \ + display=all + +sketchybar --default \ + icon.font="JetBrainsMono Nerd Font:Regular:14.0" \ + label.font="JetBrainsMono Nerd Font:Regular:13.0" \ + icon.color="$ON_SURFACE" \ + label.color="$ON_SURFACE" \ + icon.padding_left=6 \ + icon.padding_right=4 \ + label.padding_left=4 \ + label.padding_right=6 \ + background.height=27 \ + background.corner_radius=9 \ + background.shadow.drawing=off \ + popup.height=28 \ + popup.background.color="0xff${CAPSULE#????}" \ + popup.background.border_width=1 \ + popup.background.border_color="0x33${ON_SURFACE#????}" \ + padding_left=2 \ + padding_right=2 + +sketchybar --add event aerospace_workspace_change + +# q/e anchor to either side of the notch instead of drawing through its center. +sketchybar --add item clock q \ + --set clock update_freq=1 script="$PLUGIN_DIR/clock.sh" + +for sid in 1 2 3 4 5 6 7 8 9 magic; do + label=$sid + test "$sid" = magic && label=M + sketchybar --add item "space.$sid" e \ + --set "space.$sid" \ + icon="$label" \ + icon.highlight_color="$ON_PRIMARY" \ + label.drawing=off \ + background.color="$PRIMARY" \ + background.drawing=off \ + click_script="aerospace workspace $sid" \ + drawing=off +done + +sketchybar --add item workspace.observer e \ + --set workspace.observer drawing=off updates=on update_freq=2 \ + script="$PLUGIN_DIR/workspace.sh" \ + --subscribe workspace.observer aerospace_workspace_change space_windows_change front_app_switched system_woke + +sketchybar --add bracket workspaces '/space\..*/' + +sketchybar --add item volume right \ + --set volume script="$PLUGIN_DIR/volume.sh" \ + --subscribe volume volume_change mouse.scrolled mouse.clicked system_woke + +add_alias() { + # Item-0 is shared by unrelated macOS 26 items: retain its resolved index. + case "$1" in + *,Item-0\(*|*,\(*) source=$1 ;; + *) source=$(printf '%s\n' "$1" | sed 's/([0-9][0-9]*)$//') ;; + esac + sketchybar --add alias "$source" "$3" || return 0 + sketchybar --rename "$source" "$2" --set "$2" "click_script=${4-}" +} + +# A zero-width anchor gives both provider icons one shared hover popup. +sketchybar --add item codexbar left \ + --set codexbar width=0 padding_left=0 padding_right=0 \ + icon.drawing=off label.drawing=off \ + popup.align=left \ + popup.background.corner_radius=10 \ + update_freq=60 script="$PLUGIN_DIR/codexbar.sh" \ + --subscribe codexbar system_woke +sketchybar --add item codexbar.status popup.codexbar \ + --set codexbar.status width=330 icon.drawing=off \ + label="CodexBar · 正在讀取…" label.color="$PRIMARY" \ + label.padding_left=12 label.padding_right=12 + +# Only Stats, CodexBar and the input source are included; there is no App tray. +status_helper="$HOME/.local/bin/sb-status-items" +menu_items=$("$status_helper" list) || menu_items='[]' +printf '%s\n' "$menu_items" \ + | /usr/bin/jq -r '.[] | [.bundle, .source, .window, .help] | @tsv' \ + | ( + while IFS="$(printf '\t')" read -r bundle source window help; do + click=$(/usr/bin/jq -nr --arg helper "$status_helper" --arg bundle "$bundle" --arg window "$window" \ + '[$helper, "click", $bundle, $window] | @sh') + case "$bundle" in + com.steipete.codexbar) + case "$window" in codexbar-codex|codexbar-claude) ;; *) continue ;; esac + name="meter.${window#codexbar-}" + add_alias "$source" "$name" left '' + sketchybar --set "$name" \ + script="$PLUGIN_DIR/native-menu.sh" \ + --subscribe "$name" mouse.entered mouse.exited mouse.clicked + ;; + eu.exelban.Stats) + add_alias "$source" "stats.$window" right "$click" + sketchybar --set "stats.$window" \ + popup.align=right \ + popup.background.corner_radius=6 \ + script="$PLUGIN_DIR/native-menu.sh" \ + --subscribe "stats.$window" mouse.entered mouse.exited mouse.clicked \ + --add item "hint.$window" "popup.stats.$window" \ + --set "hint.$window" icon.drawing=off \ + label="${help:-$window}" label.padding_left=10 label.padding_right=10 + ;; + com.apple.TextInputMenuAgent) + add_alias "$source" input_source right "$click" + sketchybar --move input_source before volume + ;; + esac + done + ) + +# Style aliases before adding their brackets, which absorb member padding. +# Preserve raster colors: tinting an opaque chart makes it a solid rectangle. +for aliases in '/^meter\./' '/^stats\./' '/^input_source$/'; do + sketchybar --set "$aliases" icon.drawing=off label.drawing=off \ + padding_left=1 padding_right=1 alias.scale=0.82 alias.update_freq=2 +done +sketchybar --set '/^meter\./' alias.scale=0.85 padding_left=0 padding_right=0 \ + --set '/^input_source$/' alias.scale=1 +for group in meter stats; do + sketchybar --add bracket "$group.group" "/$group\\..*/" +done +for capsule in clock volume '/^input_source$/' workspaces '/\.group$/'; do + sketchybar --set "$capsule" background.color="$CAPSULE" background.drawing=on +done +for capsule in clock volume '/^input_source$/'; do + sketchybar --set "$capsule" padding_left="$CAPSULE_GAP" padding_right=0 +done + +sketchybar --update + +# One event listener survives reloads; the lock covers its fail-open supervisor. +menu_helper="$HOME/.local/bin/sb-menu-events" +# macOS pgrep excludes ancestors unless -a is set; the bar launches this rc. +# Clients share its name, so select the oldest instance including our parent. +bar_pid=$(pgrep -a -o -x -u "$(id -u)" sketchybar) +if test -x "$menu_helper" && test -n "$bar_pid"; then + mkdir -p "$HOME/.cache/sketchybar" + lockf -kst 0 "$HOME/.cache/sketchybar/menu-events.lock" /bin/sh -c ' + "$1" watch "$2" "$3" + "$2" --bar hidden=off + ' sh "$menu_helper" "$(command -v sketchybar)" "$bar_pid" & + "$menu_helper" sync "$(command -v sketchybar)" "$bar_pid" +else + sketchybar --bar hidden=off + printf '%s\n' 'SketchyBar menu coordination is unavailable; run chezmoi apply.' >&2 +fi diff --git a/home/dot_config/sketchybar/menu-bar-events.swift b/home/dot_config/sketchybar/menu-bar-events.swift new file mode 100644 index 0000000..b333ed9 --- /dev/null +++ b/home/dot_config/sketchybar/menu-bar-events.swift @@ -0,0 +1,79 @@ +import AppKit +import Carbon +import CoreGraphics + +// CarbonEvents.h documents these public events as broadcasts to all processes: +// kEventMenuBarShown / kEventMenuBarHidden. No event tap or private SkyLight API. +let arguments = CommandLine.arguments +guard arguments.count == 4, let barPID = Int32(arguments[3]), barPID > 0 else { + fputs("usage: sb-menu-events watch|sync SKETCHYBAR_PATH BAR_PID\n", stderr) + exit(64) +} +let executable = URL(fileURLWithPath: arguments[2]) +var lastHidden: Bool? + +func nativeBarVisible() -> Bool { + // NSMenu.menuBarVisible() describes this app, not the foreground app. + // https://developer.apple.com/documentation/coregraphics/cgwindowlistcopywindowinfo(_:_:) + let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], 0) + as? [[String: Any]] ?? [] + return windows.contains { + $0[kCGWindowOwnerName as String] as? String == "Window Server" + && $0[kCGWindowName as String] as? String == "Menubar" + } +} + +func setHidden(_ hidden: Bool) { + guard lastHidden != hidden else { return } + let command = Process() + command.executableURL = executable + command.arguments = ["--bar", "hidden=\(hidden ? "on" : "off")"] + do { + try command.run() + command.waitUntilExit() + guard command.terminationStatus == 0 else { exit(1) } + lastHidden = hidden + } catch { + fputs("sb-menu-events: \(error)\n", stderr) + exit(1) + } +} + +guard arguments[1] == "watch" || arguments[1] == "sync" else { exit(64) } +let app = NSApplication.shared +app.setActivationPolicy(.prohibited) +if arguments[1] == "sync" { + setHidden(nativeBarVisible()) + exit(0) +} + +var eventTypes = [ + EventTypeSpec(eventClass: OSType(kEventClassMenu), eventKind: UInt32(kEventMenuBarShown)), + EventTypeSpec(eventClass: OSType(kEventClassMenu), eventKind: UInt32(kEventMenuBarHidden)), +] +let status = InstallEventHandler(GetApplicationEventTarget(), { _, event, _ in + // ponytail: hide all displays together; use per-display bar IDs if needed. + setHidden(GetEventKind(event) == kEventMenuBarShown || nativeBarVisible()) + return OSStatus(eventNotHandledErr) +}, eventTypes.count, &eventTypes, nil, nil) +guard status == noErr else { + fputs("sb-menu-events: cannot subscribe to menu events (\(status))\n", stderr) + exit(1) +} + +let workspace = NSWorkspace.shared.notificationCenter +for name in [NSWorkspace.didWakeNotification, NSWorkspace.activeSpaceDidChangeNotification] { + workspace.addObserver(forName: name, object: nil, queue: .main) { _ in + setHidden(nativeBarVisible()) + } +} +NotificationCenter.default.addObserver( + forName: NSApplication.didChangeScreenParametersNotification, object: nil, queue: .main +) { _ in setHidden(nativeBarVisible()) } + +// The launching shell restores the bar even after SIGKILL or a helper crash. +let barExit = DispatchSource.makeProcessSource(identifier: barPID, eventMask: .exit, queue: .main) +barExit.setEventHandler { exit(0) } +barExit.resume() +setHidden(nativeBarVisible()) +app.run() diff --git a/home/dot_config/sketchybar/plugins/executable_clock.sh b/home/dot_config/sketchybar/plugins/executable_clock.sh new file mode 100755 index 0000000..81c2275 --- /dev/null +++ b/home/dot_config/sketchybar/plugins/executable_clock.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu + +label=$(date '+%m %d %H:%M:%S' | awk '{ + split("壹 貳 參 肆 伍 陸 柒 捌 玖 拾 拾壹 拾貳", months) + printf "%s月 %d %s", months[$1 + 0], $2, $3 +}') +sketchybar --set "$NAME" label="$label" diff --git a/home/dot_config/sketchybar/plugins/executable_codexbar.sh b/home/dot_config/sketchybar/plugins/executable_codexbar.sh new file mode 100755 index 0000000..b8b3d4a --- /dev/null +++ b/home/dot_config/sketchybar/plugins/executable_codexbar.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +# Keep only rendered labels in SketchyBar, not account JSON or credentials. +# macOS lockf prevents overlapping refreshes and releases on process exit. +cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/sketchybar" +mkdir -p "$cache_dir" || exit 1 +exec 9>"$cache_dir/codexbar.lock" +/usr/bin/lockf -s -t 0 9 || exit 0 + +sketchybar --set codexbar.status label="CodexBar · 更新中…" +# Both providers keep their own configured source: Codex OAuth, Claude CLI. +if ! data=$(codexbar usage --provider both --format json --json-only --no-credits 2>/dev/null); then + sketchybar --set codexbar.status label="CodexBar · 更新失敗,資料未更新" + exit 1 +fi + +if ! rows=$(printf '%s\n' "$data" | /usr/bin/jq -er ' + now as $now + | if type == "array" and (map(.provider) | sort) == ["claude", "codex"] + and all(.error == null and (.usage | type) == "object") + then .[] else error("Incomplete provider response") end + | (if .provider == "codex" then "Codex · OAuth" else "Claude · CLI" end) as $provider + | .usage as $usage + | (["header", $provider], + (if .stale == true then ["note", "注意:來源回報為快取資料"] else empty end), + ([{title: ({"300": "5 小時", "10080": "每週"}[($usage.primary.windowMinutes | tostring)] // "主要額度"), window: $usage.primary}, + {title: "每週", window: $usage.secondary}, + {title: "其他額度", window: $usage.tertiary}, + $usage.extraRateWindows[]?] + | map(select((.window.usedPercent | type) == "number")) + | if length == 0 then ["note", "目前沒有回報額度區間"] + else .[] + | ((.title // "額度") | tostring | gsub("[\r\n\t]"; " ")) as $title + | ([100, ([0, (100 - .window.usedPercent)] | max)] | min | round) as $remaining + | (try (.window.resetsAt | fromdateiso8601) catch null) as $reset + | ["quota", "\($title) · 剩餘 \($remaining)%", + (if $reset == null then "" + elif $reset <= $now then "↳ 重置時間已到,等待來源更新" + else (($reset - $now) / 60 | ceil) as $minutes + | (if $minutes >= 1440 then + "\(($minutes / 1440) | floor) 天 \((($minutes % 1440) / 60) | floor) 小時 \($minutes % 60) 分" + elif $minutes >= 60 then + "\(($minutes / 60) | floor) 小時 \($minutes % 60) 分" + else "\($minutes) 分" end) as $duration + | "↳ 約 \($duration)後重置" + end)] + end)) + | join("\t") +'); then + sketchybar --set codexbar.status label="CodexBar · 回應不完整,資料未更新" + exit 1 +fi + +CONFIG_DIR=${CONFIG_DIR:-"$HOME/.config/sketchybar"} +ON_SURFACE=0xfff2f0f4 +PRIMARY=0xffd0bcff +test -r "$CONFIG_DIR/colors.sh" && . "$CONFIG_DIR/colors.sh" + +# One native item holds both lines, so its border encloses the whole quota. +# https://felixkratz.github.io/SketchyBar/config/items#background-properties +# One IPC batch replaces the tooltip without flashing an empty popup. +set -- --remove '/codexbar\.usage\..*/' \ + --set codexbar.status "label=CodexBar · 讀取 $(date +%H:%M:%S)" +index=0 +while IFS="$(printf '\t')" read -r kind title reset; do + index=$((index + 1)) + set -- "$@" --add item "codexbar.usage.$index" popup.codexbar \ + --set "codexbar.usage.$index" width=314 padding_left=10 padding_right=10 \ + icon.drawing=off "label=$title" label.max_chars=40 \ + label.padding_left=10 label.padding_right=10 + case "$kind" in + header) + set -- "$@" label.color="$PRIMARY" label.font.style=Bold + ;; + quota) + set -- "$@" icon.drawing=on "icon=$title" \ + icon.font="JetBrainsMono Nerd Font:Regular:13.0" \ + icon.width=0 icon.padding_left=10 icon.padding_right=0 icon.max_chars=40 \ + label="$reset" label.font.size=12 label.color="0xcc${ON_SURFACE#????}" \ + background.drawing=on background.color="0x08${ON_SURFACE#????}" \ + background.border_width=1 background.border_color="0x55${ON_SURFACE#????}" \ + background.corner_radius=6 background.height=52 \ + icon.y_offset=11 label.y_offset=-11 + if test -z "$reset"; then + set -- "$@" icon.y_offset=0 label.drawing=off background.height=32 + fi + ;; + esac +done </dev/null + ;; +esac +case ${SENDER:-} in + volume_change) volume=${INFO:-0} ;; + *) volume=$(osascript -e 'output volume of (get volume settings)' 2>/dev/null || printf '0\n') ;; +esac +case $volume in *[!0-9]*|'') volume=0 ;; esac + +case ${SENDER:-} in + mouse.scrolled) + case ${SCROLL_DELTA:-0} in + -*) volume=$((volume - 3)) ;; + 0|'') ;; + *) volume=$((volume + 3)) ;; + esac + test "$volume" -ge 0 || volume=0 + test "$volume" -le 100 || volume=100 + osascript -e "set volume output volume $volume" >/dev/null + ;; +esac + +muted=$(osascript -e 'output muted of (get volume settings)' 2>/dev/null || printf 'false\n') + +if test "$muted" = true || test "$volume" -eq 0; then + icon='󰖁' +elif test "$volume" -lt 30; then + icon='󰕿' +elif test "$volume" -lt 60; then + icon='󰖀' +else + icon='󰕾' +fi + +sketchybar --set "$NAME" icon="$icon" label="$volume%" diff --git a/home/dot_config/sketchybar/plugins/executable_workspace.sh b/home/dot_config/sketchybar/plugins/executable_workspace.sh new file mode 100755 index 0000000..7e4c35d --- /dev/null +++ b/home/dot_config/sketchybar/plugins/executable_workspace.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -efu + +# One batch updates all slots. Events handle focus/create/close; the 2 s refresh +# also covers moving a background window without a native window event. +# https://nikitabobko.github.io/AeroSpace/commands#list-workspaces +mkdir -p "$HOME/.cache/sketchybar" +exec 9>"$HOME/.cache/sketchybar/workspaces.lock" +lockf -s -t 0 9 || exit 0 +focused=$(aerospace list-workspaces --focused) || exit 1 +occupied=$(aerospace list-workspaces --monitor all --empty no) || exit 1 + +set -- --set '/^space\./' drawing=off background.drawing=off icon.highlight=off +IFS=' +' +for sid in $occupied "$focused"; do + case "$sid" in [1-9]|magic) ;; *) continue ;; esac + set -- "$@" --set "space.$sid" drawing=on + if test "$sid" = "$focused"; then + set -- "$@" background.drawing=on icon.highlight=on + fi +done +sketchybar "$@" diff --git a/home/dot_config/sketchybar/status-items.swift b/home/dot_config/sketchybar/status-items.swift new file mode 100644 index 0000000..d342829 --- /dev/null +++ b/home/dot_config/sketchybar/status-items.swift @@ -0,0 +1,90 @@ +import AppKit +import ApplicationServices + +// macOS 26 hosts many status windows in ControlCenter as identical "Item-0". +// Match their geometry to each app's public AXExtrasMenuBar, not the host name. +// https://developer.apple.com/documentation/applicationservices/axuielement_h +func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? { + var value: CFTypeRef? + return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil +} + +struct StatusItem { + let bundle: String + let source: String + let window: String + let help: String + let element: AXUIElement +} + +func statusItems(bundle: String? = nil) -> [StatusItem] { + let windows = (CGWindowListCopyWindowInfo(.optionAll, 0) as? [[String: Any]] ?? []) + .filter { + $0[kCGWindowLayer as String] as? Int == Int(CGWindowLevelForKey(.statusWindow)) + && $0[kCGWindowOwnerName as String] as? String != "Window Server" + && $0[kCGWindowName as String] != nil + } + .sorted { bounds($0).minX > bounds($1).minX } + var result: [StatusItem] = [] + for app in NSWorkspace.shared.runningApplications { + guard let id = app.bundleIdentifier, app.processIdentifier != getpid(), + bundle == nil || bundle == id, + ["com.apple.TextInputMenuAgent", "eu.exelban.Stats", "com.steipete.codexbar"].contains(id) else { continue } + let root = AXUIElementCreateApplication(app.processIdentifier) + AXUIElementSetMessagingTimeout(root, 0.2) + guard let bar = attribute(root, kAXExtrasMenuBarAttribute) else { continue } + let elements = attribute(bar as! AXUIElement, kAXChildrenAttribute) as? [AXUIElement] ?? [] + for element in elements { + AXUIElementSetMessagingTimeout(element, 0.2) + guard let position = attribute(element, kAXPositionAttribute), + let size = attribute(element, kAXSizeAttribute) else { continue } + var origin = CGPoint.zero + var dimensions = CGSize.zero + guard AXValueGetValue(position as! AXValue, .cgPoint, &origin), + AXValueGetValue(size as! AXValue, .cgSize, &dimensions), + dimensions.width > 0, dimensions.height > 0 else { continue } + let center = CGPoint(x: origin.x + dimensions.width / 2, y: origin.y + dimensions.height / 2) + // Zero-size AX placeholders and non-status windows never match. + let matches = windows.enumerated().filter { bounds($0.element).contains(center) } + guard matches.count == 1, let match = matches.first, + let owner = match.element[kCGWindowOwnerName as String] as? String, + let name = match.element[kCGWindowName as String] as? String else { continue } + result.append(StatusItem(bundle: id, source: "\(owner),\(name)(\(match.offset + 1))", + window: name, help: attribute(element, kAXHelpAttribute) as? String ?? "", + element: element)) + } + } + return result +} + +func bounds(_ window: [String: Any]) -> CGRect { + guard let value = window[kCGWindowBounds as String] as? NSDictionary else { return .null } + return CGRect(dictionaryRepresentation: value) ?? .null +} + +let arguments = CommandLine.arguments +guard AXIsProcessTrusted() else { + fputs("sb-status-items: the launcher needs Accessibility permission.\n", stderr) + exit(1) +} +if arguments.count == 2 && arguments[1] == "list" { + let rows = statusItems().map { ["bundle": $0.bundle, "source": $0.source, "window": $0.window, "help": $0.help] } + do { + let data = try JSONSerialization.data(withJSONObject: rows, options: [.sortedKeys]) + print(String(decoding: data, as: UTF8.self)) + } catch { fputs("sb-status-items: \(error)\n", stderr); exit(1) } +} else if arguments.count == 4 && arguments[1] == "click" { + let matches = statusItems(bundle: arguments[2]).filter { $0.window == arguments[3] } + guard matches.count == 1, let item = matches.first else { + fputs("sb-status-items: missing or ambiguous status item; reload SketchyBar.\n", stderr) + exit(1) + } + let status = AXUIElementPerformAction(item.element, kAXPressAction as CFString) + guard status == .success else { + fputs("sb-status-items: native click failed (\(status.rawValue)).\n", stderr) + exit(1) + } +} else { + fputs("usage: sb-status-items list | click BUNDLE_ID WINDOW_NAME\n", stderr) + exit(64) +}