diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..e11b3a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,55 @@ +name: Bug report +description: Report a reproducible problem with a Macarchy component. +title: "bug: " +labels: + - bug +body: + - type: dropdown + id: component + attributes: + label: Component + options: + - Installer + - Window manager + - SketchyBar + - Terminal + - Shell + - Neovim + - Utilities + - Themes + - Raycast + validations: + required: true + + - type: input + id: macos + attributes: + label: macOS version and Mac model + placeholder: macOS 26.5, MacBook Pro with M3 Pro + validations: + required: true + + - type: textarea + id: problem + attributes: + label: What happened? + description: Include the command you ran, what you expected and what happened instead. + validations: + required: true + + - type: textarea + id: output + attributes: + label: Relevant output + description: Remove usernames, home-directory paths, tokens and other private data first. + render: shell + + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I used `./install --dry-run` where applicable. + required: true + - label: I removed personal information from the report. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..9e43899 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,21 @@ +name: Feature request +description: Suggest a focused improvement or a new optional component. +title: "feat: " +labels: + - enhancement +body: + - type: textarea + id: need + attributes: + label: What would this help you do? + description: Describe the use case before the proposed implementation. + validations: + required: true + + - type: textarea + id: scope + attributes: + label: Suggested scope + description: Explain whether this belongs in a default, an optional component or an example. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d5e1ac9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## What changed + +Describe the behaviour and the component it affects. + +## Checks + +- [ ] `./scripts/check.sh` passes +- [ ] No personal paths, accounts or app rules were added to defaults +- [ ] New dependencies and assets include their source and licence +- [ ] User-facing behaviour is documented diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..19c88ab --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: /raycast-theme + schedule: + interval: weekly + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1a6c588 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + pull_request: + push: + +permissions: + contents: read + +jobs: + check: + name: Repository checks (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-15 + + steps: + - name: Check out the repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: raycast-theme/package-lock.json + + - name: Install validation tools + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends jq zsh + + - name: Ensure jq is available + if: runner.os == 'macOS' + run: command -v jq >/dev/null || brew install jq + + - name: Run repository checks + run: ./scripts/check.sh diff --git a/.gitignore b/.gitignore index bfa1d7d..4282982 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,14 @@ *.bak # Runtime-generated files under installed dotfiles. -btop/themes/active-theme.theme +btop/themes/macarchy.theme raycast-*/node_modules/ raycast-*/.raycast/ raycast-*/dist/ themes/.backgrounds.json +themes/.current sketchybar/theme.sh sketchybar/.state/ +macarchy/generated/ yabai/.sketchybar_hidden yabai/float_state.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1b1006c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +Thanks for helping improve Macarchy. + +Keep changes focused on portable macOS configuration. Personal app rules, account switchers and machine-specific paths should be examples or local overrides rather than defaults. + +Before opening a pull request: + +```sh +./scripts/check.sh +``` + +The check covers shell syntax, installer behaviour, theme safety, personal-path scanning and the Raycast build. Do not run a real install or apply a theme as part of a test. + +For a new component, include a dry-run path, a safe uninstall path and a short guide. For a new theme, include its source, licence and wallpaper credits in `THIRD_PARTY_NOTICES.md`. + +Use clear commit prefixes such as `feat:`, `fix:`, `refactor:` and `docs:`. Write in plain language and explain behaviour that is not obvious from the code. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b1d0ec6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Macarchy contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index b1478b9..66f0ffe 100644 --- a/README.md +++ b/README.md @@ -1,220 +1,95 @@ # Macarchy -My personal macOS desktop setup, built around yabai, skhd, SketchyBar, Ghostty, Raycast, Neovim, zsh, btop, borders, and a few theme scripts. +A modular macOS desktop setup built around yabai, SketchyBar, Ghostty and a shared theme switcher. -Mostly config files, small scripts, and app settings for the way I like my Mac set up. +Macarchy used to be easiest to install as one large dotfiles bundle. That made it hard to tell what would change, and harder still to use just one part. The installer now works component by component, backs up files before replacing them and leaves services, themes and macOS security settings alone until you choose to enable them. -## Preview +## Start here -![Macarchy preview](./media/macarchy-preview.gif) +You need macOS, Git and [Homebrew](https://brew.sh). Clone the repository, open it in a terminal, then see what is available: -![Awakening](./media/macarchy-awakening.png) -![Lumon](./media/macarchy-lumon.png) - -## Components - -- `bin/`: Theme and wallpaper scripts. -- `themes/`: Theme files, wallpapers, and Ghostty palettes. -- `raycast-theme/`: Raycast theme switcher extension. -- `yabai/`: Window manager config and helper scripts. -- `skhd/`: Keyboard shortcuts. -- `sketchybar/`: Menu bar config, items, and plugins. -- `ghostty/`, `nvim/`, `zsh/`, `btop/`, `fastfetch/`, `neofetch/`, `borders/`: App configs. -- `launchagents/`: Launch agents for background services. - -## Install - -One-line install: - -```bash -brew install btop borders desktoppr fastfetch fish jq neofetch neovim ripgrep sketchybar skhd yabai && brew install --cask ghostty raycast && mkdir -p "$HOME/.local/bin" "$HOME/.config" "$HOME/.config/skhd" "$HOME/.config/zsh" "$HOME/Library/LaunchAgents" && rsync -a bin/ "$HOME/.local/bin/" && rsync -a borders/ "$HOME/.config/borders/" && rsync -a btop/ "$HOME/.config/btop/" && rsync -a fastfetch/ "$HOME/.config/fastfetch/" && rsync -a ghostty/ "$HOME/.config/ghostty/" && rsync -a neofetch/ "$HOME/.config/neofetch/" && rsync -a nvim/ "$HOME/.config/nvim/" && rsync -a sketchybar/ "$HOME/.config/sketchybar/" && rsync -a themes/ "$HOME/.config/themes/" && rsync -a zsh/ "$HOME/.config/zsh/" && printf 'export ZDOTDIR="$HOME/.config/zsh"\n[[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv"\n' > "$HOME/.zshenv" && rsync -a yabai/ "$HOME/.config/yabai/" && rsync -a skhd/skhdrc "$HOME/.config/skhd/skhdrc" && rsync -a launchagents/ "$HOME/Library/LaunchAgents/" && for service in com.asmvik.yabai com.koekeishiya.skhd homebrew.mxcl.sketchybar; do launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/$service.plist" 2>/dev/null || launchctl kickstart -k "gui/$(id -u)/$service"; done && "$HOME/.local/bin/theme-switch" "$(cat "$HOME/.config/themes/.current")" -``` - -Step by step: - -
-Tools - -```bash -brew install btop borders desktoppr fastfetch fish jq neofetch neovim ripgrep sketchybar skhd yabai -brew install --cask ghostty raycast -``` - -
- -
-Themes and scripts - -```bash -mkdir -p "$HOME/.local/bin" "$HOME/.config/themes" "$HOME/.config/zsh" -rsync -a bin/ "$HOME/.local/bin/" -rsync -a themes/ "$HOME/.config/themes/" -rsync -a zsh/ "$HOME/.config/zsh/" -printf 'export ZDOTDIR="$HOME/.config/zsh"\n[[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv"\n' > "$HOME/.zshenv" -``` - -
- -
-Window management - -```bash -mkdir -p "$HOME/.config" "$HOME/.config/skhd" -rsync -a borders/ "$HOME/.config/borders/" -rsync -a yabai/ "$HOME/.config/yabai/" -rsync -a skhd/skhdrc "$HOME/.config/skhd/skhdrc" -``` - -
- -
-SketchyBar - -```bash -mkdir -p "$HOME/.config" -rsync -a sketchybar/ "$HOME/.config/sketchybar/" +```sh +cd macarchy +./install --list ``` -
- -
-App configs +Preview an install without changing anything: -```bash -mkdir -p "$HOME/.config" -rsync -a btop/ "$HOME/.config/btop/" -rsync -a fastfetch/ "$HOME/.config/fastfetch/" -rsync -a ghostty/ "$HOME/.config/ghostty/" -rsync -a neofetch/ "$HOME/.config/neofetch/" -rsync -a nvim/ "$HOME/.config/nvim/" +```sh +./install --dry-run terminal shell ``` -
- -
-Launch agents +Then install only the parts you want: -```bash -mkdir -p "$HOME/Library/LaunchAgents" -rsync -a launchagents/ "$HOME/Library/LaunchAgents/" - -for service in com.asmvik.yabai com.koekeishiya.skhd homebrew.mxcl.sketchybar; do - launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/$service.plist" 2>/dev/null || launchctl kickstart -k "gui/$(id -u)/$service" -done +```sh +./install terminal shell +./install window-manager sketchybar +./install themes raycast ``` -
- -
-Apply the current theme +`./install all` is available, but it is not the recommended starting point. The window manager and bar both need a little macOS setup after their files are installed. -```bash -"$HOME/.local/bin/theme-switch" "$(cat "$HOME/.config/themes/.current")" -``` +## Components -
+| Component | What it installs | +| --- | --- | +| `window-manager` | yabai, skhd and JankyBorders configs | +| `sketchybar` | The bar, app labels, network speed and optional Apple Silicon metrics | +| `terminal` | A small Ghostty config with a separate generated theme file | +| `shell` | A portable Zsh setup and prompt | +| `nvim` | LazyVim, theme integration and a small set of colour schemes | +| `utilities` | Minimal btop and fastfetch configs | +| `themes` | Theme packs, `theme-switch` and the wallpaper helper | +| `raycast` | Source and tooling for the Raycast theme picker | -
-Just sync configs +The installer prints the next step for each selected component. It never disables SIP, writes a sudoers file, starts a background service or applies a theme on its own. -```bash -mkdir -p "$HOME/.local/bin" "$HOME/.config" "$HOME/.config/skhd" "$HOME/.config/zsh" && rsync -a bin/ "$HOME/.local/bin/" && rsync -a borders/ "$HOME/.config/borders/" && rsync -a btop/ "$HOME/.config/btop/" && rsync -a fastfetch/ "$HOME/.config/fastfetch/" && rsync -a ghostty/ "$HOME/.config/ghostty/" && rsync -a neofetch/ "$HOME/.config/neofetch/" && rsync -a nvim/ "$HOME/.config/nvim/" && rsync -a sketchybar/ "$HOME/.config/sketchybar/" && rsync -a themes/ "$HOME/.config/themes/" && rsync -a zsh/ "$HOME/.config/zsh/" && printf 'export ZDOTDIR="$HOME/.config/zsh"\n[[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv"\n' > "$HOME/.zshenv" && rsync -a yabai/ "$HOME/.config/yabai/" && rsync -a skhd/skhdrc "$HOME/.config/skhd/skhdrc" -``` +Raycast development source is installed under `${XDG_DATA_HOME:-~/.local/share}/macarchy/raycast-theme`. Raycast's directory under `~/.config/raycast/extensions` is generated runtime output and should never contain `node_modules`. -
+The `shell` component changes `ZDOTDIR` and backs up `~/.zshenv`. Existing `~/.zprofile` and `~/.zshrc` files stay on disk but are no longer loaded automatically. Review the [shell notes](./docs/install.md#what-happens-to-existing-files) before installing it. -## Raycast +## After installation -One-line setup: +Check what Macarchy manages: -```bash -mkdir -p "$HOME/.config/raycast/extensions" && rsync -a raycast-theme/ "$HOME/.config/raycast/extensions/theme-switcher/" && cd "$HOME/.config/raycast/extensions/theme-switcher" && ray develop +```sh +./install status ``` -Step by step: - -
-Install the extension - -```bash -mkdir -p "$HOME/.config/raycast/extensions" -rsync -a raycast-theme/ "$HOME/.config/raycast/extensions/theme-switcher/" +Apply a theme explicitly: -cd "$HOME/.config/raycast/extensions/theme-switcher" -ray develop +```sh +"$HOME/.local/bin/theme-switch" --list +"$HOME/.local/bin/theme-switch" carbonfox ``` -
+That is the default install path. The `shell` component adds `~/.local/bin` to `PATH`; a themes-only install does not, so the full path works in either case. -The extension reads themes from `~/.config/themes`, previews the active wallpaper, and calls `~/.local/bin/theme-switch`. The shell script remains the source of truth. +Update by pulling the repository and rerunning the components you use. Existing files are backed up under `${XDG_STATE_HOME:-~/.local/state}/macarchy/backups`. -## Key Bindings +To remove a component and restore the files that were present before Macarchy: -Modifier symbols: `⌘` Command, `⌃` Control, `⇧` Shift. Space numbers use `1` through `9`, with `0` for space 10. - -| Shortcut | Action | -| --- | --- | -| `⌃↩` | Open Ghostty. | -| `⌘Q` | Quit the current app. In Chrome, close the current profile window instead. | -| `⌘W` | In Messages, TablePlus, and Obsidian, quit the app instead of closing a window. | -| Media keys | Control Spotify, but only when Spotify is already running. | -| `⌃F` | Toggle fullscreen for the focused window. | -| `⌘⇧B` or `⌃⇧B` | Open the Firefox profile manager. | -| `⌘⇧F` | Open the home folder in Finder. | -| `⌘←/↓/↑/→` or `⌃←/↓/↑/→` | Focus the window in that direction. | -| `⌘⇧←/↓/↑/→` or `⌃⇧←/↓/↑/→` | Swap the focused window in that direction, then focus it. | -| `⌘1..0` or `⌃1..0` | Switch to a space. | -| `⌘⌃1..0` | Swap the current space with another space. | -| `⌘⇧1..0` or `⌃⇧1..0` | Move the focused window to a space and follow it. | -| `⌃T` | Toggle floating or tiled mode for the focused window and remember it for that app. | -| `⌃J` | Toggle the split direction. | -| `⌃L` | Rotate and rebalance the current layout. | -| `⌃=` / `⌃-` | Adjust the focused window split ratio. | -| `⌃⇧S` | Take an interactive screenshot to the clipboard. | -| `⌘⌃=` or `⌘⌃⇧=` | Switch to the next Macarchy theme. | -| `⌘⌃-` | Switch to the previous Macarchy theme. | - -
-SIP and yabai - -This setup assumes Apple Silicon with the `yabai` scripting addition and this boot arg: - -```bash -sudo nvram boot-args=-arm64e_preview_abi -``` - -The working SIP profile is partial SIP with filesystem, debug, and NVRAM protections disabled: - -```bash -csrutil enable --without fs --without debug --without nvram +```sh +./install uninstall terminal shell ``` -A full replication sequence for a new Mac: +Modified files are kept rather than deleted. Homebrew packages and empty directories are also left in place. -1. Boot to Recovery. -2. In Startup Security Utility, choose Reduced Security and allow user management of kernel extensions. -3. In Recovery Terminal: +## Repository shape -```bash -csrutil disable -``` - -4. Reboot to macOS. -5. Set the yabai boot arg: +The configs, installer and Raycast picker stay together because they share one small theme contract. Selective installation now provides the useful separation without making people clone several repositories. -```bash -sudo nvram boot-args=-arm64e_preview_abi -``` +Wallpaper-heavy theme packs should be separate repositories. The old wallpapers have been removed from the current tree, but their blobs remain in Git history. Publishing a completely clean core would therefore mean creating a new snapshot repository or explicitly approving a history rewrite. The existing history has not been rewritten. -6. Reboot to Recovery. -7. Re-enable the intended partial SIP profile: - -```bash -csrutil enable --without fs --without debug --without nvram -``` +## Guides -8. Reboot to macOS. +- [Installation, updates and backups](./docs/install.md) +- [Window management and shortcuts](./docs/window-management.md) +- [SketchyBar and system metrics](./docs/sketchybar.md) +- [Themes and custom theme packs](./docs/themes.md) +- [Raycast theme picker](./raycast-theme/README.md) +- [Troubleshooting](./docs/troubleshooting.md) -`csrutil status` will show `unknown (Custom Configuration)`. That is expected for this profile. +Macarchy's original code is MIT licensed. The LazyVim starter files, adapted palettes and app icon map retain their upstream licences; read [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) before redistributing them. -
+Contributions are welcome. Please read [CONTRIBUTING.md](./CONTRIBUTING.md) and run `./scripts/check.sh` before opening a pull request. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c9183cd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security + +Please do not open a public issue for a vulnerability that could expose files, execute untrusted theme content or weaken a user's macOS security settings. + +Use GitHub's private security advisory form for this repository instead. Include the affected component, a minimal reproduction and the impact you observed. + +Macarchy never needs account credentials. Do not include tokens, usernames, home-directory paths or other personal data in a report. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..e7e168c --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,73 @@ +# Third-party notices + +Macarchy's root MIT licence covers the original installer, scripts and configuration adapters. It does not replace the licences of the third-party material listed here. + +No third-party wallpapers are included in the current tree. Earlier versions contained wallpaper and preview files with incomplete or restrictive provenance. Their blobs still exist in Git history, so a clean new snapshot or a separately approved history rewrite is required before describing the repository history as sanitised. + +## LazyVim starter + +Parts of the Neovim configuration are derived from the [LazyVim starter at commit `803bc181d7c0d6d5eeba9274d9be49b287294d99`](https://github.com/LazyVim/starter/tree/803bc181d7c0d6d5eeba9274d9be49b287294d99). + +The bootstrap files remain close to the upstream starter, while Macarchy adds its own theme state, transparent-background handling and dashboard behaviour. LazyVim starter is licensed under the Apache License 2.0. A copy is included at [`LICENSES/Apache-2.0.txt`](./LICENSES/Apache-2.0.txt). + +## Theme palettes + +### Blackgold + +Adapted from [HANCORE's Blackgold theme](https://github.com/HANCORE-linux/omarchy-blackgold-theme/tree/48361b9593fd3e51769a8350025fb454f0de141b). + +Copyright (c) 2025 HANCORE. Licensed under the MIT licence below. + +### Carbonfox + +Adapted from [Carbonfox](https://github.com/gchmel/carbonfox/tree/c2200fff57b6d6f005d1152feb8067ee4b1da97e) and the [Nightfox Neovim theme](https://github.com/EdenEast/nightfox.nvim/tree/4dacd3f0185a2227bdf3b6c0975a8f0bf87cac9a). + +Copyright (c) 2026 Georgy Chmel. + +Copyright (c) 2021 James Simpson. + +Both sources are licensed under the MIT licence below. + +### Cool Blue and Matte Black + +Adapted from the Lumon and Matte Black palettes in [Basecamp's Omarchy](https://github.com/basecamp/omarchy/tree/9cf1852525a5f7de26d3162db9d61e2f5c1d5523/themes). + +Copyright (c) David Heinemeier Hansson. Licensed under the MIT licence below. + +Cool Blue is a renamed colour adapter. Macarchy is not affiliated with Apple or the Severance television series. + +### Amber Metal + +Adapted from [HANCORE's Turbonite theme](https://github.com/HANCORE-linux/omarchy-turbonite-theme/tree/921aea335619bbfff187403dadca2bd75ee364f8). + +Copyright (c) 2026 HANCORE. Licensed under the MIT licence below. + +Amber Metal is a renamed colour adapter. Macarchy is not affiliated with Porsche or Porsche Design. + +### MIT licence text for the palette sources + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## SketchyBar application mappings + +[`sketchybar/plugins/icon_map.sh`](./sketchybar/plugins/icon_map.sh) is generated from or derived from [sketchybar-app-font at commit `11b080e4211038114746c8423abac7557e0b7a86`](https://github.com/kvndrsslr/sketchybar-app-font/tree/11b080e4211038114746c8423abac7557e0b7a86), which is released under [CC0 1.0 Universal](https://github.com/kvndrsslr/sketchybar-app-font/blob/11b080e4211038114746c8423abac7557e0b7a86/LICENSE). + +The font itself is installed separately through Homebrew and is not copied into this repository. diff --git a/bin/set-wallpaper b/bin/set-wallpaper index 12e3406..648dd46 100755 --- a/bin/set-wallpaper +++ b/bin/set-wallpaper @@ -6,17 +6,19 @@ if [ "$#" -ne 1 ]; then exit 1 fi -IMG="$(python3 - <<'PY' "$1" -import pathlib, sys -print(pathlib.Path(sys.argv[1]).expanduser().resolve()) -PY -)" - -if [ ! -f "$IMG" ]; then - echo "image not found: $IMG" >&2 +input="$1" +case "$input" in + '~') input="$HOME" ;; + '~/'*) input="$HOME/${input#'~/'}" ;; +esac + +if [ ! -f "$input" ]; then + echo "image not found: $input" >&2 exit 1 fi +IMG="$(cd "$(dirname "$input")" && pwd -P)/$(basename "$input")" + WALLPAPER_INDEX="$HOME/Library/Application Support/com.apple.wallpaper/Store/Index.plist" desktoppr_bin() { @@ -98,7 +100,8 @@ def desktop_choice(): def update_desktop(node): if not isinstance(node, dict): return - # Update existing Linked entry (used by Spaces that follow All-Spaces wallpaper) + # Update an existing Linked entry used by Spaces that follow the + # all-Spaces wallpaper. linked = node.get("Linked") if isinstance(linked, dict): content = linked.setdefault("Content", {}) @@ -153,7 +156,7 @@ PY nudge_wallpaper_agent() { # Re-kicks WallpaperAgent so it re-reads Index.plist and applies the new - # image to every Space. Importantly, we do NOT touch Dock — restarting Dock + # image to every Space. Importantly, we do not touch Dock, as restarting Dock # is what caused the old black flash. WallpaperAgent on its own is silent. /bin/launchctl kickstart -k "gui/$(id -u)/com.apple.wallpaper.agent" >/dev/null 2>&1 || true } @@ -168,13 +171,13 @@ fi # 2) Plist edit: rewrites Index.plist for every Space / display so when the # user switches Spaces those windows already show the new wallpaper. -# Always run alongside desktoppr — they cover different surfaces. +# Always run alongside desktoppr because they cover different surfaces. if set_with_wallpaper_store; then applied=1 nudge_wallpaper_agent fi -# Fallbacks only if both of the above failed (rare — usually missing tools). +# Fallbacks only if both of the above failed, usually because tools are missing. if [ "$applied" -ne 1 ] && set_with_finder; then applied=1 fi diff --git a/bin/theme-switch b/bin/theme-switch index dfaf413..805e8fb 100755 --- a/bin/theme-switch +++ b/bin/theme-switch @@ -1,1005 +1,100 @@ -#!/bin/bash -set -euo pipefail - -export USER="${USER:-$(id -un)}" -export HOME="${HOME:-$(cd ~"$USER" && pwd)}" - -THEMES_DIR="$HOME/.config/themes" -THEME_BACKGROUND_STATE="$THEMES_DIR/.backgrounds.json" -GHOSTTY_CONFIG="$HOME/.config/ghostty/config" -BTOP_CONFIG="$HOME/.config/btop/btop.conf" -BTOP_THEME_DIR="$HOME/.config/btop/themes" -BTOP_THEME_NAME="active-theme" -RAYCAST_BUNDLE_ID="com.raycast.macos" -FIREFOX_DIR="$HOME/Library/Application Support/Firefox" -BORDERS_BIN="/opt/homebrew/opt/borders/bin/borders" -BORDERS_CONFIG="$HOME/.config/borders/bordersrc" - -is_valid_theme_background() { - local theme_dir="$1" - local background="$2" - - [ -n "$background" ] || return 1 - case "$background" in - /*|*../*|../*|*"/.."*) - return 1 - ;; - esac - - [ -f "$theme_dir/$background" ] -} - -save_selected_background() { - local theme="$1" - local background="$2" - local tmp - - mkdir -p "$THEMES_DIR" - tmp="$(mktemp)" - if [ -f "$THEME_BACKGROUND_STATE" ]; then - cp "$THEME_BACKGROUND_STATE" "$tmp" - else - printf '{}\n' > "$tmp" - fi - - jq --arg theme "$theme" --arg background "$background" \ - '.[$theme] = $background' "$tmp" > "$tmp.next" - mv "$tmp.next" "$THEME_BACKGROUND_STATE" - rm -f "$tmp" -} - -list_themes() { - find "$THEMES_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec basename {} \; | LC_ALL=C sort -} - -cycle_theme() { - local direction="$1" - local current target i - local -a themes=() - - while IFS= read -r theme; do - themes+=("$theme") - done < <(list_themes) - - if [ "${#themes[@]}" -eq 0 ]; then - echo "Error: no themes found in $THEMES_DIR" - exit 1 - fi - - current="$(cat "$THEMES_DIR/.current" 2>/dev/null || true)" - - for i in "${!themes[@]}"; do - if [ "${themes[$i]}" = "$current" ]; then - case "$direction" in - next) - target="${themes[$(((i + 1) % ${#themes[@]}))]}" - ;; - prev) - target="${themes[$(((i - 1 + ${#themes[@]}) % ${#themes[@]}))]}" - ;; - *) - echo "Error: invalid cycle direction '$direction'" - exit 1 - ;; - esac - - exec "$0" "$target" - fi - done - - if [ "$direction" = "prev" ]; then - target="${themes[$((${#themes[@]} - 1))]}" - else - target="${themes[0]}" - fi - - exec "$0" "$target" -} - -# --- Background-only mode --- -if [ "${1:-}" = "--background" ]; then - THEME="${2:-}" - WALLPAPER_CHOICE="${3:-}" - THEME_DIR="$THEMES_DIR/$THEME" - - if [ ! -d "$THEME_DIR" ]; then - echo "Error: theme '$THEME' not found in $THEMES_DIR" - exit 1 - fi - - if ! is_valid_theme_background "$THEME_DIR" "$WALLPAPER_CHOICE"; then - echo "Error: invalid background '$WALLPAPER_CHOICE' for theme '$THEME'" - exit 1 - fi - - save_selected_background "$THEME" "$WALLPAPER_CHOICE" - - if [ "$(cat "$THEMES_DIR/.current" 2>/dev/null || true)" = "$THEME" ]; then - "$HOME/.local/bin/set-wallpaper" "$THEME_DIR/$WALLPAPER_CHOICE" >/tmp/theme-switch-wallpaper.log 2>&1 & - echo "Wallpaper queued: $THEME/$WALLPAPER_CHOICE" - else - echo "Wallpaper default saved: $THEME/$WALLPAPER_CHOICE" - fi - exit 0 -fi - -# --- Cycle mode --- -if [ "${1:-}" = "next" ] || [ "${1:-}" = "prev" ]; then - cycle_theme "$1" -fi - -# --- Usage --- -if [ -z "${1:-}" ]; then - echo "Usage: theme-switch |next|prev" - echo "Available themes:" - list_themes - exit 1 -fi - -THEME="$1" -THEME_DIR="$THEMES_DIR/$THEME" - -if [ ! -d "$THEME_DIR" ]; then - echo "Error: theme '$THEME' not found in $THEMES_DIR" - exit 1 +#!/usr/bin/env bash +set -u + +MACARCHY_BIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_lib="$(cd "$MACARCHY_BIN_DIR/.." && pwd)/lib/macarchy" +installed_lib="${XDG_DATA_HOME:-$HOME/.local/share}/macarchy/lib" +MACARCHY_LIB_DIR="${MACARCHY_LIB_DIR:-}" + +if [[ -z "$MACARCHY_LIB_DIR" && -d "$repo_lib" ]]; then + MACARCHY_LIB_DIR="$repo_lib" +elif [[ -z "$MACARCHY_LIB_DIR" && -d "$installed_lib" ]]; then + MACARCHY_LIB_DIR="$installed_lib" fi -if [ ! -f "$THEME_DIR/theme.env" ]; then - echo "Error: no theme.env in $THEME_DIR" +if [[ -z "$MACARCHY_LIB_DIR" || ! -d "$MACARCHY_LIB_DIR" ]]; then + printf 'error: Macarchy libraries were not found\n' >&2 exit 1 fi # shellcheck source=/dev/null -source "$THEME_DIR/theme.env" - -# Save current theme early so repeated next/prev presses advance immediately -# instead of waiting for the full apply pipeline to finish. -echo "$THEME" > "$THEMES_DIR/.current" - -get_selected_wallpaper() { - local selected="" - - if [ -f "$THEME_BACKGROUND_STATE" ]; then - selected="$(jq -r --arg theme "$THEME" '.[$theme] // empty' "$THEME_BACKGROUND_STATE" 2>/dev/null || true)" - if is_valid_theme_background "$THEME_DIR" "$selected"; then - printf '%s\n' "$selected" - return - fi - fi - - printf '%s\n' "${WALLPAPER:-}" -} - -SELECTED_WALLPAPER="$(get_selected_wallpaper)" - -ghostty_color() { - local key="$1" - local fallback="$2" - local value="" - - if [ -f "$THEME_DIR/ghostty.conf" ]; then - value="$(grep -E "^${key}[[:space:]]*=[[:space:]]*#[0-9A-Fa-f]{6}$" "$THEME_DIR/ghostty.conf" | sed -E "s/^${key}[[:space:]]*=[[:space:]]*//" | head -n 1 || true)" - fi - - if [ -n "$value" ]; then - printf '%s\n' "$value" - else - printf '%s\n' "$fallback" - fi -} - -ghostty_palette() { - local index="$1" - local fallback="$2" - local value="" - - if [ -f "$THEME_DIR/ghostty.conf" ]; then - value="$(grep -E "^palette[[:space:]]*=[[:space:]]*$index=#[0-9A-Fa-f]{6}$" "$THEME_DIR/ghostty.conf" | sed -E 's/^palette[[:space:]]*=[[:space:]]*[0-9]+=//' | head -n 1 || true)" - fi - - if [ -n "$value" ]; then - printf '%s\n' "$value" - else - printf '%s\n' "$fallback" - fi -} - -to_macos_color() { - local hex="$1" - if [[ "$hex" =~ ^#[0-9A-Fa-f]{6}$ ]]; then - printf '0xff%s\n' "${hex#\#}" - else - printf '%s\n' "$hex" - fi -} - -normalize_hex_color() { - local color="${1:-}" - - if [[ "$color" =~ ^#[0-9A-Fa-f]{6}$ ]]; then - printf '#%s\n' "${color#\#}" - return 0 - fi - - if [[ "$color" =~ ^0[xX][0-9A-Fa-f]{8}$ ]]; then - printf '#%s\n' "${color:4:6}" - return 0 - fi - - if [[ "$color" =~ ^0[xX][0-9A-Fa-f]{6}$ ]]; then - printf '#%s\n' "${color:2:6}" - return 0 - fi - - if [[ "$color" =~ ^[0-9A-Fa-f]{6}$ ]]; then - printf '#%s\n' "$color" - return 0 - fi - - return 1 -} - -hex_channel_to_decimal() { - printf '%d\n' "0x$1" -} - -hex_channel_to_float() { - awk -v channel="$1" 'BEGIN { printf "%.6f", channel / 255 }' -} - -hex_to_apple_highlight_color() { - local hex="$1" - local r g b - - r="$(hex_channel_to_decimal "${hex:1:2}")" - g="$(hex_channel_to_decimal "${hex:3:2}")" - b="$(hex_channel_to_decimal "${hex:5:2}")" - - printf '%s %s %s Custom\n' \ - "$(hex_channel_to_float "$r")" \ - "$(hex_channel_to_float "$g")" \ - "$(hex_channel_to_float "$b")" -} - -apple_accent_name_for_id() { - case "$1" in - -1) printf 'Multicolour\n' ;; - 0) printf 'Red\n' ;; - 1) printf 'Orange\n' ;; - 2) printf 'Yellow\n' ;; - 3) printf 'Green\n' ;; - 4) printf 'Blue\n' ;; - 5) printf 'Purple\n' ;; - 6) printf 'Pink\n' ;; - 7) printf 'Graphite\n' ;; - *) return 1 ;; - esac -} - -resolve_apple_accent_id() { - local accent="${1:-}" - local key - - if [[ "$accent" =~ ^-?[0-9]+$ ]] && [ "$accent" -ge -1 ] && [ "$accent" -le 7 ]; then - printf '%s\n' "$accent" - return 0 - fi - - key="$(printf '%s' "$accent" | tr '[:upper:]' '[:lower:]')" - key="${key// /}" - key="${key//-/}" - key="${key//_/}" - - case "$key" in - multicolor|multicolour) printf -- '-1\n' ;; - red) printf '0\n' ;; - orange) printf '1\n' ;; - yellow) printf '2\n' ;; - green) printf '3\n' ;; - blue) printf '4\n' ;; - purple) printf '5\n' ;; - pink) printf '6\n' ;; - graphite|grey|gray) printf '7\n' ;; - *) return 1 ;; - esac -} - -nearest_apple_accent_id() { - local hex="$1" - local r g b max min chroma hue - - r="$(hex_channel_to_decimal "${hex:1:2}")" - g="$(hex_channel_to_decimal "${hex:3:2}")" - b="$(hex_channel_to_decimal "${hex:5:2}")" - - max="$r" - [ "$g" -gt "$max" ] && max="$g" - [ "$b" -gt "$max" ] && max="$b" - min="$r" - [ "$g" -lt "$min" ] && min="$g" - [ "$b" -lt "$min" ] && min="$b" - chroma=$((max - min)) - - if [ "$chroma" -lt 24 ]; then - printf '7\n' - return - fi - - if [ "$max" -eq "$r" ]; then - hue=$((60 * (g - b) / chroma)) - [ "$hue" -lt 0 ] && hue=$((hue + 360)) - elif [ "$max" -eq "$g" ]; then - hue=$((120 + 60 * (b - r) / chroma)) - else - hue=$((240 + 60 * (r - g) / chroma)) - fi - - if [ "$hue" -lt 15 ] || [ "$hue" -ge 345 ]; then - printf '0\n' - elif [ "$hue" -lt 40 ]; then - printf '1\n' - elif [ "$hue" -lt 75 ]; then - printf '2\n' - elif [ "$hue" -lt 165 ]; then - printf '3\n' - elif [ "$hue" -lt 255 ]; then - printf '4\n' - elif [ "$hue" -lt 290 ]; then - printf '5\n' - else - printf '6\n' - fi -} - -get_theme_accent_hex() { - local accent="" - - if [ -n "${SYSTEM_ACCENT_COLOR:-}" ]; then - accent="$SYSTEM_ACCENT_COLOR" - elif [ -n "${FIREFOX_ACCENT:-}" ]; then - accent="$FIREFOX_ACCENT" - elif [ -n "${BORDER_ACTIVE:-}" ]; then - accent="$BORDER_ACTIVE" - else - accent="$(ghostty_palette 4 "$(ghostty_color "foreground" "#ffffff")")" - fi - - normalize_hex_color "$accent" -} - -flush_macos_preferences() { - return 0 -} - -keep_macos_accent_and_highlight_static() { - echo " macOS accent/highlight: unchanged" -} - -get_sketchybar_text_color() { - if [ -n "${SKETCHYBAR_TEXT_COLOR:-}" ]; then - printf '%s\n' "$SKETCHYBAR_TEXT_COLOR" - return - fi - - if [ "${DARK_MODE:-}" = "false" ]; then - to_macos_color "$(ghostty_color "foreground" "#000000")" - else - to_macos_color "$(ghostty_color "foreground" "#ffffff")" - fi -} - -get_sketchybar_accent_color() { - if [ -n "${BORDER_ACTIVE:-}" ]; then - printf '%s\n' "$BORDER_ACTIVE" - else - to_macos_color "$(ghostty_palette 4 "$(ghostty_color "foreground" "#ffffff")")" - fi -} - -get_sketchybar_bar_color() { - if [ -n "${SKETCHYBAR_BAR_COLOR:-}" ]; then - printf '%s\n' "$SKETCHYBAR_BAR_COLOR" - else - printf '0x00000000\n' - fi -} - -get_border_inactive_color() { - printf '%s\n' "${BORDER_INACTIVE:-0x00000000}" -} - -get_borders_blacklist() { - local blacklist="${BORDERS_BLACKLIST:-}" - - case ",$blacklist," in - *",iPhone Mirroring,"*) ;; - ",,") blacklist="iPhone Mirroring" ;; - *) blacklist="$blacklist,iPhone Mirroring" ;; - esac - - printf '%s\n' "$blacklist" -} - -write_borders_config() { - mkdir -p "$(dirname "$BORDERS_CONFIG")" - - cat > "$BORDERS_CONFIG" <<'EOF' -#!/bin/bash -set -euo pipefail - -BORDERS_BIN="/opt/homebrew/opt/borders/bin/borders" -THEMES_DIR="$HOME/.config/themes" -CURRENT_THEME="$(cat "$THEMES_DIR/.current" 2>/dev/null || true)" - -if [ -n "$CURRENT_THEME" ] && [ -f "$THEMES_DIR/$CURRENT_THEME/theme.env" ]; then - # shellcheck source=/dev/null - source "$THEMES_DIR/$CURRENT_THEME/theme.env" -fi - -BORDER_ACTIVE="${BORDER_ACTIVE:-0xff45e8ff}" -BORDER_INACTIVE="${BORDER_INACTIVE:-0x00000000}" -BORDER_WIDTH="${BORDER_WIDTH:-6.0}" -BORDERS_VISIBLE="${BORDERS_VISIBLE:-true}" -BORDERS_BLACKLIST="${BORDERS_BLACKLIST:-}" - -case ",$BORDERS_BLACKLIST," in - *",iPhone Mirroring,"*) ;; - ",,") BORDERS_BLACKLIST="iPhone Mirroring" ;; - *) BORDERS_BLACKLIST="$BORDERS_BLACKLIST,iPhone Mirroring" ;; -esac - -if [ "$BORDERS_VISIBLE" != "true" ]; then - BORDER_ACTIVE="0x00000000" - BORDER_INACTIVE="0x00000000" - BORDER_WIDTH="0.0" -fi - -if [ -n "${BORDER_ANIMATION_COLORS:-}" ]; then - read -r -a BORDER_COLORS <<< "$BORDER_ANIMATION_COLORS" - FIRST="${BORDER_COLORS[0]}" - SECOND="${BORDER_COLORS[3]:-${BORDER_COLORS[1]:-$FIRST}}" - - if [ "${BORDER_ANIMATION_EFFECT:-glow}" = "glow" ]; then - ACTIVE_ARG="active_color=glow($FIRST)" - else - ACTIVE_ARG="active_color=gradient(top_left=$FIRST,bottom_right=$SECOND)" - fi -else - ACTIVE_ARG="active_color=glow($BORDER_ACTIVE)" -fi - -exec "$BORDERS_BIN" \ - style=round \ - width="$BORDER_WIDTH" \ - hidpi=on \ - ax_focus=on \ - "$ACTIVE_ARG" \ - inactive_color="$BORDER_INACTIVE" \ - blacklist="$BORDERS_BLACKLIST" -EOF - - chmod +x "$BORDERS_CONFIG" -} - -restart_borders_service() { - local service="gui/$(id -u)/homebrew.mxcl.borders" - - if /bin/launchctl print "$service" >/dev/null 2>&1; then - /bin/launchctl kickstart -k "$service" >/dev/null 2>&1 || true - else - "$BORDERS_CONFIG" >/dev/null 2>&1 & - fi -} - -apply_borders() { - local inactive width blacklist active_arg first second - - inactive="$(get_border_inactive_color)" - width="${BORDER_WIDTH:-6.0}" - blacklist="$(get_borders_blacklist)" - - write_borders_config - - if [ "${BORDERS_VISIBLE:-true}" != "true" ]; then - "$BORDERS_BIN" \ - style=round \ - width=0.0 \ - hidpi=on \ - ax_focus=on \ - active_color=0x00000000 \ - inactive_color=0x00000000 \ - blacklist="$blacklist" >/dev/null 2>&1 || \ - restart_borders_service - echo " Borders: hidden" - return 0 - fi - - if [ -n "${BORDER_ANIMATION_COLORS:-}" ]; then - read -r -a BORDER_COLORS <<< "$BORDER_ANIMATION_COLORS" - first="${BORDER_COLORS[0]}" - second="${BORDER_COLORS[3]:-${BORDER_COLORS[1]:-$first}}" - - if [ "${BORDER_ANIMATION_EFFECT:-glow}" = "glow" ]; then - active_arg="active_color=glow(${first})" - echo " Borders: glow (${first})" - else - active_arg="active_color=gradient(top_left=${first},bottom_right=${second})" - echo " Borders: gradient" - fi - else - active_arg="active_color=glow(${BORDER_ACTIVE})" - echo " Borders: glow (${BORDER_ACTIVE})" - fi - - "$BORDERS_BIN" \ - style=round \ - width="$width" \ - hidpi=on \ - ax_focus=on \ - "$active_arg" \ - inactive_color="$inactive" \ - blacklist="$blacklist" >/dev/null 2>&1 || \ - restart_borders_service -} - -get_raycast_theme_id() { - if [ "${DARK_MODE:-}" = "true" ]; then - printf 'bundled-raycast-dark\n' - else - printf 'bundled-raycast-light\n' - fi -} - -map_btop_theme() { - printf '%s\n' "$BTOP_THEME_NAME" -} - -write_btop_theme() { - mkdir -p "$BTOP_THEME_DIR" - - local bg fg selection accent muted green yellow red cyan purple orange - bg="$(ghostty_color "background" "#000000")" - fg="$(ghostty_color "foreground" "#ffffff")" - selection="$(ghostty_color "selection-background" "$bg")" - accent="$(ghostty_palette 4 "$fg")" - muted="$(ghostty_palette 8 "$fg")" - green="$(ghostty_palette 2 "$accent")" - yellow="$(ghostty_palette 3 "$accent")" - red="$(ghostty_palette 1 "$accent")" - cyan="$(ghostty_palette 6 "$accent")" - purple="$(ghostty_palette 5 "$accent")" - orange="$(ghostty_palette 11 "$yellow")" - - cat > "$BTOP_THEME_DIR/$BTOP_THEME_NAME.theme" < + theme-switch next|prev + theme-switch --background EOF } -reload_btop_instances() { - if pkill -USR2 -x btop 2>/dev/null; then - return 0 - fi - - local found=1 - while IFS= read -r tty; do - [ -n "$tty" ] || continue - [ "$tty" = "??" ] && continue - if [ -w "/dev/$tty" ]; then - printf '\022' > "/dev/$tty" || true - found=0 - fi - done < <(ps -axo tty=,comm= 2>/dev/null | awk '$2 ~ /(^|\/)btop$/ { print $1 }' | sort -u) +apply_theme() { + local name="$1" + local background failures=0 - return $found -} + macarchy_theme_load "$name" || return 1 + background="$(macarchy_theme_selected_background)" -# Cursor Agent (and similar TUIs) keep resolved colors until they redraw. Ghostty -# reload does not restart those processes. SIGWINCH often triggers a full -# re-layout; if paste/overlay colors are still wrong, restart the agent session. -reload_cursor_agent_instances() { - local pid count=0 - while IFS= read -r pid; do - [ -n "$pid" ] || continue - if kill -WINCH "$pid" 2>/dev/null; then - count=$((count + 1)) - fi - done < <(pgrep -f '[/]cursor-agent' 2>/dev/null || true) - if [ "$count" -gt 0 ]; then - echo " cursor-agent: SIGWINCH to $count process(es) (TUI redraw hint)" - fi -} + macarchy_info "Applying theme: $name" + macarchy_apply_appearance || failures=$((failures + 1)) + macarchy_apply_raycast || failures=$((failures + 1)) + macarchy_apply_wallpaper "$background" || failures=$((failures + 1)) + macarchy_apply_ghostty || failures=$((failures + 1)) + macarchy_apply_neovim || failures=$((failures + 1)) + macarchy_apply_borders || failures=$((failures + 1)) + macarchy_apply_btop || failures=$((failures + 1)) + macarchy_apply_sketchybar || failures=$((failures + 1)) + macarchy_apply_shell || failures=$((failures + 1)) -stop_border_animation() { - local pid_file="$HOME/.cache/borders-gradient-spin.pid" - local label="dev.macarchy.borders-gradient-spin" - local plist="$HOME/Library/LaunchAgents/$label.plist" - /bin/launchctl bootout "gui/$(id -u)" "$plist" >/dev/null 2>&1 || true - if [ -f "$pid_file" ]; then - local pid - pid="$(cat "$pid_file" 2>/dev/null || true)" - if [ -n "$pid" ]; then - kill "$pid" 2>/dev/null || true - fi - rm -f "$pid_file" + if (( failures > 0 )); then + macarchy_error "theme '$name' was only partly applied" + return 1 fi -} - -start_border_animation() { - local colors="$1" - local label="dev.macarchy.borders-gradient-spin" - local config="$HOME/.cache/borders-gradient-spin.conf" - local plist="$HOME/Library/LaunchAgents/$label.plist" - mkdir -p "$HOME/.cache" "$HOME/Library/LaunchAgents" - - cat > "$config" < "$plist" < - - - - Label - $label - ProgramArguments - - /bin/bash - -lc - exec "\$HOME/.local/bin/borders-gradient-spin" - - RunAtLoad - - EnvironmentVariables - - PATH - /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - - StandardOutPath - /tmp/borders-gradient-spin.log - StandardErrorPath - /tmp/borders-gradient-spin.log - - -EOF - /bin/launchctl bootstrap "gui/$(id -u)" "$plist" >/dev/null 2>&1 || true - /bin/launchctl kickstart -k "gui/$(id -u)/$label" >/dev/null 2>&1 || true -} - -map_nvim_colorscheme() { - local theme_name="${1:-}" - case "$theme_name" in - flexoki-light) - printf 'flexoki-light\n' - ;; - kanagawa) - printf 'kanagawa-wave\n' - ;; - nord) - printf 'nord\n' - ;; - aetheria|futurism|aura) - printf 'tokyonight-night\n' - ;; - akane|sakura) - printf 'rose-pine-moon\n' - ;; - awakening|pulsar|firesky|nes|batou|blackgold) - printf 'gruvbox\n' - ;; - midnight) - printf 'tokyonight-night\n' - ;; - blackturq) - printf 'nord\n' - ;; - carbonfox) - printf 'tokyonight-night\n' - ;; - killer-klownz) - printf 'aether\n' - ;; - florida-man) - printf 'catppuccin\n' - ;; - catppuccin) - printf 'catppuccin\n' - ;; - retro-82) - printf 'gruvbox\n' - ;; - lumon) - printf 'rose-pine-dawn\n' - ;; - matte-black) - printf 'gruvbox\n' - ;; - vantablack|hackerman) - printf 'tokyonight-night\n' - ;; - osaka-jade) - printf 'rose-pine-main\n' - ;; - *) - printf '%s\n' "${NVIM_COLORSCHEME:-tokyonight}" - ;; - esac -} - -write_nvim_colorscheme_state() { - local file="$1" - local colorscheme="$2" - local dir tmp - - dir="$(dirname "$file")" - [ -d "$dir" ] || return 0 - - if [ -f "$file" ] && [ "$(sed -n '1p' "$file" 2>/dev/null || true)" = "$colorscheme" ]; then + if ! printf '%s\n' "$name" | macarchy_write_generated "$MACARCHY_THEME_STATE"; then + macarchy_error "active theme state could not be written" return 1 fi - - tmp="$(mktemp "$dir/.nvim-colorscheme.XXXXXX")" - printf '%s\n' "$colorscheme" > "$tmp" - mv "$tmp" "$file" + macarchy_info "Theme '$name' applied." } -echo "Switching to theme: $THEME" - -# --- 1. macOS Dark/Light Mode --- -if [ -n "${DARK_MODE:-}" ]; then - if osascript -e "tell application \"System Events\" to tell appearance preferences to set dark mode to $DARK_MODE" 2>/dev/null; then - echo " Dark mode: $DARK_MODE" - else - echo " Dark mode: skipped" - fi -fi - -# --- 2. macOS Accent + Highlight --- -keep_macos_accent_and_highlight_static - -# --- 3. Wallpaper --- -if [ -n "${SELECTED_WALLPAPER:-}" ] && [ -f "$THEME_DIR/$SELECTED_WALLPAPER" ]; then - "$HOME/.local/bin/set-wallpaper" "$THEME_DIR/$SELECTED_WALLPAPER" >/tmp/theme-switch-wallpaper.log 2>&1 & - echo " Wallpaper: queued $SELECTED_WALLPAPER" -else - echo " Wallpaper: skipped (no file at $THEME_DIR/${SELECTED_WALLPAPER:-})" -fi - -# --- 4. Ghostty (inline palette into main config, then reload all windows) --- -if [ -f "$THEME_DIR/ghostty.conf" ] && [ -f "$GHOSTTY_CONFIG" ]; then - # Strip old palette/color lines and config-file refs from main config - TMP=$(mktemp) - awk ' - /^# --- THEME COLORS / { exit } - NF { - for (i = 0; i < blanks; i++) print "" - blanks = 0 - print - next - } - { blanks++ } - ' "$GHOSTTY_CONFIG" \ - | grep -Ev "^[[:space:]]*(background|foreground|cursor-color|cursor-text|selection-background|selection-foreground|palette|theme)[[:space:]]*=" \ - | grep -Ev "^[[:space:]]*config-file[[:space:]]*=.*\.themes/" \ - > "$TMP" || true - - # Append theme colors - echo "" >> "$TMP" - echo "# --- THEME COLORS ($THEME) ---" >> "$TMP" - cat "$THEME_DIR/ghostty.conf" >> "$TMP" - - cp "$TMP" "$GHOSTTY_CONFIG" && rm "$TMP" - - # Reload Ghostty app-wide without moving focus through the menu bar. - pkill -USR2 -x ghostty 2>/dev/null || pkill -USR2 -x Ghostty 2>/dev/null || true - echo " Ghostty: reload requested for all windows" -fi - -# --- 5. Neovim (running instances + LazyVim config) --- -if [ -n "${NVIM_COLORSCHEME:-}" ]; then - NVIM_THEME="$(map_nvim_colorscheme "$THEME")" - NVIM_COLORSCHEME_STATE="$THEMES_DIR/.nvim-colorscheme" - if write_nvim_colorscheme_state "$NVIM_COLORSCHEME_STATE" "$NVIM_THEME"; then - pkill -WINCH -x nvim >/dev/null 2>&1 || true - echo " Neovim: $NVIM_THEME (refresh requested)" - else - echo " Neovim: $NVIM_THEME" - fi -fi - -# --- 6. JankyBorders --- -if [ -n "${BORDER_ACTIVE:-}" ] || [ -n "${BORDER_ANIMATION_COLORS:-}" ]; then - stop_border_animation - apply_borders -fi - -# --- 7. btop --- -if [ -f "$BTOP_CONFIG" ]; then - BTOP_THEME="$(map_btop_theme "$THEME")" - write_btop_theme - /usr/bin/sed -i '' "s/^color_theme = .*/color_theme = \"$BTOP_THEME\"/" "$BTOP_CONFIG" - if reload_btop_instances; then - echo " btop: theme=$BTOP_THEME (reload requested)" - else - echo " btop: theme=$BTOP_THEME" - fi -fi - -# --- 8. SketchyBar --- -SKETCHYBAR_ACCENT="$(get_sketchybar_accent_color)" -SKETCHYBAR_TEXT="$(get_sketchybar_text_color)" -SKETCHYBAR_BAR="$(get_sketchybar_bar_color)" -apply_sketchybar_theme() { - local accent="$1" - local text="${2:-$accent}" - local bar="${3:-0x00000000}" - local sketchybar_bin="" - local theme_file="$HOME/.config/sketchybar/theme.sh" - - if command -v sketchybar >/dev/null 2>&1; then - sketchybar_bin="$(command -v sketchybar)" - elif [ -x /opt/homebrew/bin/sketchybar ]; then - sketchybar_bin="/opt/homebrew/bin/sketchybar" - else - return 0 - fi - - mkdir -p "${theme_file%/*}" - if [ ! -f "$theme_file" ]; then - printf '#!/usr/bin/env bash\n' > "$theme_file" - fi - - upsert_sketchybar_var() { - local key="$1" - local value="$2" - - if grep -q "^${key}=" "$theme_file"; then - /usr/bin/sed -i '' "s|^${key}=.*|${key}=\"${value}\"|" "$theme_file" +case "${1:-}" in + --list) + macarchy_theme_list + ;; + --background) + [[ $# -eq 3 ]] || { usage >&2; exit 1; } + macarchy_theme_load "$2" || exit 1 + macarchy_theme_save_background "$3" || exit 1 + if [[ "$(macarchy_theme_current)" == "$2" ]]; then + macarchy_apply_wallpaper "$3" else - printf '%s="%s"\n' "$key" "$value" >> "$theme_file" + macarchy_info "Saved background for theme '$2'." fi - } - - upsert_sketchybar_var "ACCENT_COLOR" "$accent" - upsert_sketchybar_var "TEXT_COLOR" "$text" - upsert_sketchybar_var "BAR_COLOR" "$bar" - - chmod +x "$theme_file" - "$sketchybar_bin" --reload >/dev/null 2>&1 || true -} -apply_sketchybar_theme "$SKETCHYBAR_ACCENT" "$SKETCHYBAR_TEXT" "$SKETCHYBAR_BAR" || true -echo " SketchyBar: accent=$SKETCHYBAR_ACCENT text=$SKETCHYBAR_TEXT bar=$SKETCHYBAR_BAR" - -# --- 9. Raycast --- -RAYCAST_THEME_ID="$(get_raycast_theme_id)" -/usr/bin/defaults write "$RAYCAST_BUNDLE_ID" raycastShouldFollowSystemAppearance -bool true -/usr/bin/defaults write "$RAYCAST_BUNDLE_ID" raycastCurrentThemeId -string "$RAYCAST_THEME_ID" -/usr/bin/defaults write "$RAYCAST_BUNDLE_ID" raycastCurrentThemeIdDarkAppearance -string "bundled-raycast-dark" -/usr/bin/defaults write "$RAYCAST_BUNDLE_ID" raycastCurrentThemeIdLightAppearance -string "bundled-raycast-light" -flush_macos_preferences -echo " Raycast: $RAYCAST_THEME_ID" - -# --- 10b. Shell env (eza + prompt colours for zsh) --- -# -# Emit *indexed* colours (38;5;N), not truecolor. When theme-switch reloads -# ghostty (SIGUSR2 above), the terminal re-resolves palette cells against -# the new palette — so existing scrollback recolours along with new output. -# Truecolor RGB would be baked into the cell buffer and stuck forever. -write_shell_env() { - # Mirror Raycast's accent priority: SYSTEM_ACCENT_COLOR -> FIREFOX_ACCENT - # -> BORDER_ACTIVE -> palette[4]. We walk this list and pick the first - # candidate whose hex actually matches a palette slot — that's the index - # we need so ghostty palette-reload recolours scrollback live. - _palette_idx_for_hex() { - local h="${1#\#}" - [[ "$h" =~ ^[0-9A-Fa-f]{6}$ ]] || return 0 - [ -f "$THEME_DIR/ghostty.conf" ] || return 0 - awk -v want="$(printf '%s' "$h" | tr 'A-F' 'a-f')" ' - tolower($0) ~ "^palette[[:space:]]*=[[:space:]]*[0-9]+=#" want "$" { - sub(/^palette[[:space:]]*=[[:space:]]*/, "", $0) - split($0, p, "=") - print p[1] - exit - }' "$THEME_DIR/ghostty.conf" - } - local border_hex="" - if [[ "${BORDER_ACTIVE:-}" =~ ^0x[fF]{2}([0-9A-Fa-f]{6})$ ]]; then - border_hex="${BASH_REMATCH[1]}" - fi - local palette4_hex="" - if [ -f "$THEME_DIR/ghostty.conf" ]; then - palette4_hex=$(awk '/^palette[[:space:]]*=[[:space:]]*4=#[0-9A-Fa-f]{6}$/ { - sub(/.*=#/, ""); print; exit - }' "$THEME_DIR/ghostty.conf") - fi - local accent_idx="" accent_hex="" candidate - for candidate in "${SYSTEM_ACCENT_COLOR:-}" "${FIREFOX_ACCENT:-}" "$border_hex" "$palette4_hex"; do - candidate="${candidate#\#}" - [[ "$candidate" =~ ^[0-9A-Fa-f]{6}$ ]] || continue - accent_idx=$(_palette_idx_for_hex "$candidate") - if [ -n "$accent_idx" ]; then - accent_hex="$candidate" - break - fi - done - # Last-resort fallback: Raycast's own convention (palette slot 4). - [ -z "$accent_idx" ] && { accent_idx=4; accent_hex="${palette4_hex:-}"; } - - local fg_idx=15 - [ "${DARK_MODE:-true}" = "false" ] && fg_idx=0 - local mut_idx=8 - - local acc="38;5;${accent_idx}" - local mut="38;5;${mut_idx}" - # Override only what's theme-specific: directories/executables get the - # accent, links stay muted grey. Everything else (perms, user, date, - # size, etc.) uses eza's stock palette-based defaults — which already - # repaint on theme reload because they're indexed colours. - local eza="di=${acc}:ex=1;${acc}:ln=${mut}:xx=${mut}" - - local fg_hex="ffffff" - [ "${DARK_MODE:-true}" = "false" ] && fg_hex="000000" - - local env_file="$THEMES_DIR/.shell-env" - { - echo "# Auto-generated by theme-switch. Do not edit." - echo "export EZA_COLORS='${eza}'" - echo "export MACARCHY_ACCENT_IDX='${accent_idx}'" - echo "export MACARCHY_FG_IDX='${fg_idx}'" - echo "export MACARCHY_ACCENT='#${accent_hex:-${fg_hex}}'" - echo "export MACARCHY_FG='#${fg_hex}'" - echo "export MACARCHY_DARK_MODE='${DARK_MODE:-true}'" - } > "$env_file" - echo " Shell env: accent_idx=${accent_idx} fg_idx=${fg_idx} -> $env_file" -} -write_shell_env - -reload_cursor_agent_instances - -echo "Done! Theme '$THEME' applied." + ;; + next|prev) + target="$(macarchy_theme_cycle "$1")" || exit 1 + apply_theme "$target" + ;; + '') + usage >&2 + exit 1 + ;; + -*) + usage >&2 + exit 1 + ;; + *) + [[ $# -eq 1 ]] || { usage >&2; exit 1; } + apply_theme "$1" + ;; +esac diff --git a/borders/bordersrc b/borders/bordersrc index f15c803..a76ad9c 100755 --- a/borders/bordersrc +++ b/borders/bordersrc @@ -1,3 +1,24 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -borders active_color=0xfff2fcff inactive_color=0x00000000 width=6.0 +theme_file="${XDG_CONFIG_HOME:-$HOME/.config}/macarchy/generated/borders.conf" + +active_color="0xfff2fcff" +inactive_color="0x00000000" +width="6.0" +blacklist="iPhone Mirroring" + +# This file is generated by theme-switch and contains validated values only. +if [[ -r "$theme_file" ]]; then + # shellcheck source=/dev/null + source "$theme_file" +fi + +exec borders \ + style=round \ + hidpi=on \ + ax_focus=on \ + active_color="$active_color" \ + inactive_color="$inactive_color" \ + width="$width" \ + blacklist="$blacklist" diff --git a/btop/btop.conf b/btop/btop.conf index 11976ba..d48edb3 100644 --- a/btop/btop.conf +++ b/btop/btop.conf @@ -1,224 +1,18 @@ -#? Config file for btop v. 1.4.5 +# Macarchy keeps only deliberate overrides here. btop supplies the rest of its +# defaults, so new upstream options are not frozen by an old generated config. -#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. -#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" -color_theme = "active-theme" - -#* If the theme set background should be shown, set to False if you want terminal background transparency. +color_theme = "macarchy" theme_background = False - -#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. truecolor = True - -#* Set to true to force tty mode regardless if a real tty has been detected or not. -#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. -force_tty = False - -#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. -#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. -#* Use whitespace " " as separator between different presets. -#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" -presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" - -#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. -#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. -vim_keys = False - -#* Rounded corners on boxes, is ignored if TTY mode is ON. rounded_corners = True - -#* Default symbols to use for graph creation, "braille", "block" or "tty". -#* "braille" offers the highest resolution but might not be included in all fonts. -#* "block" has half the resolution of braille but uses more common characters. -#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. -#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. -graph_symbol = "braille" - -# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". -graph_symbol_cpu = "default" - -# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". -graph_symbol_mem = "default" - -# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". -graph_symbol_net = "default" - -# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". -graph_symbol_proc = "default" - -#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. -shown_boxes = "cpu mem net proc" - -#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. update_ms = 2000 -#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", -#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. proc_sorting = "cpu lazy" - -#* Reverse sorting order, True or False. -proc_reversed = False - -#* Show processes as a tree. -proc_tree = False - -#* Use the cpu graph colors in the process list. -proc_colors = True - -#* Use a darkening gradient in the process list. -proc_gradient = False - -#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. -proc_per_core = False - -#* Show process memory as bytes instead of percent. proc_mem_bytes = True +proc_gradient = False -#* Show cpu graph for each process. -proc_cpu_graphs = False - -#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) -proc_info_smaps = False - -#* Show proc box on left side of screen instead of right. -proc_left = False - -#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). -proc_filter_kernel = False - -#* In tree-view, always accumulate child process resources in the parent process. -proc_aggregate = False - -#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. -#* Select from a list of detected attributes from the options menu. -cpu_graph_upper = "Auto" - -#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. -#* Select from a list of detected attributes from the options menu. -cpu_graph_lower = "Auto" - -#* Toggles if the lower CPU graph should be inverted. -cpu_invert_lower = True - -#* Set to True to completely disable the lower CPU graph. -cpu_single_graph = False - -#* Show cpu box at bottom of screen instead of top. -cpu_bottom = False - -#* Shows the system uptime in the CPU box. -show_uptime = True - -#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo. -show_cpu_watts = True - -#* Show cpu temperature. check_temp = True - -#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. -cpu_sensor = "Auto" - -#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. -show_coretemp = True - -#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. -#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. -#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. -#* Example: "4:0 5:1 6:3" -cpu_core_map = "" - -#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". temp_scale = "celsius" - -#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. -base_10_sizes = False - -#* Show CPU frequency. -show_cpu_freq = True - -#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. -#* Special formatting: /host = hostname | /user = username | /uptime = system uptime -clock_format = "%X" - -#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. -background_update = False - -#* Custom cpu model name, empty string to disable. -custom_cpu_name = "" - -#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". -#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user" -disks_filter = "" - -#* Show graphs instead of meters for memory values. -mem_graphs = True - -#* Show mem box below net box instead of above. -mem_below_net = False - -#* Count ZFS ARC in cached and available memory. -zfs_arc_cached = True - -#* If swap memory should be shown in memory box. -show_swap = True - -#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. -swap_disk = True - -#* If mem box should be split to also show disks info. -show_disks = True - -#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. -only_physical = True - -#* Read disks list from /etc/fstab. This also disables only_physical. -use_fstab = True - -#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) -zfs_hide_datasets = False - -#* Set to true to show available disk space for privileged users. -disk_free_priv = False - -#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. -show_io_stat = True - -#* Toggles io mode for disks, showing big graphs for disk read/write speeds. -io_mode = False - -#* Set to True to show combined read/write io graphs in io mode. -io_graph_combined = False - -#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". -#* Example: "/mnt/media:100 /:20 /boot:1". -io_graph_speeds = "" - -#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. -net_download = 100 - -net_upload = 100 - -#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. -net_auto = True - -#* Sync the auto scaling for download and upload to whichever currently has the highest scale. -net_sync = True - -#* Starts with the Network Interface specified here. -net_iface = "" - -#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes. -base_10_bitrate = "Auto" - -#* Show battery stats in top right if battery is present. +show_cpu_watts = True show_battery = True - -#* Which battery to use if multiple are present. "Auto" for auto detection. -selected_battery = "Auto" - -#* Show power stats of battery next to charge indicator. show_battery_watts = True - -#* Set loglevel for "~/.config/btop/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". -#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. -log_level = "WARNING" diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..4e16ee8 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,115 @@ +# Install and update Macarchy + +Macarchy copies files rather than symlinking them. That makes an installed setup independent of the clone, while a manifest records which files belong to each component. + +## Requirements + +- A currently supported macOS release +- [Homebrew](https://brew.sh) +- Git +- Zsh and the macOS system Bash, both included with macOS + +Raycast development needs Node.js 22.22.2 or newer. Apple Silicon system metrics use `macmon`; Intel Macs still get the bar, network and battery items, while the CPU, GPU and RAM tiles are omitted. + +## Choose components + +```sh +./install --list +./install --dry-run window-manager sketchybar +./install window-manager sketchybar +``` + +You can pass several components at once. Running the same command again updates the managed files and leaves unrelated components alone. + +Commands are installed in `~/.local/bin` by default. Set `MACARCHY_BIN_HOME` before running the installer if you need another location, and keep using the same value for later updates and uninstall commands. + +The installer also installs a component's Homebrew dependencies. Third-party taps are used for yabai, skhd, JankyBorders and SketchyBar. If your Homebrew setup requires explicit tap trust, review the tap and trust only the formulae you plan to install, for example: + +```sh +brew trust --formula asmvik/formulae/yabai asmvik/formulae/skhd +brew trust --formula FelixKratz/formulae/borders FelixKratz/formulae/sketchybar +``` + +Then rerun the installer. + +## What happens to existing files + +Before replacing a file, Macarchy copies it to: + +```text +${XDG_STATE_HOME:-~/.local/state}/macarchy/backups/// +``` + +Manifests live beside the backups under `macarchy/manifests`. They store the installed checksum and the path to the original backup. This is what makes status checks and safe uninstall possible. + +If you edit a managed file and run uninstall, Macarchy keeps your edited version and reports it. It will not silently delete work it no longer recognises. + +The `shell` component replaces `~/.zshenv` with a small bootstrap that sets `ZDOTDIR` to `${XDG_CONFIG_HOME:-~/.config}/zsh`. Once that is active, Zsh loads Macarchy's `.zprofile` and `.zshrc` from the new directory instead of existing copies in your home directory. Those home-directory files are not deleted, but they become inactive. Review them first and move settings you still need into the installed config or `${XDG_CONFIG_HOME:-~/.config}/zsh/local.zsh`. + +## Start only what you use + +The installer does not start background services. After reviewing the installed configs, use the relevant commands: + +```sh +yabai --start-service +skhd --start-service +brew services start borders +brew services start sketchybar +``` + +yabai and skhd need Accessibility access in System Settings under Privacy & Security. SketchyBar also expects "Displays have separate Spaces" to be enabled in Desktop & Dock. + +## Custom XDG paths + +Files honour `XDG_CONFIG_HOME`, `XDG_DATA_HOME` and `XDG_STATE_HOME`. If `XDG_CONFIG_HOME` is not `~/.config`, launchd services and GUI applications also need to see it: + +```sh +launchctl setenv XDG_CONFIG_HOME "$XDG_CONFIG_HOME" +``` + +Run that before starting yabai, skhd, SketchyBar or Ghostty. The installer prints a reminder when it detects a custom path. + +## Raycast source and runtime + +The `raycast` component installs development source in: + +```text +${XDG_DATA_HOME:-~/.local/share}/macarchy/raycast-theme +``` + +Install its dependencies and register the command from that directory. Raycast generates the active command under `~/.config/raycast/extensions/theme-switcher` itself. Never run `npm install` in that generated directory. Keeping `node_modules` there gives the command a second React instance and causes an invalid hook error. + +The installer moves any legacy runtime `node_modules` into Macarchy's normal backup directory. It leaves Raycast's generated command in place when you update or uninstall the source component. If an older Macarchy install had replaced a pre-existing runtime file, its original backup remains listed in `${XDG_STATE_HOME:-~/.local/state}/macarchy/recovery/raycast-runtime.tsv` for manual recovery. + +## Update + +```sh +git pull --ff-only +./install terminal shell themes +./install status terminal shell themes +``` + +Rerun only the components you use. When Homebrew upgrades yabai, an optional scripting-addition sudoers hash must also be refreshed. See the [window management guide](./window-management.md). + +If a managed file has local edits, an ordinary update stops before replacing it. Review the file first, then rerun that component with `--force` if you want the repository version. The installer keeps an additional backup of the edited copy. + +```sh +./install --force terminal +``` + +When upgrading from the old all-in-one setup, the installer also handles two legacy cases: + +- An existing Ghostty `config` file is backed up and replaced with a comment-only compatibility file, preventing it from overriding the newer `config.ghostty`. +- On the first `window-manager` install, an existing unmanaged `skhdrc` is copied to `local.skhdrc` before the public configuration is installed. An existing `local.skhdrc` is never overwritten. +- Older Raycast source is no longer managed inside Raycast's generated extension directory, and unsafe runtime dependencies are moved into a backup. + +The `themes` component leaves existing theme directories and wallpaper files in place. If Macarchy ships a file at the same path, that individual file still goes through the normal backup and replacement process. + +## Uninstall + +```sh +./install --dry-run uninstall sketchybar +./install uninstall sketchybar +``` + +Uninstall restores the original files where backups exist. Removing the `themes` component also removes unchanged files generated by `theme-switch`, while preserving any that were edited afterwards. It does not uninstall Homebrew packages, stop services or remove empty directories because those may be shared with another setup. diff --git a/docs/sketchybar.md b/docs/sketchybar.md new file mode 100644 index 0000000..7055c5c --- /dev/null +++ b/docs/sketchybar.md @@ -0,0 +1,33 @@ +# SketchyBar + +The `sketchybar` component installs the bar config, the app icon font and its small shell plugins. + +```sh +./install sketchybar +brew services start sketchybar +``` + +For application labels inside each Space, install the `window-manager` component as well. Without yabai, the rest of the bar still loads and the Space list falls back gracefully. + +## System metrics + +On Apple Silicon, the installer adds [macmon](https://github.com/vladkens/macmon). A small local worker runs `macmon pipe` and writes the newest sample under the SketchyBar state directory. It does not start macmon's HTTP server or open port 9090. + +One hidden SketchyBar item reads that sample every five seconds and updates CPU, GPU and RAM together. Network speed has its own five-second updater because it compares macOS interface counters over time. + +If macmon is unavailable, the CPU, GPU and RAM tiles are omitted. The rest of the bar remains usable on Intel Macs, where macmon is not supported. + +## Themes + +`colors.sh` has safe defaults and reads the generated `theme.sh` file when the `themes` component is installed. Applying a theme reloads SketchyBar automatically. + +## Debugging + +Stop the service and run the bar in the foreground to see plugin errors: + +```sh +brew services stop sketchybar +sketchybar +``` + +After fixing the issue, press Control-C and restart the service. `sketchybar --reload` is enough for ordinary config changes. diff --git a/docs/themes.md b/docs/themes.md new file mode 100644 index 0000000..f8d270d --- /dev/null +++ b/docs/themes.md @@ -0,0 +1,80 @@ +# Themes + +The themes component keeps colours in sync across the parts of Macarchy you have installed. The built-in themes are palette-only; wallpaper-heavy packs can be installed separately. + +```sh +./install themes +"$HOME/.local/bin/theme-switch" --list +"$HOME/.local/bin/theme-switch" carbonfox +"$HOME/.local/bin/theme-switch" next +"$HOME/.local/bin/theme-switch" prev +``` + +The installer uses `~/.local/bin/theme-switch` unless `MACARCHY_BIN_HOME` is set. Use the full path when the `shell` component is not installed, because a themes-only install does not change `PATH`. + +An external pack can include wallpapers. Choose one with a path relative to its theme directory: + +```sh +"$HOME/.local/bin/theme-switch" --background my-theme backgrounds/my-wallpaper.jpg +``` + +The selected background is remembered per theme. Built-in palettes leave the desktop picture unchanged. + +Theme choices are stored under `${XDG_STATE_HOME:-~/.local/state}/macarchy/themes`, separately from the theme pack itself. This keeps external theme repositories clean and allows the pack directory to be read-only. + +## What gets updated + +`theme-switch` can update: + +- macOS light or dark appearance and the desktop picture +- Raycast's current, dark and light appearance defaults +- Ghostty's dedicated `macarchy-theme.ghostty` colour file +- Neovim's selected colour-scheme state +- a generated btop theme +- JankyBorders colours +- SketchyBar colours +- the Zsh prompt colour state + +Missing applications are skipped. The script writes only Macarchy-owned generated files. + +## Theme format + +Each theme is a directory containing `theme.env` and `ghostty.conf`. Images are optional and can sit in the theme directory or a `backgrounds` subdirectory. `theme.env` is parsed as data and is never sourced as shell code. + +Supported keys are: + +| Key | Purpose | +| --- | --- | +| `DARK_MODE` | `true` or `false` | +| `WALLPAPER` | Relative image path inside the theme | +| `NVIM_COLORSCHEME` | Installed Neovim colour-scheme name | +| `BORDER_ACTIVE` | Active border in `0xAARRGGBB` format | +| `BORDER_INACTIVE` | Inactive border in `0xAARRGGBB` format | +| `BORDER_WIDTH` | JankyBorders width | +| `SKETCHYBAR_BAR_COLOR` | Optional bar colour | +| `SKETCHYBAR_TEXT_COLOR` | Optional bar text colour. Defaults to Ghostty's foreground colour | +| `FIREFOX_ACCENT` | Optional legacy accent hint used for palette-based shell colours | + +Ghostty files accept palette and colour keys plus these visual settings used by the original packs: + +- `background-opacity`, from `0` to `1` +- `window-padding-x` and `window-padding-y`, as one or two non-negative numbers +- `window-padding-balance`, as `true` or `false` + +A downloaded theme still cannot use its Ghostty file to change commands, fonts or key bindings. + +Run `./tests/theme_validation_test.sh` before sharing a pack. + +## External theme packs + +The theme directory is intentionally independent from the rest of the configs. You can point the command at another pack collection: + +```sh +MACARCHY_THEMES_DIR="$HOME/.local/share/my-themes" "$HOME/.local/bin/theme-switch" --list +``` + +The Raycast extension has a matching Themes Directory preference. A theme repo can therefore be installed or linked separately without changing the core scripts. + +Locally retained copies of the original `awakening`, `blackgold`, `carbonfox`, `city-783`, `lumon`, `matte-black`, `midnight` and `turbonite` packs remain compatible. Their names are preserved in `--list`, `next` and `prev`. Saved `lumon` or `turbonite` state is mapped to the built-in `cool-blue` or `amber-metal` palette only when the original local directory is absent. + +The old bundled wallpapers were removed after the provenance audit found unclear and restricted redistribution terms. New wallpaper-heavy packs should live in separate repositories so the core clone stays small and each pack can carry its own licence and credits. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..de32378 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,98 @@ +# Troubleshooting + +## See what would change + +```sh +./install --dry-run terminal +./install status terminal +``` + +`status` reports missing or modified managed files and exits non-zero when something needs attention. + +## Homebrew refuses a third-party tap + +Recent Homebrew versions can require explicit trust before loading formulae from a non-official tap. Review the upstream repository first, then trust only the formula you need: + +```sh +brew trust --formula asmvik/formulae/yabai +brew trust --formula FelixKratz/formulae/sketchybar +``` + +Rerun the installer after approving it. + +## A service cannot find its config + +If you use a non-default `XDG_CONFIG_HOME`, expose it to the launchd user session before starting services: + +```sh +launchctl setenv XDG_CONFIG_HOME "$XDG_CONFIG_HOME" +``` + +Then restart the affected service. + +## yabai Space commands fail + +Confirm Accessibility access first. Space swapping and some other advanced operations also require yabai's optional scripting addition. The [window management guide](./window-management.md) explains the boundary and links to the current upstream instructions. + +## SketchyBar shows metric placeholders + +Apple Silicon metrics require `macmon` and the local sampler started by SketchyBar. Check both processes and reload the bar: + +```sh +command -v macmon +pgrep -fl 'macmon pipe' +sketchybar --reload +``` + +Intel Macs intentionally omit the CPU, GPU and RAM tiles. Network and battery items do not need macmon. + +Older Macarchy releases used `macmon serve` on port 9090. The current config uses a local pipe and does not stop an old process automatically. After upgrading, check before ending it: + +```sh +pgrep -fl 'macmon serve' +``` + +If that output is the old Macarchy process, stop its listed PID normally and reload SketchyBar. Do not kill an unrelated macmon session. + +## A theme only partly applies + +Run it from a terminal to see the failing target: + +```sh +"$HOME/.local/bin/theme-switch" carbonfox +``` + +The active theme is only recorded after all required writes succeed. Optional applications that are not installed are skipped. + +## Ghostty still uses old settings + +Ghostty loads its macOS Application Support config after its XDG config. Check both locations if a setting still overrides Macarchy: + +```sh +sed -n '1,120p' "$HOME/.config/ghostty/config.ghostty" +sed -n '1,120p' "$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty" +``` + +The installer neutralises the older XDG file named `config`, but it only warns about a non-empty Application Support file because that file may be intentionally machine-specific. + +## Raycast store validation rejects the author + +Local linting and builds work with the neutral `macarchy` author. Raycast Store publishing requires a real Raycast handle, so a publisher must change the manifest author before running: + +```sh +cd raycast-theme +npm run validate:store +``` + +## Raycast reports an invalid hook call + +This means `node_modules` was installed beside Raycast's generated command. Reinstall the component to preserve those dependencies in a backup, then register the extension from its source directory: + +```sh +./install raycast +cd "${XDG_DATA_HOME:-$HOME/.local/share}/macarchy/raycast-theme" +npm ci +npm run dev +``` + +Do not run npm commands in `~/.config/raycast/extensions/theme-switcher`. diff --git a/docs/window-management.md b/docs/window-management.md new file mode 100644 index 0000000..4345f18 --- /dev/null +++ b/docs/window-management.md @@ -0,0 +1,55 @@ +# Window management + +The `window-manager` component installs configs for [yabai](https://github.com/asmvik/yabai), [skhd](https://github.com/asmvik/skhd) and [JankyBorders](https://github.com/FelixKratz/JankyBorders). + +```sh +./install window-manager +``` + +Review the files, grant Accessibility access to yabai and skhd, then start the services you want: + +```sh +yabai --start-service +skhd --start-service +brew services start borders +``` + +## Default shortcuts + +The default skhd file contains window-management bindings only. Application launchers, media keys and theme controls are examples you can opt into separately. + +| Shortcut | Action | +| --- | --- | +| `⌃⌥F` | Toggle fullscreen for the focused window | +| `⌃⌥` + arrow | Focus a window in that direction | +| `⌃⌥⇧` + arrow | Swap with a window in that direction | +| `⌃⌥1` to `⌃⌥0` | Focus Spaces 1 to 10 | +| `⌃⌥⇧1` to `⌃⌥⇧0` | Move a window to a Space and follow it | +| `⌃⌥T` | Toggle floating and remember the choice for that app | +| `⌃⌥J` | Toggle the BSP split direction | +| `⌃⌥L` | Rotate and rebalance the current layout | +| `⌃⌥=` and `⌃⌥-` | Change the focused split ratio | + +Optional application launchers, theme controls, screenshots and scripting-addition Space swaps live in [`skhd/examples`](../skhd/examples). They are not loaded by default. + +The installed `skhdrc` loads `~/.config/skhd/local.skhdrc`, or the equivalent XDG path. On the first install, an existing unmanaged `skhdrc` is preserved there before the public configuration replaces it. Otherwise, the installer creates the local file once. It never manages, replaces or removes that file, so it is the right place for personal app shortcuts and legacy key combinations. Copy examples there rather than editing the managed `skhdrc`. + +Floating preferences are stored as a JSON array in `~/.config/yabai/float_state.json`, or the equivalent XDG path. The generated yabai rules are labelled, so reloading the config replaces them instead of adding duplicates. + +An additional helper under [`yabai/examples`](../yabai/examples) can bring a minimised window onto the current Space when its app is activated. It is not enabled by default because moving a window automatically can be surprising. Wire it to an `application_activated` signal only if that is the behaviour you want. + +The same examples directory contains a workaround that restarts JankyBorders after a native fullscreen Space closes. It is also opt-in because it restarts a user service. Add the labelled yabai signals yourself only if you encounter stale border overlays. + +## Optional scripting addition + +Most everyday tiling works with SIP enabled. Some Space operations, including swapping Spaces, need yabai's scripting addition and a partially disabled SIP configuration. + +Changing SIP is a meaningful reduction in macOS security and is never automated by Macarchy. Read yabai's current [SIP instructions](https://github.com/asmvik/yabai/wiki/Disabling-System-Integrity-Protection) before deciding whether those features are worth it. + +If you enable the scripting addition, follow yabai's [installation guide](https://github.com/asmvik/yabai/wiki/Installing-yabai-(latest-release)) to create a hash-pinned sudoers rule for `yabai --load-sa`. Macarchy checks that rule with non-interactive sudo. When it is available, the config loads the addition and reloads it after Dock restarts; otherwise it quietly skips it. + +The binary hash changes after a yabai upgrade, so update the sudoers entry each time. Do not replace the hash-pinned rule with unrestricted passwordless sudo. + +## Personal changes + +Padding, gaps, mouse behaviour and the small set of floating system windows are grouped near the bottom of `yabairc`. Keep machine-specific rules in your installed copy and review the backup shown by the installer before updating that component. diff --git a/ghostty/config b/ghostty/config deleted file mode 100644 index d99d853..0000000 --- a/ghostty/config +++ /dev/null @@ -1,626 +0,0 @@ -window-decoration = true -macos-titlebar-style = hidden -macos-window-buttons = hidden -window-padding-x = 14 -window-padding-y = 14 -window-padding-balance = true -background-opacity = 0.92 -quit-after-last-window-closed = true -clipboard-paste-protection = false -keybind = cmd+enter=unbind -keybind = cmd+enter=new_window - keybind = ctrl+left=unbind - keybind = ctrl+right=unbind - keybind = ctrl+up=unbind - keybind = ctrl+down=unbind -confirm-close-surface = false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# --- THEME COLORS (lumon) --- -background = #16242d -foreground = #d6e2ee -cursor-color = #f2fcff -selection-background = #4d9ed3 -selection-foreground = #1b2d40 - -palette = 0=#1b2d40 -palette = 1=#4d86b0 -palette = 2=#5e95bc -palette = 3=#6fa4c9 -palette = 4=#6fb8e3 -palette = 5=#8bc9eb -palette = 6=#b4e4f6 -palette = 7=#d6e2ee -palette = 8=#304860 -palette = 9=#73a6cb -palette = 10=#86b7d8 -palette = 11=#9dcae5 -palette = 12=#f2fcff -palette = 13=#b1d8ee -palette = 14=#d1eef8 -palette = 15=#ffffff diff --git a/ghostty/config.ghostty b/ghostty/config.ghostty new file mode 100644 index 0000000..9c3595a --- /dev/null +++ b/ghostty/config.ghostty @@ -0,0 +1,18 @@ +window-decoration = true +macos-titlebar-style = hidden +macos-window-buttons = hidden +window-padding-x = 14 +window-padding-y = 14 +window-padding-balance = true +background-opacity = 0.92 + +keybind = cmd+enter=unbind +keybind = cmd+enter=new_window +keybind = ctrl+left=unbind +keybind = ctrl+right=unbind +keybind = ctrl+up=unbind +keybind = ctrl+down=unbind + +# theme-switch owns this optional file, leaving the user's main configuration +# untouched when colours change. +config-file = ?macarchy-theme.ghostty diff --git a/install b/install new file mode 100755 index 0000000..632faf1 --- /dev/null +++ b/install @@ -0,0 +1,1102 @@ +#!/usr/bin/env bash + +# macOS still ships Bash 3.2, where nounset treats empty arrays as unbound. +set -eo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" + +CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" +DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" +STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" +BIN_HOME="${MACARCHY_BIN_HOME:-$HOME/.local/bin}" +APPLICATIONS_DIR="${MACARCHY_APPLICATIONS_DIR:-/Applications}" +RAYCAST_SOURCE_DIR="$DATA_HOME/macarchy/raycast-theme" + +MACARCHY_STATE_DIR="$STATE_HOME/macarchy" +MANIFEST_DIR="$MACARCHY_STATE_DIR/manifests" +BACKUP_DIR="$MACARCHY_STATE_DIR/backups" +RAYCAST_RECOVERY_MANIFEST="$MACARCHY_STATE_DIR/recovery/raycast-runtime.tsv" +BACKUP_RUN_ID="$(date '+%Y%m%d-%H%M%S')-$$" +BACKUP_COUNTER=0 +DRY_RUN=0 +FORCE=0 +SEEN_FILE="" + +COMPONENTS=( + window-manager + sketchybar + terminal + shell + nvim + utilities + themes + raycast +) + +info() { + printf '%s\n' "$*" +} + +warn() { + printf 'Warning: %s\n' "$*" >&2 +} + +die() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +usage() { + cat <<'EOF' +Usage: + ./install --list + ./install [--dry-run] [--force] install + ./install [--dry-run] [--force] + ./install status [component...] + ./install [--dry-run] uninstall + +The installer copies only the selected configuration. It does not change SIP, +write sudoers files, start services or apply a theme. + +Updates stop if a managed file has local edits. Review the diff first, then use +--force to replace it while keeping an extra backup of the edited version. +EOF +} + +component_description() { + case "$1" in + window-manager) printf '%s' 'yabai, skhd and borders' ;; + sketchybar) printf '%s' 'menu bar configuration and plugins' ;; + terminal) printf '%s' 'Ghostty configuration' ;; + shell) printf '%s' 'zsh configuration and bootstrap' ;; + nvim) printf '%s' 'Neovim and LazyVim configuration' ;; + utilities) printf '%s' 'btop and fastfetch configuration' ;; + themes) printf '%s' 'themes, theme switcher and wallpaper helper' ;; + raycast) printf '%s' 'Raycast theme switcher extension' ;; + *) return 1 ;; + esac +} + +dependencies_for() { + FORMULAE=() + CASKS=() + + case "$1" in + window-manager) + FORMULAE=(asmvik/formulae/yabai asmvik/formulae/skhd FelixKratz/formulae/borders jq) + ;; + sketchybar) + FORMULAE=(FelixKratz/formulae/sketchybar jq) + CASKS=(font-sketchybar-app-font) + if [ "$(uname -s)" = 'Darwin' ] && [ "$(uname -m)" = 'arm64' ]; then + FORMULAE+=(macmon) + fi + ;; + terminal) + CASKS=(ghostty) + ;; + shell) + ;; + nvim) + FORMULAE=(neovim ripgrep) + ;; + utilities) + FORMULAE=(btop fastfetch) + ;; + themes) + FORMULAE=(jq) + ;; + raycast) + FORMULAE=(node) + CASKS=(raycast) + ;; + *) + return 1 + ;; + esac +} + +list_components() { + local component formula cask dependency_list + + printf '%-17s %-43s %s\n' 'COMPONENT' 'INCLUDES' 'HOMEBREW DEPENDENCIES' + for component in "${COMPONENTS[@]}"; do + dependencies_for "$component" + dependency_list="" + for formula in "${FORMULAE[@]}"; do + dependency_list="${dependency_list}${dependency_list:+, }$formula" + done + for cask in "${CASKS[@]}"; do + dependency_list="${dependency_list}${dependency_list:+, }$cask (cask)" + done + printf '%-17s %-43s %s\n' \ + "$component" "$(component_description "$component")" "$dependency_list" + done +} + +is_component() { + local known + for known in "${COMPONENTS[@]}"; do + [ "$known" = "$1" ] && return 0 + done + return 1 +} + +select_components() { + local requested candidate existing found + SELECTED_COMPONENTS=() + + [ "$#" -gt 0 ] || die 'choose at least one component (use --list to see them)' + + for requested in "$@"; do + if [ "$requested" = 'all' ]; then + for candidate in "${COMPONENTS[@]}"; do + SELECTED_COMPONENTS+=("$candidate") + done + continue + fi + + is_component "$requested" || die "unknown component '$requested' (use --list to see them)" + + found=0 + for existing in "${SELECTED_COMPONENTS[@]}"; do + if [ "$existing" = "$requested" ]; then + found=1 + break + fi + done + [ "$found" -eq 1 ] || SELECTED_COMPONENTS+=("$requested") + done + + # `all` may have been combined with another name, so remove duplicates once. + UNIQUE_COMPONENTS=() + for candidate in "${SELECTED_COMPONENTS[@]}"; do + found=0 + for existing in "${UNIQUE_COMPONENTS[@]}"; do + [ "$existing" = "$candidate" ] && found=1 && break + done + [ "$found" -eq 1 ] || UNIQUE_COMPONENTS+=("$candidate") + done + SELECTED_COMPONENTS=("${UNIQUE_COMPONENTS[@]}") +} + +brew_formula_installed() { + local formula="$1" + if "$BREW_BIN" list --formula --versions "${formula##*/}" >/dev/null 2>&1; then + return 0 + fi + + # A suitable Node installation may come from fnm, nvm or a versioned + # Homebrew formula. Do not install a second copy just to build Raycast code. + if [ "${formula##*/}" = 'node' ] && command -v node >/dev/null 2>&1; then + node -e ' + const [major, minor, patch] = process.versions.node.split(".").map(Number); + process.exit(major > 22 || (major === 22 && (minor > 22 || (minor === 22 && patch >= 2))) ? 0 : 1); + ' >/dev/null 2>&1 + return + fi + + return 1 +} + +brew_cask_installed() { + local cask="$1" app_name="" + + "$BREW_BIN" list --cask "$cask" >/dev/null 2>&1 && return 0 + + case "$cask" in + ghostty) app_name='Ghostty.app' ;; + raycast) app_name='Raycast.app' ;; + esac + + [ -n "$app_name" ] \ + && { [ -d "$APPLICATIONS_DIR/$app_name" ] || [ -d "$HOME/Applications/$app_name" ]; } +} + +install_dependencies() { + local component="$1" formula cask + + dependencies_for "$component" + + if [ "${#FORMULAE[@]}" -eq 0 ] && [ "${#CASKS[@]}" -eq 0 ]; then + return + fi + + if [ "$DRY_RUN" -eq 1 ]; then + for formula in "${FORMULAE[@]}"; do + info "Would ensure Homebrew formula: $formula" + done + for cask in "${CASKS[@]}"; do + info "Would ensure Homebrew cask: $cask" + done + return + fi + + BREW_BIN="$(command -v brew 2>/dev/null || true)" + [ -n "$BREW_BIN" ] || die 'Homebrew is required to install dependencies: https://brew.sh' + + for formula in "${FORMULAE[@]}"; do + if brew_formula_installed "$formula"; then + info "Dependency already installed: ${formula##*/}" + else + info "Installing Homebrew formula: $formula" + "$BREW_BIN" install "$formula" + fi + done + + for cask in "${CASKS[@]}"; do + if brew_cask_installed "$cask"; then + info "Dependency already installed: $cask" + else + info "Installing Homebrew cask: $cask" + "$BREW_BIN" install --cask "$cask" + fi + done +} + +file_hash() { + local file="$1" + + [ -f "$file" ] || return 1 + + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + else + die 'shasum or sha256sum is required to track installed files' + fi +} + +path_exists() { + [ -e "$1" ] || [ -L "$1" ] +} + +manifest_path() { + printf '%s/%s.tsv\n' "$MANIFEST_DIR" "$1" +} + +lookup_manifest_entry() { + local component="$1" destination="$2" manifest current backup checksum + manifest="$(manifest_path "$component")" + [ -f "$manifest" ] || return 1 + + while IFS=$'\t' read -r current backup checksum; do + if [ "$current" = "$destination" ]; then + printf '%s\t%s\n' "$backup" "$checksum" + return 0 + fi + done < "$manifest" + + return 1 +} + +record_manifest_entry() { + local component="$1" destination="$2" backup="$3" checksum="$4" + local manifest temporary current old_backup old_checksum + + mkdir -p "$MANIFEST_DIR" + manifest="$(manifest_path "$component")" + temporary="$(mktemp "$MANIFEST_DIR/.${component}.XXXXXX")" + + if [ -f "$manifest" ]; then + while IFS=$'\t' read -r current old_backup old_checksum; do + [ "$current" = "$destination" ] && continue + printf '%s\t%s\t%s\n' "$current" "$old_backup" "$old_checksum" >> "$temporary" + done < "$manifest" + fi + + printf '%s\t%s\t%s\n' "$destination" "$backup" "$checksum" >> "$temporary" + mv "$temporary" "$manifest" +} + +backup_current_path() { + local component="$1" destination="$2" name backup_parent + + BACKUP_COUNTER=$((BACKUP_COUNTER + 1)) + name="$(basename "$destination")" + backup_parent="$BACKUP_DIR/$BACKUP_RUN_ID/$component" + BACKUP_RESULT="$backup_parent/${BACKUP_COUNTER}-$name" + + mkdir -p "$backup_parent" + if [ -L "$destination" ]; then + cp -Pp "$destination" "$BACKUP_RESULT" + else + cp -p "$destination" "$BACKUP_RESULT" + fi + info "Backed up $destination to $BACKUP_RESULT" +} + +copy_file_atomically() { + local source="$1" destination="$2" parent temporary + parent="$(dirname "$destination")" + mkdir -p "$parent" + temporary="$(mktemp "$parent/.macarchy.XXXXXX")" + + if [ -L "$source" ]; then + rm -f "$temporary" + cp -Pp "$source" "$temporary" + else + cp -p "$source" "$temporary" + fi + [ -L "$destination" ] && rm -f "$destination" + mv "$temporary" "$destination" +} + +install_managed_file() { + local component="$1" source="$2" destination="$3" + local entry original_backup='-' previous_checksum='' checksum actual had_entry=0 + + [ -f "$source" ] || [ -L "$source" ] || die "installer source is missing: $source" + + if [ -n "$SEEN_FILE" ]; then + printf '%s\n' "$destination" >> "$SEEN_FILE" + fi + + if entry="$(lookup_manifest_entry "$component" "$destination" 2>/dev/null)"; then + had_entry=1 + original_backup="${entry%%$'\t'*}" + previous_checksum="${entry#*$'\t'}" + fi + + if path_exists "$destination" && [ -d "$destination" ]; then + die "cannot replace directory with file: $destination" + fi + + checksum="$(file_hash "$source")" + if path_exists "$destination" && cmp -s "$source" "$destination"; then + if [ "$DRY_RUN" -eq 1 ]; then + if [ "$had_entry" -eq 1 ]; then + info "Unchanged: $destination" + else + info "Would register and back up matching file: $destination" + fi + else + if [ "$had_entry" -eq 0 ]; then + backup_current_path "$component" "$destination" + original_backup="$BACKUP_RESULT" + fi + record_manifest_entry "$component" "$destination" "$original_backup" "$checksum" + info "Unchanged: $destination" + fi + return + fi + + if [ "$had_entry" -eq 1 ] && path_exists "$destination"; then + actual="$(file_hash "$destination" 2>/dev/null || true)" + if [ "$actual" != "$previous_checksum" ]; then + if [ "$DRY_RUN" -eq 1 ]; then + info "Would keep locally modified managed file: $destination" + info "Use --force after reviewing the changes if you want to replace it." + return + fi + + if [ "$FORCE" -eq 0 ]; then + die "managed file has local edits: $destination (review it, then rerun with --force to replace it)" + fi + + backup_current_path "$component" "$destination" + info "Preserved locally edited version before forced update: $BACKUP_RESULT" + fi + fi + + if [ "$DRY_RUN" -eq 1 ]; then + if path_exists "$destination"; then + info "Would back up and replace: $destination" + else + info "Would install: $destination" + fi + return + fi + + if path_exists "$destination" && [ "$had_entry" -eq 0 ]; then + backup_current_path "$component" "$destination" + original_backup="$BACKUP_RESULT" + fi + + copy_file_atomically "$source" "$destination" + record_manifest_entry "$component" "$destination" "$original_backup" "$checksum" + info "Installed: $destination" +} + +seen_file_contains() { + local destination="$1" + [ -n "$SEEN_FILE" ] && grep -Fqx -- "$destination" "$SEEN_FILE" 2>/dev/null +} + +prune_removed_managed_files() { + local component="$1" manifest temporary destination backup expected actual + local unresolved=0 + + manifest="$(manifest_path "$component")" + [ -s "$manifest" ] || return 0 + + temporary='' + if [ "$DRY_RUN" -eq 0 ]; then + temporary="$(mktemp "$MANIFEST_DIR/.${component}-prune.XXXXXX")" + fi + + while IFS=$'\t' read -r destination backup expected; do + if seen_file_contains "$destination"; then + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\t%s\n' "$destination" "$backup" "$expected" >> "$temporary" + fi + continue + fi + + if path_exists "$destination"; then + actual="$(file_hash "$destination" 2>/dev/null || true)" + if [ "$actual" != "$expected" ]; then + warn "kept locally modified file no longer shipped by $component: $destination" + unresolved=$((unresolved + 1)) + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\t%s\n' "$destination" "$backup" "$expected" >> "$temporary" + fi + continue + fi + fi + + if [ "$backup" != '-' ]; then + if ! path_exists "$backup"; then + warn "kept $destination because its original backup is missing: $backup" + unresolved=$((unresolved + 1)) + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\t%s\n' "$destination" "$backup" "$expected" >> "$temporary" + fi + continue + fi + + if [ "$DRY_RUN" -eq 1 ]; then + info "Would restore file no longer shipped by $component: $destination" + else + restore_backup "$backup" "$destination" + info "Restored file no longer shipped by $component: $destination" + fi + elif path_exists "$destination"; then + if [ "$DRY_RUN" -eq 1 ]; then + info "Would remove file no longer shipped by $component: $destination" + else + rm -f "$destination" + info "Removed file no longer shipped by $component: $destination" + fi + fi + done < "$manifest" + + if [ "$DRY_RUN" -eq 0 ]; then + if [ -s "$temporary" ]; then + mv "$temporary" "$manifest" + else + rm -f "$temporary" "$manifest" + fi + fi + + [ "$unresolved" -eq 0 ] +} + +should_skip_source_path() { + case "$1" in + */.DS_Store|*/node_modules/*|*/.raycast/*|*/dist/*|*/raycast-env.d.ts|*/.state/*|*/sketchybar/theme.sh|*/.backgrounds.json|*/.current|*/.shell-env|*/.nvim-colorscheme) + return 0 + ;; + *) + return 1 + ;; + esac +} + +is_legacy_raycast_runtime_path() { + local path="$1" directory + + for directory in \ + "$CONFIG_HOME/raycast/extensions/theme-switcher" \ + "$CONFIG_HOME/raycast/extensions/macarchy-theme-switcher" \ + "$HOME/.config/raycast/extensions/theme-switcher" \ + "$HOME/.config/raycast/extensions/macarchy-theme-switcher"; do + case "$path" in + "$directory"|"$directory"/*) return 0 ;; + esac + done + + return 1 +} + +raycast_runtime_relative_path() { + local path="$1" directory + + for directory in \ + "$CONFIG_HOME/raycast/extensions/theme-switcher" \ + "$CONFIG_HOME/raycast/extensions/macarchy-theme-switcher" \ + "$HOME/.config/raycast/extensions/theme-switcher" \ + "$HOME/.config/raycast/extensions/macarchy-theme-switcher"; do + case "$path" in + "$directory"/*) + printf '%s\n' "${path#"$directory"/}" + return 0 + ;; + esac + done + + return 1 +} + +is_raycast_development_file() { + case "$1" in + .gitignore|.nvmrc|.prettierrc|README.md|eslint.config.mjs|package-lock.json|raycast-env.d.ts|tsconfig.json|scripts/*|src/*|test/*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +record_raycast_runtime_recovery() { + local destination="$1" backup="$2" manifest temporary current current_backup checksum + + manifest="$RAYCAST_RECOVERY_MANIFEST" + if [ -f "$manifest" ]; then + while IFS=$'\t' read -r current current_backup checksum; do + [ "$current" = "$destination" ] && return 0 + done < "$manifest" + fi + + checksum="$(file_hash "$backup" 2>/dev/null || true)" + [ -n "$checksum" ] || checksum='-' + mkdir -p "$(dirname "$manifest")" + temporary="$(mktemp "$(dirname "$manifest")/.raycast-runtime.XXXXXX")" + [ -f "$manifest" ] && cat "$manifest" > "$temporary" + printf '%s\t%s\t%s\n' "$destination" "$backup" "$checksum" >> "$temporary" + mv "$temporary" "$manifest" + info "Kept Raycast runtime recovery information in: $manifest" +} + +release_legacy_raycast_runtime() { + local manifest temporary destination backup expected relative actual runtime + + manifest="$(manifest_path raycast)" + [ -s "$manifest" ] || return 0 + + temporary='' + if [ "$DRY_RUN" -eq 0 ]; then + temporary="$(mktemp "$MANIFEST_DIR/.raycast-runtime.XXXXXX")" + fi + + while IFS=$'\t' read -r destination backup expected; do + if is_legacy_raycast_runtime_path "$destination"; then + # A dry run still reaches the generic pruning pass. Mark released paths + # as seen so that pass does not claim they would be restored or removed. + if [ -n "$SEEN_FILE" ]; then + printf '%s\n' "$destination" >> "$SEEN_FILE" + fi + + relative="$(raycast_runtime_relative_path "$destination" 2>/dev/null || true)" + if is_raycast_development_file "$relative" && path_exists "$destination"; then + actual="$(file_hash "$destination" 2>/dev/null || true)" + if [ "$actual" = "$expected" ]; then + if [ "$DRY_RUN" -eq 1 ]; then + info "Would remove old Raycast development file: $destination" + else + rm -f "$destination" + info "Removed old Raycast development file: $destination" + fi + else + warn "kept modified old Raycast development file: $destination" + fi + fi + + if [ "$backup" != '-' ]; then + if [ "$DRY_RUN" -eq 1 ]; then + info "Would retain Raycast runtime recovery information for: $destination" + else + record_raycast_runtime_recovery "$destination" "$backup" + fi + fi + + if [ "$DRY_RUN" -eq 1 ]; then + info "Would stop managing Raycast runtime file: $destination" + else + info "Stopped managing Raycast runtime file: $destination" + fi + continue + fi + + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\t%s\n' "$destination" "$backup" "$expected" >> "$temporary" + fi + done < "$manifest" + + if [ "$DRY_RUN" -eq 0 ]; then + if [ -s "$temporary" ]; then + mv "$temporary" "$manifest" + else + rm -f "$temporary" "$manifest" + fi + + for runtime in \ + "$CONFIG_HOME/raycast/extensions/theme-switcher" \ + "$CONFIG_HOME/raycast/extensions/macarchy-theme-switcher" \ + "$HOME/.config/raycast/extensions/theme-switcher" \ + "$HOME/.config/raycast/extensions/macarchy-theme-switcher"; do + rmdir "$runtime/scripts" "$runtime/src" "$runtime/test" 2>/dev/null || true + done + fi +} + +quarantine_raycast_runtime_dependencies() { + local runtime dependencies backup_parent backup + + for runtime in \ + "$CONFIG_HOME/raycast/extensions/theme-switcher" \ + "$CONFIG_HOME/raycast/extensions/macarchy-theme-switcher" \ + "$HOME/.config/raycast/extensions/theme-switcher" \ + "$HOME/.config/raycast/extensions/macarchy-theme-switcher"; do + dependencies="$runtime/node_modules" + path_exists "$dependencies" || continue + + BACKUP_COUNTER=$((BACKUP_COUNTER + 1)) + backup_parent="$BACKUP_DIR/$BACKUP_RUN_ID/raycast" + backup="$backup_parent/${BACKUP_COUNTER}-runtime-node_modules" + + if [ "$DRY_RUN" -eq 1 ]; then + info "Would move unsafe Raycast runtime dependencies to: $backup" + continue + fi + + mkdir -p "$backup_parent" + mv "$dependencies" "$backup" + info "Moved unsafe Raycast runtime dependencies to: $backup" + done +} + +install_tree() { + local component="$1" source_dir="$2" destination_dir="$3" + local source_file relative found=0 + + [ -d "$source_dir" ] || die "installer source directory is missing: $source_dir" + + while IFS= read -r -d '' source_file; do + should_skip_source_path "$source_file" && continue + found=1 + relative="${source_file#"$source_dir"/}" + install_managed_file "$component" "$source_file" "$destination_dir/$relative" + done < <(find "$source_dir" \( -type f -o -type l \) -print0) + + [ "$found" -eq 1 ] || warn "no files found in $source_dir" +} + +install_legacy_ghostty_shim() { + local legacy_config="$CONFIG_HOME/ghostty/config" + local macos_config="$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty" + local temporary + + # Ghostty still loads the pre-1.2.3 filename after config.ghostty. Replacing + # an existing legacy file with a managed, empty shim prevents stale settings + # from overriding the new config while allowing uninstall to restore it. + if path_exists "$legacy_config" || lookup_manifest_entry terminal "$legacy_config" >/dev/null 2>&1; then + temporary="$(mktemp "${TMPDIR:-/tmp}/macarchy-ghostty-legacy.XXXXXX")" + printf '%s\n' \ + '# Legacy Ghostty compatibility file managed by Macarchy.' \ + '# Put local overrides in config.ghostty or a file imported from it.' \ + > "$temporary" + install_managed_file terminal "$temporary" "$legacy_config" + rm -f "$temporary" + fi + + if [ "$(uname -s)" = 'Darwin' ] && [ -f "$macos_config" ] \ + && grep -Ev '^[[:space:]]*(#|$)' "$macos_config" >/dev/null 2>&1; then + warn "Ghostty also loads $macos_config after the XDG config; settings there may override Macarchy" + fi +} + +ensure_skhd_local_config() { + local public_config="$CONFIG_HOME/skhd/skhdrc" + local local_config="$CONFIG_HOME/skhd/local.skhdrc" + + path_exists "$local_config" && return 0 + + # On the first install, keep an existing unmanaged shortcut file active by + # loading it as the machine-specific override. A later install must not copy + # Macarchy's own managed public file back into that override. + if ! lookup_manifest_entry window-manager "$public_config" >/dev/null 2>&1 \ + && path_exists "$public_config" \ + && [ ! -d "$public_config" ]; then + if [ "$DRY_RUN" -eq 1 ]; then + info "Would preserve existing shortcuts in: $local_config" + return 0 + fi + + copy_file_atomically "$public_config" "$local_config" + info "Preserved existing shortcuts in: $local_config" + return 0 + fi + + if [ "$DRY_RUN" -eq 1 ]; then + info "Would create local shortcut file: $local_config" + return 0 + fi + + mkdir -p "$(dirname "$local_config")" + printf '%s\n' \ + '# Machine-specific skhd shortcuts. This file is not managed by Macarchy.' \ + '# Copy optional bindings from examples/ here and customise them freely.' \ + > "$local_config" + chmod 600 "$local_config" + info "Created local shortcut file: $local_config" +} + +install_zsh_bootstrap() { + local temporary + temporary="$(mktemp "${TMPDIR:-/tmp}/macarchy-zshenv.XXXXXX")" + printf '%s\n' \ + '# Macarchy shell bootstrap. Remove this file or run ./install uninstall shell to restore the previous one.' \ + > "$temporary" + printf 'export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-%q}\n' "$CONFIG_HOME" >> "$temporary" + printf '%s\n' \ + 'export ZDOTDIR="${ZDOTDIR:-$XDG_CONFIG_HOME/zsh}"' \ + '[[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv"' >> "$temporary" + install_managed_file shell "$temporary" "$HOME/.zshenv" + rm -f "$temporary" +} + +install_component_files() { + local component="$1" + + case "$component" in + window-manager) + ensure_skhd_local_config + install_tree "$component" "$REPO_ROOT/yabai" "$CONFIG_HOME/yabai" + install_managed_file "$component" "$REPO_ROOT/skhd/skhdrc" "$CONFIG_HOME/skhd/skhdrc" + install_tree "$component" "$REPO_ROOT/borders" "$CONFIG_HOME/borders" + ;; + sketchybar) + install_tree "$component" "$REPO_ROOT/sketchybar" "$CONFIG_HOME/sketchybar" + ;; + terminal) + install_managed_file "$component" "$REPO_ROOT/ghostty/config.ghostty" "$CONFIG_HOME/ghostty/config.ghostty" + install_legacy_ghostty_shim + ;; + shell) + if path_exists "$HOME/.zshrc" || path_exists "$HOME/.zprofile"; then + warn "the shell component sets ZDOTDIR to $CONFIG_HOME/zsh, so existing ~/.zshrc and ~/.zprofile files will no longer load automatically" + warn "move any settings you still need into $CONFIG_HOME/zsh/local.zsh after installation" + fi + install_tree "$component" "$REPO_ROOT/zsh" "$CONFIG_HOME/zsh" + install_zsh_bootstrap + ;; + nvim) + install_tree "$component" "$REPO_ROOT/nvim" "$CONFIG_HOME/nvim" + ;; + utilities) + install_tree "$component" "$REPO_ROOT/btop" "$CONFIG_HOME/btop" + install_tree "$component" "$REPO_ROOT/fastfetch" "$CONFIG_HOME/fastfetch" + ;; + themes) + install_tree "$component" "$REPO_ROOT/themes" "$CONFIG_HOME/themes" + install_managed_file "$component" "$REPO_ROOT/bin/theme-switch" "$BIN_HOME/theme-switch" + install_managed_file "$component" "$REPO_ROOT/bin/set-wallpaper" "$BIN_HOME/set-wallpaper" + if [ -d "$REPO_ROOT/lib/macarchy" ]; then + install_tree "$component" "$REPO_ROOT/lib/macarchy" "$DATA_HOME/macarchy/lib" + fi + ;; + raycast) + # Raycast writes compiled commands to ~/.config/raycast/extensions. + # Keep source and npm dependencies elsewhere so the command cannot load + # a second copy of React from its generated runtime directory. + quarantine_raycast_runtime_dependencies + release_legacy_raycast_runtime + install_tree "$component" "$REPO_ROOT/raycast-theme" "$RAYCAST_SOURCE_DIR" + ;; + esac +} + +print_next_step() { + if [ "$CONFIG_HOME" != "$HOME/.config" ] \ + && { [ "$1" = 'window-manager' ] || [ "$1" = 'sketchybar' ] || [ "$1" = 'terminal' ]; }; then + info "Before using this component, expose the custom config path with: launchctl setenv XDG_CONFIG_HOME '$CONFIG_HOME'" + fi + + case "$1" in + window-manager) + info 'Next: grant Accessibility access, then start only the services you want with `yabai --start-service`, `skhd --start-service` and `brew services start borders`.' + info 'SIP and the yabai scripting addition are optional and are never changed by this installer.' + ;; + sketchybar) + info 'Next: start SketchyBar when ready with `brew services start sketchybar`.' + ;; + themes) + info "Next: apply a theme explicitly with '$BIN_HOME/theme-switch' ." + ;; + raycast) + info "Next: run \`npm install && npm run dev\` in $RAYCAST_SOURCE_DIR." + ;; + esac +} + +install_components() { + local component result=0 + + for component in "${SELECTED_COMPONENTS[@]}"; do + info "" + info "Installing $component" + SEEN_FILE="$(mktemp "${TMPDIR:-/tmp}/macarchy-seen.XXXXXX")" + install_dependencies "$component" + install_component_files "$component" + if ! prune_removed_managed_files "$component"; then + result=1 + fi + rm -f "$SEEN_FILE" + SEEN_FILE="" + print_next_step "$component" + done + + return "$result" +} + +status_component() { + local component="$1" manifest destination backup expected actual + local missing=0 modified=0 total=0 + manifest="$(manifest_path "$component")" + + if [ ! -s "$manifest" ]; then + printf '%-17s %s\n' "$component" 'not installed' + return 1 + fi + + while IFS=$'\t' read -r destination backup expected; do + total=$((total + 1)) + if ! path_exists "$destination"; then + missing=$((missing + 1)) + continue + fi + actual="$(file_hash "$destination" 2>/dev/null || true)" + [ "$actual" = "$expected" ] || modified=$((modified + 1)) + done < "$manifest" + + if [ "$missing" -eq 0 ] && [ "$modified" -eq 0 ]; then + printf '%-17s installed (%d managed files)\n' "$component" "$total" + return 0 + fi + + printf '%-17s needs attention (%d missing, %d modified, %d managed)\n' \ + "$component" "$missing" "$modified" "$total" + return 1 +} + +status_components() { + local component result=0 + for component in "${SELECTED_COMPONENTS[@]}"; do + if ! status_component "$component"; then + result=1 + fi + done + return "$result" +} + +restore_backup() { + local backup="$1" destination="$2" + copy_file_atomically "$backup" "$destination" +} + +uninstall_component() { + local component="$1" manifest temporary destination backup expected actual + local unresolved=0 + manifest="$(manifest_path "$component")" + + if [ ! -s "$manifest" ]; then + info "$component is not installed by Macarchy." + return 0 + fi + + temporary='' + if [ "$DRY_RUN" -eq 0 ]; then + temporary="$(mktemp "$MANIFEST_DIR/.${component}-uninstall.XXXXXX")" + fi + + while IFS=$'\t' read -r destination backup expected; do + if path_exists "$destination"; then + actual="$(file_hash "$destination" 2>/dev/null || true)" + if [ "$actual" != "$expected" ]; then + warn "kept modified file: $destination" + unresolved=$((unresolved + 1)) + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\t%s\n' "$destination" "$backup" "$expected" >> "$temporary" + fi + continue + fi + fi + + if [ "$backup" != '-' ]; then + if ! path_exists "$backup"; then + warn "kept $destination because its original backup is missing: $backup" + unresolved=$((unresolved + 1)) + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\t%s\n' "$destination" "$backup" "$expected" >> "$temporary" + fi + continue + fi + + if [ "$DRY_RUN" -eq 1 ]; then + info "Would restore: $destination" + else + restore_backup "$backup" "$destination" + info "Restored: $destination" + fi + elif path_exists "$destination"; then + if [ "$DRY_RUN" -eq 1 ]; then + info "Would remove: $destination" + else + rm -f "$destination" + info "Removed: $destination" + fi + fi + done < "$manifest" + + if [ "$DRY_RUN" -eq 0 ]; then + if [ "$unresolved" -eq 0 ]; then + rm -f "$temporary" "$manifest" + info "Uninstalled $component configuration. Homebrew packages were left in place." + else + mv "$temporary" "$manifest" + warn "$component was only partly uninstalled; resolve modified files and run uninstall again" + fi + fi + + [ "$unresolved" -eq 0 ] +} + +uninstall_generated_theme_files() { + local manifest="$MACARCHY_STATE_DIR/generated/themes.tsv" + local temporary destination expected actual + local unresolved=0 + + [ -s "$manifest" ] || return 0 + + temporary='' + if [ "$DRY_RUN" -eq 0 ]; then + temporary="$(mktemp "$(dirname "$manifest")/.themes-uninstall.XXXXXX")" + fi + + while IFS=$'\t' read -r destination expected; do + [ -n "$destination" ] || continue + + if path_exists "$destination"; then + if [ -d "$destination" ]; then + warn "kept generated path because it is now a directory: $destination" + unresolved=$((unresolved + 1)) + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\n' "$destination" "$expected" >> "$temporary" + fi + continue + fi + + actual="$(file_hash "$destination" 2>/dev/null || true)" + if [ "$actual" != "$expected" ]; then + warn "kept modified generated file: $destination" + unresolved=$((unresolved + 1)) + if [ "$DRY_RUN" -eq 0 ]; then + printf '%s\t%s\n' "$destination" "$expected" >> "$temporary" + fi + continue + fi + + if [ "$DRY_RUN" -eq 1 ]; then + info "Would remove generated theme file: $destination" + else + rm -f "$destination" + info "Removed generated theme file: $destination" + fi + fi + done < "$manifest" + + if [ "$DRY_RUN" -eq 0 ]; then + if [ -s "$temporary" ]; then + mv "$temporary" "$manifest" + else + rm -f "$temporary" "$manifest" + fi + fi + + [ "$unresolved" -eq 0 ] +} + +uninstall_components() { + local component result=0 + for component in "${SELECTED_COMPONENTS[@]}"; do + if ! uninstall_component "$component"; then + result=1 + fi + if [ "$component" = 'themes' ] && ! uninstall_generated_theme_files; then + result=1 + fi + done + return "$result" +} + +main() { + local first argument + local -a arguments=() + + for argument in "$@"; do + case "$argument" in + --dry-run) + DRY_RUN=1 + ;; + --force) + FORCE=1 + ;; + -h|--help) + usage + return 0 + ;; + --list) + arguments+=(list) + ;; + --) + ;; + -* ) + die "unknown option '$argument'" + ;; + *) + arguments+=("$argument") + ;; + esac + done + + if [ "${#arguments[@]}" -eq 0 ]; then + usage + return 1 + fi + + first="${arguments[0]}" + case "$first" in + list) + [ "$FORCE" -eq 0 ] || die '--force can only be used while installing components' + [ "${#arguments[@]}" -eq 1 ] || die '--list does not accept component names' + list_components + ;; + install) + select_components "${arguments[@]:1}" + install_components + ;; + status) + [ "$FORCE" -eq 0 ] || die '--force can only be used while installing components' + if [ "${#arguments[@]}" -eq 1 ]; then + select_components all + else + select_components "${arguments[@]:1}" + fi + status_components + ;; + uninstall) + [ "$FORCE" -eq 0 ] || die '--force can only be used while installing components' + select_components "${arguments[@]:1}" + uninstall_components + ;; + *) + select_components "${arguments[@]}" + install_components + ;; + esac +} + +main "$@" diff --git a/launchagents/com.asmvik.yabai.plist b/launchagents/com.asmvik.yabai.plist deleted file mode 100644 index 18453d5..0000000 --- a/launchagents/com.asmvik.yabai.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Label - com.asmvik.yabai - ProgramArguments - - /opt/homebrew/bin/yabai - - EnvironmentVariables - - PATH - /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin - - RunAtLoad - - KeepAlive - - SuccessfulExit - - Crashed - - - StandardOutPath - /tmp/yabai.out.log - StandardErrorPath - /tmp/yabai.err.log - ProcessType - Interactive - Nice - -20 - - diff --git a/launchagents/com.koekeishiya.skhd.plist b/launchagents/com.koekeishiya.skhd.plist deleted file mode 100644 index 6c0c191..0000000 --- a/launchagents/com.koekeishiya.skhd.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Label - com.koekeishiya.skhd - ProgramArguments - - /opt/homebrew/bin/skhd - - EnvironmentVariables - - PATH - /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin - - RunAtLoad - - KeepAlive - - SuccessfulExit - - Crashed - - - StandardOutPath - /tmp/skhd.out.log - StandardErrorPath - /tmp/skhd.err.log - ProcessType - Interactive - Nice - -20 - - diff --git a/launchagents/homebrew.mxcl.sketchybar.plist b/launchagents/homebrew.mxcl.sketchybar.plist deleted file mode 100644 index d47e4c8..0000000 --- a/launchagents/homebrew.mxcl.sketchybar.plist +++ /dev/null @@ -1,37 +0,0 @@ - - - - - EnvironmentVariables - - LANG - en_US.UTF-8 - PATH - /opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin - - KeepAlive - - Label - homebrew.mxcl.sketchybar - LimitLoadToSessionType - - Aqua - Background - LoginWindow - StandardIO - System - - ProcessType - Interactive - ProgramArguments - - /opt/homebrew/opt/sketchybar/bin/sketchybar - - RunAtLoad - - StandardErrorPath - /opt/homebrew/var/log/sketchybar/sketchybar.err.log - StandardOutPath - /opt/homebrew/var/log/sketchybar/sketchybar.out.log - - diff --git a/lib/macarchy/apply/appearance.sh b/lib/macarchy/apply/appearance.sh new file mode 100644 index 0000000..952a3b2 --- /dev/null +++ b/lib/macarchy/apply/appearance.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +macarchy_apply_appearance() { + [[ "$(uname -s)" == "Darwin" ]] || return 0 + if osascript -e "tell application \"System Events\" to tell appearance preferences to set dark mode to $THEME_DARK_MODE" >/dev/null 2>&1; then + macarchy_info " macOS appearance: $([[ "$THEME_DARK_MODE" == true ]] && printf 'dark' || printf 'light')" + else + macarchy_warn "macOS appearance could not be changed" + fi +} + +macarchy_apply_wallpaper() { + local relative="$1" + local setter + + [[ -n "$relative" ]] || { macarchy_info " Wallpaper: skipped"; return 0; } + setter="$(macarchy_command set-wallpaper || true)" + [[ -n "$setter" && -x "$setter" ]] || setter="$MACARCHY_BIN_DIR/set-wallpaper" + [[ -x "$setter" ]] || { + macarchy_warn "set-wallpaper is not installed" + return 0 + } + + if "$setter" "$MACARCHY_THEME_DIR/$relative"; then + macarchy_info " Wallpaper: $relative" + else + macarchy_error "wallpaper could not be changed" + return 1 + fi +} diff --git a/lib/macarchy/apply/desktop.sh b/lib/macarchy/apply/desktop.sh new file mode 100644 index 0000000..fbc3700 --- /dev/null +++ b/lib/macarchy/apply/desktop.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +macarchy_apply_borders() { + local generated="$MACARCHY_GENERATED_DIR/borders.conf" + local borders_bin + + borders_bin="$(macarchy_command borders || true)" + [[ -f "$MACARCHY_CONFIG_HOME/borders/bordersrc" || -n "$borders_bin" ]] || return 0 + + if ! macarchy_write_generated "$generated" </dev/null 2>&1; then + "$borders_bin" active_color="$THEME_BORDER_ACTIVE" \ + inactive_color="$THEME_BORDER_INACTIVE" width="$THEME_BORDER_WIDTH" >/dev/null 2>&1 || true + fi + macarchy_info " Borders: $THEME_BORDER_ACTIVE" +} + +macarchy_apply_sketchybar() { + local config_dir="$MACARCHY_CONFIG_HOME/sketchybar" + local theme_file="$config_dir/theme.sh" + local text_color="${THEME_SKETCHYBAR_TEXT_COLOR:-}" + local foreground + local sketchybar_bin + + [[ -d "$config_dir" ]] || return 0 + if [[ -z "$text_color" ]]; then + foreground="$(macarchy_ghostty_colour "$MACARCHY_THEME_DIR/ghostty.conf" foreground '#ffffff')" + text_color="0xff${foreground#\#}" + fi + if ! macarchy_write_generated "$theme_file" </dev/null 2>&1 || true + macarchy_info " SketchyBar: colours updated" +} + +macarchy_apply_shell() { + local shell_env="$MACARCHY_GENERATED_DIR/shell-env" + local border_hex="${THEME_BORDER_ACTIVE#0x}" + local border_rgb="${border_hex#??}" + local palette_file="$MACARCHY_THEME_DIR/ghostty.conf" + local palette4_hex accent_hex accent_index candidate candidate_index + local foreground_index=15 + + [[ "$THEME_DARK_MODE" == "false" ]] && foreground_index=0 + palette4_hex="$(macarchy_ghostty_palette "$palette_file" 4 '#ffffff')" + accent_index="" + accent_hex="" + for candidate in "${THEME_FIREFOX_ACCENT:-}" "#$border_rgb" "$palette4_hex"; do + [[ "$candidate" =~ ^#[0-9A-Fa-f]{6}$ ]] || continue + candidate_index="$(macarchy_ghostty_palette_index "$palette_file" "$candidate")" + [[ -n "$candidate_index" ]] || continue + accent_index="$candidate_index" + accent_hex="${candidate#\#}" + break + done + if [[ -z "$accent_index" ]]; then + accent_index=4 + accent_hex="${palette4_hex#\#}" + fi + if ! macarchy_write_generated "$shell_env" </dev/null 2>&1 || pkill -USR2 -x Ghostty >/dev/null 2>&1 || true + macarchy_info " Ghostty: colours updated" +} diff --git a/lib/macarchy/apply/raycast.sh b/lib/macarchy/apply/raycast.sh new file mode 100644 index 0000000..7b7f7a5 --- /dev/null +++ b/lib/macarchy/apply/raycast.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +macarchy_apply_raycast() { + local bundle_id="com.raycast.macos" + local defaults_bin="${MACARCHY_DEFAULTS_BIN:-}" + local raycast_app="${MACARCHY_RAYCAST_APP:-/Applications/Raycast.app}" + local preferences="$HOME/Library/Preferences/$bundle_id.plist" + local current_theme="bundled-raycast-light" + + [[ "$(uname -s)" == "Darwin" ]] || return 0 + [[ -d "$raycast_app" || -f "$preferences" ]] || return 0 + [[ -n "$defaults_bin" ]] || defaults_bin="$(macarchy_command defaults || true)" + [[ -n "$defaults_bin" ]] || return 0 + [[ "$THEME_DARK_MODE" == "true" ]] && current_theme="bundled-raycast-dark" + + "$defaults_bin" write "$bundle_id" raycastShouldFollowSystemAppearance -bool true && + "$defaults_bin" write "$bundle_id" raycastCurrentThemeId -string "$current_theme" && + "$defaults_bin" write "$bundle_id" raycastCurrentThemeIdDarkAppearance -string bundled-raycast-dark && + "$defaults_bin" write "$bundle_id" raycastCurrentThemeIdLightAppearance -string bundled-raycast-light || { + macarchy_error "Raycast appearance defaults could not be updated" + return 1 + } + + macarchy_info " Raycast: $current_theme" +} diff --git a/lib/macarchy/apply/tools.sh b/lib/macarchy/apply/tools.sh new file mode 100644 index 0000000..61d1256 --- /dev/null +++ b/lib/macarchy/apply/tools.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +macarchy_apply_neovim() { + local config_dir="$MACARCHY_CONFIG_HOME/nvim" + local state_file="$MACARCHY_GENERATED_DIR/nvim-colorscheme" + + [[ -n "$THEME_NVIM_COLORSCHEME" && -d "$config_dir" ]] || return 0 + if ! printf '%s\n' "$THEME_NVIM_COLORSCHEME" | macarchy_write_generated "$state_file"; then + macarchy_error "Neovim theme state could not be written" + return 1 + fi + pkill -WINCH -x nvim >/dev/null 2>&1 || true + macarchy_info " Neovim: $THEME_NVIM_COLORSCHEME" +} + +macarchy_write_btop_theme() { + local ghostty_file="$MACARCHY_THEME_DIR/ghostty.conf" + local target="$MACARCHY_CONFIG_HOME/btop/themes/macarchy.theme" + local background foreground selection accent muted green yellow red cyan purple orange + + background="$(macarchy_ghostty_colour "$ghostty_file" background '#000000')" + foreground="$(macarchy_ghostty_colour "$ghostty_file" foreground '#ffffff')" + selection="$(macarchy_ghostty_colour "$ghostty_file" selection-background "$background")" + accent="$(macarchy_ghostty_palette "$ghostty_file" 4 "$foreground")" + muted="$(macarchy_ghostty_palette "$ghostty_file" 8 "$foreground")" + green="$(macarchy_ghostty_palette "$ghostty_file" 2 "$accent")" + yellow="$(macarchy_ghostty_palette "$ghostty_file" 3 "$accent")" + red="$(macarchy_ghostty_palette "$ghostty_file" 1 "$accent")" + cyan="$(macarchy_ghostty_palette "$ghostty_file" 6 "$accent")" + purple="$(macarchy_ghostty_palette "$ghostty_file" 5 "$accent")" + orange="$(macarchy_ghostty_palette "$ghostty_file" 11 "$yellow")" + + macarchy_write_generated "$target" </dev/null 2>&1 || true + macarchy_info " btop: colours updated" +} diff --git a/lib/macarchy/colours.sh b/lib/macarchy/colours.sh new file mode 100644 index 0000000..8859f4d --- /dev/null +++ b/lib/macarchy/colours.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +macarchy_ghostty_colour() { + local file="$1" + local key="$2" + local fallback="$3" + local value + + value="$(awk -F= -v key="$key" ' + { + name=$1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) + if (name == key) { + value=substr($0, index($0, "=") + 1) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + print value + exit + } + } + ' "$file" 2>/dev/null)" + + [[ "$value" =~ ^#[0-9A-Fa-f]{6}$ ]] && printf '%s\n' "$value" || printf '%s\n' "$fallback" +} + +macarchy_ghostty_palette() { + local file="$1" + local index="$2" + local fallback="$3" + local value + + value="$(awk -F= -v wanted="$index" ' + /^[[:space:]]*palette[[:space:]]*=/ { + palette_index=$2 + colour=$3 + gsub(/[[:space:]]/, "", palette_index) + gsub(/[[:space:]]/, "", colour) + if (palette_index == wanted && colour ~ /^#[0-9A-Fa-f]{6}$/) { + print colour + exit + } + } + ' "$file" 2>/dev/null)" + + [[ "$value" =~ ^#[0-9A-Fa-f]{6}$ ]] && printf '%s\n' "$value" || printf '%s\n' "$fallback" +} + +macarchy_ghostty_palette_index() { + local file="$1" + local wanted="${2#\#}" + + awk -F= -v wanted="$wanted" ' + BEGIN { wanted=tolower(wanted) } + /^[[:space:]]*palette[[:space:]]*=/ { + palette_index=$2 + colour=$3 + gsub(/[[:space:]]/, "", palette_index) + gsub(/[[:space:]]/, "", colour) + sub(/^#/, "", colour) + if (tolower(colour) == wanted) { + print palette_index + exit + } + } + ' "$file" 2>/dev/null +} + +# Ghostty theme files are restricted to colours and a small set of visual +# settings used by the original packs. Commands, fonts and input behaviour are +# still rejected. +macarchy_ghostty_theme_valid() { + local file="$1" + awk -F= ' + /^[[:space:]]*($|#)/ { next } + { + key=$1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", key) + value=substr($0, index($0, "=") + 1) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + if (key == "palette") { + if (value !~ /^[0-9]+=#[0-9A-Fa-f]{6}$/) exit 1 + } else if (key ~ /^(background|foreground|cursor-color|cursor-text|selection-background|selection-foreground)$/) { + if (value !~ /^#[0-9A-Fa-f]{6}$/) exit 1 + } else if (key == "background-opacity") { + if (value !~ /^(0([.][0-9]+)?|1([.]0+)?)$/) exit 1 + } else if (key ~ /^(window-padding-x|window-padding-y)$/) { + if (value !~ /^[0-9]+([.][0-9]+)?(,[[:space:]]*[0-9]+([.][0-9]+)?)?$/) exit 1 + } else if (key == "window-padding-balance") { + if (value !~ /^(true|false)$/) exit 1 + } else { + exit 1 + } + } + ' "$file" +} diff --git a/lib/macarchy/common.sh b/lib/macarchy/common.sh new file mode 100644 index 0000000..bc432fe --- /dev/null +++ b/lib/macarchy/common.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash + +MACARCHY_CONFIG_HOME="${MACARCHY_CONFIG_HOME:-${XDG_CONFIG_HOME:-$HOME/.config}}" +MACARCHY_DATA_HOME="${MACARCHY_DATA_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}}" +MACARCHY_STATE_HOME="${MACARCHY_STATE_HOME:-${XDG_STATE_HOME:-$HOME/.local/state}}" +MACARCHY_CACHE_HOME="${MACARCHY_CACHE_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}}" +MACARCHY_GENERATED_DIR="${MACARCHY_GENERATED_DIR:-$MACARCHY_CONFIG_HOME/macarchy/generated}" +MACARCHY_GENERATED_MANIFEST="${MACARCHY_GENERATED_MANIFEST:-$MACARCHY_STATE_HOME/macarchy/generated/themes.tsv}" + +macarchy_info() { + printf '%s\n' "$*" +} + +macarchy_warn() { + printf 'warning: %s\n' "$*" >&2 +} + +macarchy_error() { + printf 'error: %s\n' "$*" >&2 +} + +macarchy_command() { + command -v "$1" 2>/dev/null +} + +# Write through a temporary file so an interrupted update cannot leave a +# partially written configuration behind. Generated files are private to the +# current user and should not follow a pre-existing symlink unexpectedly. +macarchy_atomic_write() { + local target="$1" + local mode="${2:-600}" + local directory temporary + + directory="$(dirname "$target")" + if [[ -L "$target" || -d "$target" ]]; then + macarchy_error "refusing to replace non-file path: $target" + return 1 + fi + mkdir -p "$directory" || { + macarchy_error "could not create directory: $directory" + return 1 + } + temporary="$(mktemp "$directory/.macarchy.XXXXXX")" || { + macarchy_error "could not create a temporary file in $directory" + return 1 + } + if ! cat > "$temporary"; then + rm -f "$temporary" + macarchy_error "could not write temporary file for $target" + return 1 + fi + if ! chmod "$mode" "$temporary" || ! mv "$temporary" "$target"; then + rm -f "$temporary" + macarchy_error "could not replace $target" + return 1 + fi +} + +macarchy_file_hash() { + local file="$1" + local output checksum + + if command -v shasum >/dev/null 2>&1; then + output="$(shasum -a 256 < "$file")" || { + macarchy_error "could not checksum generated file: $file" + return 1 + } + elif command -v sha256sum >/dev/null 2>&1; then + output="$(sha256sum < "$file")" || { + macarchy_error "could not checksum generated file: $file" + return 1 + } + else + macarchy_error "shasum or sha256sum is required to track generated files" + return 1 + fi + + checksum="${output%% *}" + [[ "$checksum" =~ ^[0-9A-Fa-f]{64}$ ]] || { + macarchy_error "checksum command returned invalid output for $file" + return 1 + } + printf '%s\n' "$checksum" +} + +# Keep a checksum beside every generated file. The installer uses this list to +# remove Macarchy-owned output while preserving anything the user later edits. +macarchy_record_generated_file() { + local target="$1" + local checksum manifest_dir temporary current current_checksum + + [[ -f "$target" && ! -L "$target" ]] || { + macarchy_error "cannot record missing generated file: $target" + return 1 + } + case "$target" in + *$'\t'*|*$'\n'*) + macarchy_error "generated file path contains a tab or newline: $target" + return 1 + ;; + esac + + checksum="$(macarchy_file_hash "$target")" || return 1 + manifest_dir="$(dirname "$MACARCHY_GENERATED_MANIFEST")" + mkdir -p "$manifest_dir" || { + macarchy_error "could not create generated-file state directory: $manifest_dir" + return 1 + } + temporary="$(mktemp "$manifest_dir/.themes.XXXXXX")" || { + macarchy_error "could not create generated-file manifest" + return 1 + } + + if [[ -L "$MACARCHY_GENERATED_MANIFEST" ]]; then + rm -f "$temporary" + macarchy_error "generated-file manifest must not be a symlink" + return 1 + elif [[ -f "$MACARCHY_GENERATED_MANIFEST" ]]; then + if [[ ! -r "$MACARCHY_GENERATED_MANIFEST" ]]; then + rm -f "$temporary" + macarchy_error "generated-file manifest is not readable" + return 1 + fi + while IFS=$'\t' read -r current current_checksum; do + [[ -n "$current" ]] || continue + [[ "$current" == "$target" ]] && continue + printf '%s\t%s\n' "$current" "$current_checksum" >> "$temporary" || { + rm -f "$temporary" + return 1 + } + done < "$MACARCHY_GENERATED_MANIFEST" + elif [[ -e "$MACARCHY_GENERATED_MANIFEST" ]]; then + rm -f "$temporary" + macarchy_error "generated-file manifest is not a regular file" + return 1 + fi + + printf '%s\t%s\n' "$target" "$checksum" >> "$temporary" || { + rm -f "$temporary" + return 1 + } + if ! macarchy_atomic_write "$MACARCHY_GENERATED_MANIFEST" < "$temporary"; then + rm -f "$temporary" + return 1 + fi + rm -f "$temporary" +} + +macarchy_write_generated() { + local target="$1" + local mode="${2:-600}" + + macarchy_atomic_write "$target" "$mode" || return 1 + macarchy_record_generated_file "$target" +} + +macarchy_trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' +} diff --git a/lib/macarchy/theme.sh b/lib/macarchy/theme.sh new file mode 100644 index 0000000..acb3da5 --- /dev/null +++ b/lib/macarchy/theme.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash + +MACARCHY_THEMES_DIR="${MACARCHY_THEMES_DIR:-$MACARCHY_CONFIG_HOME/themes}" +MACARCHY_THEME_RUNTIME_DIR="${MACARCHY_THEME_RUNTIME_DIR:-$MACARCHY_STATE_HOME/macarchy/themes}" +MACARCHY_THEME_STATE="${MACARCHY_THEME_STATE:-$MACARCHY_THEME_RUNTIME_DIR/current}" +MACARCHY_BACKGROUND_STATE="${MACARCHY_BACKGROUND_STATE:-$MACARCHY_THEME_RUNTIME_DIR/backgrounds.json}" +MACARCHY_LEGACY_THEME_STATE="$MACARCHY_THEMES_DIR/.current" +MACARCHY_LEGACY_BACKGROUND_STATE="$MACARCHY_THEMES_DIR/.backgrounds.json" + +macarchy_theme_reset() { + THEME_DARK_MODE="true" + THEME_WALLPAPER="" + THEME_NVIM_COLORSCHEME="" + THEME_BORDER_ACTIVE="0xfff2fcff" + THEME_BORDER_INACTIVE="0x00000000" + THEME_BORDER_WIDTH="6.0" + THEME_SKETCHYBAR_BAR_COLOR="0x00000000" + THEME_SKETCHYBAR_TEXT_COLOR="" + THEME_FIREFOX_ACCENT="" +} + +macarchy_theme_unquote() { + local value="$1" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +macarchy_theme_assign() { + local key="$1" + local value="$2" + + case "$key" in + DARK_MODE) THEME_DARK_MODE="$value" ;; + WALLPAPER) THEME_WALLPAPER="$value" ;; + NVIM_COLORSCHEME) THEME_NVIM_COLORSCHEME="$value" ;; + BORDER_ACTIVE) THEME_BORDER_ACTIVE="$value" ;; + BORDER_INACTIVE) THEME_BORDER_INACTIVE="$value" ;; + BORDER_WIDTH) THEME_BORDER_WIDTH="$value" ;; + SKETCHYBAR_BAR_COLOR) THEME_SKETCHYBAR_BAR_COLOR="$value" ;; + SKETCHYBAR_TEXT_COLOR) THEME_SKETCHYBAR_TEXT_COLOR="$value" ;; + FIREFOX_ACCENT) THEME_FIREFOX_ACCENT="$value" ;; + *) + macarchy_error "unsupported key '$key' in theme.env" + return 1 + ;; + esac +} + +macarchy_theme_validate() { + case "$THEME_DARK_MODE" in true|false) ;; *) macarchy_error "DARK_MODE must be true or false"; return 1 ;; esac + [[ "$THEME_WALLPAPER" != /* && "$THEME_WALLPAPER" != *".."* ]] || { + macarchy_error "WALLPAPER must be a path inside the theme" + return 1 + } + [[ -z "$THEME_NVIM_COLORSCHEME" || "$THEME_NVIM_COLORSCHEME" =~ ^[A-Za-z0-9._-]+$ ]] || { + macarchy_error "invalid NVIM_COLORSCHEME" + return 1 + } + [[ "$THEME_BORDER_ACTIVE" =~ ^0x[0-9A-Fa-f]{8}$ ]] || return 1 + [[ "$THEME_BORDER_INACTIVE" =~ ^0x[0-9A-Fa-f]{8}$ ]] || return 1 + [[ "$THEME_BORDER_WIDTH" =~ ^[0-9]+([.][0-9]+)?$ ]] || return 1 + [[ "$THEME_SKETCHYBAR_BAR_COLOR" =~ ^0x[0-9A-Fa-f]{8}$ ]] || return 1 + [[ -z "$THEME_SKETCHYBAR_TEXT_COLOR" || "$THEME_SKETCHYBAR_TEXT_COLOR" =~ ^0x[0-9A-Fa-f]{8}$ ]] || return 1 + [[ -z "$THEME_FIREFOX_ACCENT" || "$THEME_FIREFOX_ACCENT" =~ ^#[0-9A-Fa-f]{6}$ ]] || return 1 +} + +# theme.env is parsed as data. It is never sourced, so a downloaded theme +# cannot execute shell commands during validation or application. +macarchy_theme_load() { + local name="$1" + local line key value + + MACARCHY_THEME_NAME="$name" + MACARCHY_THEME_DIR="$MACARCHY_THEMES_DIR/$name" + MACARCHY_THEME_FILE="$MACARCHY_THEME_DIR/theme.env" + + [[ -d "$MACARCHY_THEME_DIR" ]] || { + macarchy_error "theme '$name' was not found in $MACARCHY_THEMES_DIR" + return 1 + } + [[ -f "$MACARCHY_THEME_FILE" ]] || { + macarchy_error "theme '$name' has no theme.env" + return 1 + } + + macarchy_theme_reset + while IFS= read -r line || [[ -n "$line" ]]; do + line="$(macarchy_trim "$line")" + [[ -z "$line" || "$line" == \#* ]] && continue + [[ "$line" == *=* ]] || { + macarchy_error "invalid line in $MACARCHY_THEME_FILE: $line" + return 1 + } + key="$(macarchy_trim "${line%%=*}")" + value="$(macarchy_trim "${line#*=}")" + value="$(macarchy_theme_unquote "$value")" + macarchy_theme_assign "$key" "$value" || return 1 + done < "$MACARCHY_THEME_FILE" + + macarchy_theme_validate || { + macarchy_error "theme '$name' contains an invalid value" + return 1 + } +} + +macarchy_theme_list() { + local directory + + [[ -d "$MACARCHY_THEMES_DIR" ]] || return 0 + for directory in "$MACARCHY_THEMES_DIR"/*; do + [[ -d "$directory" && -f "$directory/theme.env" ]] || continue + basename "$directory" + done | LC_ALL=C sort +} + +macarchy_theme_normalise_name() { + local name="$1" + + # Keep an installed legacy pack selectable under its original name. The + # aliases only bridge old saved state when that pack is not present. + case "$name" in + lumon) + [[ ! -d "$MACARCHY_THEMES_DIR/lumon" && -d "$MACARCHY_THEMES_DIR/cool-blue" ]] && name="cool-blue" + ;; + turbonite) + [[ ! -d "$MACARCHY_THEMES_DIR/turbonite" && -d "$MACARCHY_THEMES_DIR/amber-metal" ]] && name="amber-metal" + ;; + esac + printf '%s\n' "$name" +} + +macarchy_theme_current() { + local current="" + + if [[ -r "$MACARCHY_THEME_STATE" ]]; then + current="$(sed -n '1p' "$MACARCHY_THEME_STATE")" + elif [[ -r "$MACARCHY_LEGACY_THEME_STATE" ]]; then + current="$(sed -n '1p' "$MACARCHY_LEGACY_THEME_STATE")" + fi + macarchy_theme_normalise_name "$current" +} + +macarchy_theme_cycle() { + local direction="$1" + local current index target + local -a themes=() + + while IFS= read -r target; do + [[ -n "$target" ]] && themes+=("$target") + done < <(macarchy_theme_list) + (( ${#themes[@]} > 0 )) || { + macarchy_error "no themes were found in $MACARCHY_THEMES_DIR" + return 1 + } + + current="$(macarchy_theme_current)" + for index in "${!themes[@]}"; do + [[ "${themes[$index]}" == "$current" ]] || continue + if [[ "$direction" == "prev" ]]; then + printf '%s\n' "${themes[$(((index - 1 + ${#themes[@]}) % ${#themes[@]}))]}" + else + printf '%s\n' "${themes[$(((index + 1) % ${#themes[@]}))]}" + fi + return 0 + done + + [[ "$direction" == "prev" ]] && target="${themes[${#themes[@]}-1]}" || target="${themes[0]}" + printf '%s\n' "$target" +} + +macarchy_theme_background_valid() { + local relative="$1" + [[ -n "$relative" && "$relative" != /* && "$relative" != *".."* && -f "$MACARCHY_THEME_DIR/$relative" ]] +} + +macarchy_theme_selected_background() { + local selected="" + local jq_bin="" + local background_state="$MACARCHY_BACKGROUND_STATE" + + jq_bin="$(macarchy_command jq || true)" + if [[ ! -r "$background_state" && -r "$MACARCHY_LEGACY_BACKGROUND_STATE" ]]; then + background_state="$MACARCHY_LEGACY_BACKGROUND_STATE" + fi + if [[ -n "$jq_bin" && -r "$background_state" ]]; then + selected="$("$jq_bin" -r --arg theme "$MACARCHY_THEME_NAME" '.[$theme] // empty' "$background_state" 2>/dev/null || true)" + fi + macarchy_theme_background_valid "$selected" && { printf '%s\n' "$selected"; return 0; } + macarchy_theme_background_valid "$THEME_WALLPAPER" && printf '%s\n' "$THEME_WALLPAPER" +} + +macarchy_theme_save_background() { + local relative="$1" + local jq_bin temporary source_state="" + + macarchy_theme_background_valid "$relative" || { + macarchy_error "'$relative' is not a background in theme '$MACARCHY_THEME_NAME'" + return 1 + } + jq_bin="$(macarchy_command jq || true)" + [[ -n "$jq_bin" ]] || { macarchy_error "jq is required to save a background choice"; return 1; } + + mkdir -p "$MACARCHY_THEME_RUNTIME_DIR" || { + macarchy_error "could not create theme state directory: $MACARCHY_THEME_RUNTIME_DIR" + return 1 + } + temporary="$(mktemp "$MACARCHY_THEME_RUNTIME_DIR/.backgrounds.XXXXXX")" || { + macarchy_error "could not create temporary background state" + return 1 + } + + if [[ -e "$MACARCHY_BACKGROUND_STATE" || -L "$MACARCHY_BACKGROUND_STATE" ]]; then + source_state="$MACARCHY_BACKGROUND_STATE" + elif [[ -e "$MACARCHY_LEGACY_BACKGROUND_STATE" || -L "$MACARCHY_LEGACY_BACKGROUND_STATE" ]]; then + source_state="$MACARCHY_LEGACY_BACKGROUND_STATE" + fi + + if [[ -n "$source_state" ]]; then + if [[ ! -f "$source_state" || -L "$source_state" || ! -r "$source_state" ]]; then + rm -f "$temporary" + macarchy_error "background state is not a readable regular file: $source_state" + return 1 + fi + if ! "$jq_bin" -e 'type == "object"' "$source_state" >/dev/null 2>&1; then + rm -f "$temporary" + macarchy_error "background state is not a valid JSON object: $source_state" + return 1 + fi + if ! "$jq_bin" --arg theme "$MACARCHY_THEME_NAME" --arg background "$relative" \ + '.[$theme] = $background' "$source_state" > "$temporary"; then + rm -f "$temporary" + macarchy_error "could not update background state" + return 1 + fi + else + if ! "$jq_bin" -n --arg theme "$MACARCHY_THEME_NAME" --arg background "$relative" \ + '{($theme): $background}' > "$temporary"; then + rm -f "$temporary" + macarchy_error "could not create background state" + return 1 + fi + fi + + if ! macarchy_write_generated "$MACARCHY_BACKGROUND_STATE" < "$temporary"; then + rm -f "$temporary" + return 1 + fi + rm -f "$temporary" +} diff --git a/media/macarchy-awakening.png b/media/macarchy-awakening.png deleted file mode 100644 index bbe5dd2..0000000 Binary files a/media/macarchy-awakening.png and /dev/null differ diff --git a/media/macarchy-lumon.png b/media/macarchy-lumon.png deleted file mode 100644 index cf486dd..0000000 Binary files a/media/macarchy-lumon.png and /dev/null differ diff --git a/media/macarchy-preview.gif b/media/macarchy-preview.gif deleted file mode 100644 index 77e9d22..0000000 Binary files a/media/macarchy-preview.gif and /dev/null differ diff --git a/neofetch/config.conf b/neofetch/config.conf deleted file mode 100644 index 15bbc87..0000000 --- a/neofetch/config.conf +++ /dev/null @@ -1,864 +0,0 @@ -# See this wiki page for more info: -# https://github.com/dylanaraps/neofetch/wiki/Customizing-Info -print_info() { - #info title - #info underline - - info "OS" distro - info "Host" model - info "Kernel" kernel - info "Uptime" uptime - info "Packages" packages - info "Shell" shell - info "Resolution" resolution - info "DE" de - info "WM" wm - info "WM Theme" wm_theme - info "Theme" theme - info "Icons" icons - info "Terminal" term - #info "Terminal Font" term_font - info "CPU" cpu - info "GPU" gpu - info "Memory" memory - - info "GPU Driver" gpu_driver # Linux/macOS only - info "CPU Usage" cpu_usage - #info "Disk" disk - info "Battery" battery - info "Font" font - #info "Song" song - # [[ "$player" ]] && prin "Music Player" "$player" - info "Local IP" local_ip - # info "Public IP" public_ip - # info "Users" users - # info "Locale" locale # This only works on glibc systems. - - #info cols -} - -# Title - - -# Hide/Show Fully qualified domain name. -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --title_fqdn -title_fqdn="off" - - -# Kernel - - -# Shorten the output of the kernel function. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --kernel_shorthand -# Supports: Everything except *BSDs (except PacBSD and PC-BSD) -# -# Example: -# on: '4.8.9-1-ARCH' -# off: 'Linux 4.8.9-1-ARCH' -kernel_shorthand="on" - - -# Distro - - -# Shorten the output of the distro function -# -# Default: 'off' -# Values: 'on', 'tiny', 'off' -# Flag: --distro_shorthand -# Supports: Everything except Windows and Haiku -distro_shorthand="off" - -# Show/Hide OS Architecture. -# Show 'x86_64', 'x86' and etc in 'Distro:' output. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --os_arch -# -# Example: -# on: 'Arch Linux x86_64' -# off: 'Arch Linux' -os_arch="on" - - -# Uptime - - -# Shorten the output of the uptime function -# -# Default: 'on' -# Values: 'on', 'tiny', 'off' -# Flag: --uptime_shorthand -# -# Example: -# on: '2 days, 10 hours, 3 mins' -# tiny: '2d 10h 3m' -# off: '2 days, 10 hours, 3 minutes' -uptime_shorthand="on" - - -# Memory - - -# Show memory pecentage in output. -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --memory_percent -# -# Example: -# on: '1801MiB / 7881MiB (22%)' -# off: '1801MiB / 7881MiB' -memory_percent="off" - -# Change memory output unit. -# -# Default: 'mib' -# Values: 'kib', 'mib', 'gib' -# Flag: --memory_unit -# -# Example: -# kib '1020928KiB / 7117824KiB' -# mib '1042MiB / 6951MiB' -# gib: ' 0.98GiB / 6.79GiB' -memory_unit="mib" - - -# Packages - - -# Show/Hide Package Manager names. -# -# Default: 'tiny' -# Values: 'on', 'tiny' 'off' -# Flag: --package_managers -# -# Example: -# on: '998 (pacman), 8 (flatpak), 4 (snap)' -# tiny: '908 (pacman, flatpak, snap)' -# off: '908' -package_managers="on" - - -# Shell - - -# Show the path to $SHELL -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --shell_path -# -# Example: -# on: '/bin/bash' -# off: 'bash' -shell_path="off" - -# Show $SHELL version -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --shell_version -# -# Example: -# on: 'bash 4.4.5' -# off: 'bash' -shell_version="on" - - -# CPU - - -# CPU speed type -# -# Default: 'bios_limit' -# Values: 'scaling_cur_freq', 'scaling_min_freq', 'scaling_max_freq', 'bios_limit'. -# Flag: --speed_type -# Supports: Linux with 'cpufreq' -# NOTE: Any file in '/sys/devices/system/cpu/cpu0/cpufreq' can be used as a value. -speed_type="bios_limit" - -# CPU speed shorthand -# -# Default: 'off' -# Values: 'on', 'off'. -# Flag: --speed_shorthand -# NOTE: This flag is not supported in systems with CPU speed less than 1 GHz -# -# Example: -# on: 'i7-6500U (4) @ 3.1GHz' -# off: 'i7-6500U (4) @ 3.100GHz' -speed_shorthand="off" - -# Enable/Disable CPU brand in output. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --cpu_brand -# -# Example: -# on: 'Intel i7-6500U' -# off: 'i7-6500U (4)' -cpu_brand="on" - -# CPU Speed -# Hide/Show CPU speed. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --cpu_speed -# -# Example: -# on: 'Intel i7-6500U (4) @ 3.1GHz' -# off: 'Intel i7-6500U (4)' -cpu_speed="on" - -# CPU Cores -# Display CPU cores in output -# -# Default: 'logical' -# Values: 'logical', 'physical', 'off' -# Flag: --cpu_cores -# Support: 'physical' doesn't work on BSD. -# -# Example: -# logical: 'Intel i7-6500U (4) @ 3.1GHz' (All virtual cores) -# physical: 'Intel i7-6500U (2) @ 3.1GHz' (All physical cores) -# off: 'Intel i7-6500U @ 3.1GHz' -cpu_cores="logical" - -# CPU Temperature -# Hide/Show CPU temperature. -# Note the temperature is added to the regular CPU function. -# -# Default: 'off' -# Values: 'C', 'F', 'off' -# Flag: --cpu_temp -# Supports: Linux, BSD -# NOTE: For FreeBSD and NetBSD-based systems, you'll need to enable -# coretemp kernel module. This only supports newer Intel processors. -# -# Example: -# C: 'Intel i7-6500U (4) @ 3.1GHz [27.2°C]' -# F: 'Intel i7-6500U (4) @ 3.1GHz [82.0°F]' -# off: 'Intel i7-6500U (4) @ 3.1GHz' -cpu_temp="off" - - -# GPU - - -# Enable/Disable GPU Brand -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --gpu_brand -# -# Example: -# on: 'AMD HD 7950' -# off: 'HD 7950' -gpu_brand="on" - -# Which GPU to display -# -# Default: 'all' -# Values: 'all', 'dedicated', 'integrated' -# Flag: --gpu_type -# Supports: Linux -# -# Example: -# all: -# GPU1: AMD HD 7950 -# GPU2: Intel Integrated Graphics -# -# dedicated: -# GPU1: AMD HD 7950 -# -# integrated: -# GPU1: Intel Integrated Graphics -gpu_type="all" - - -# Resolution - - -# Display refresh rate next to each monitor -# Default: 'off' -# Values: 'on', 'off' -# Flag: --refresh_rate -# Supports: Doesn't work on Windows. -# -# Example: -# on: '1920x1080 @ 60Hz' -# off: '1920x1080' -refresh_rate="off" - - -# Gtk Theme / Icons / Font - - -# Shorten output of GTK Theme / Icons / Font -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --gtk_shorthand -# -# Example: -# on: 'Numix, Adwaita' -# off: 'Numix [GTK2], Adwaita [GTK3]' -gtk_shorthand="off" - - -# Enable/Disable gtk2 Theme / Icons / Font -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --gtk2 -# -# Example: -# on: 'Numix [GTK2], Adwaita [GTK3]' -# off: 'Adwaita [GTK3]' -gtk2="on" - -# Enable/Disable gtk3 Theme / Icons / Font -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --gtk3 -# -# Example: -# on: 'Numix [GTK2], Adwaita [GTK3]' -# off: 'Numix [GTK2]' -gtk3="on" - - -# IP Address - - -# Website to ping for the public IP -# -# Default: 'http://ident.me' -# Values: 'url' -# Flag: --ip_host -public_ip_host="http://ident.me" - -# Public IP timeout. -# -# Default: '2' -# Values: 'int' -# Flag: --ip_timeout -public_ip_timeout=2 - - -# Desktop Environment - - -# Show Desktop Environment version -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --de_version -de_version="on" - - -# Disk - - -# Which disks to display. -# The values can be any /dev/sdXX, mount point or directory. -# NOTE: By default we only show the disk info for '/'. -# -# Default: '/' -# Values: '/', '/dev/sdXX', '/path/to/drive'. -# Flag: --disk_show -# -# Example: -# disk_show=('/' '/dev/sdb1'): -# 'Disk (/): 74G / 118G (66%)' -# 'Disk (/mnt/Videos): 823G / 893G (93%)' -# -# disk_show=('/'): -# 'Disk (/): 74G / 118G (66%)' -# -disk_show=('/') - -# Disk subtitle. -# What to append to the Disk subtitle. -# -# Default: 'mount' -# Values: 'mount', 'name', 'dir', 'none' -# Flag: --disk_subtitle -# -# Example: -# name: 'Disk (/dev/sda1): 74G / 118G (66%)' -# 'Disk (/dev/sdb2): 74G / 118G (66%)' -# -# mount: 'Disk (/): 74G / 118G (66%)' -# 'Disk (/mnt/Local Disk): 74G / 118G (66%)' -# 'Disk (/mnt/Videos): 74G / 118G (66%)' -# -# dir: 'Disk (/): 74G / 118G (66%)' -# 'Disk (Local Disk): 74G / 118G (66%)' -# 'Disk (Videos): 74G / 118G (66%)' -# -# none: 'Disk: 74G / 118G (66%)' -# 'Disk: 74G / 118G (66%)' -# 'Disk: 74G / 118G (66%)' -disk_subtitle="mount" - -# Disk percent. -# Show/Hide disk percent. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --disk_percent -# -# Example: -# on: 'Disk (/): 74G / 118G (66%)' -# off: 'Disk (/): 74G / 118G' -disk_percent="on" - - -# Song - - -# Manually specify a music player. -# -# Default: 'auto' -# Values: 'auto', 'player-name' -# Flag: --music_player -# -# Available values for 'player-name': -# -# amarok -# audacious -# banshee -# bluemindo -# clementine -# cmus -# deadbeef -# deepin-music -# dragon -# elisa -# exaile -# gnome-music -# gmusicbrowser -# gogglesmm -# guayadeque -# io.elementary.music -# iTunes -# juk -# lollypop -# mocp -# mopidy -# mpd -# muine -# netease-cloud-music -# olivia -# playerctl -# pogo -# pragha -# qmmp -# quodlibet -# rhythmbox -# sayonara -# smplayer -# spotify -# strawberry -# tauonmb -# tomahawk -# vlc -# xmms2d -# xnoise -# yarock -music_player="auto" - -# Format to display song information. -# -# Default: '%artist% - %album% - %title%' -# Values: '%artist%', '%album%', '%title%' -# Flag: --song_format -# -# Example: -# default: 'Song: Jet - Get Born - Sgt Major' -song_format="%artist% - %album% - %title%" - -# Print the Artist, Album and Title on separate lines -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --song_shorthand -# -# Example: -# on: 'Artist: The Fratellis' -# 'Album: Costello Music' -# 'Song: Chelsea Dagger' -# -# off: 'Song: The Fratellis - Costello Music - Chelsea Dagger' -song_shorthand="off" - -# 'mpc' arguments (specify a host, password etc). -# -# Default: '' -# Example: mpc_args=(-h HOST -P PASSWORD) -mpc_args=() - - -# Text Colors - - -# Text Colors -# -# Default: 'distro' -# Values: 'distro', 'num' 'num' 'num' 'num' 'num' 'num' -# Flag: --colors -# -# Each number represents a different part of the text in -# this order: 'title', '@', 'underline', 'subtitle', 'colon', 'info' -# -# Example: -# colors=(distro) - Text is colored based on Distro colors. -# colors=(4 6 1 8 8 6) - Text is colored in the order above. -colors=(distro) - - -# Text Options - - -# Toggle bold text -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --bold -bold="on" - -# Enable/Disable Underline -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --underline -underline_enabled="on" - -# Underline character -# -# Default: '-' -# Values: 'string' -# Flag: --underline_char -underline_char="-" - - -# Info Separator -# Replace the default separator with the specified string. -# -# Default: ':' -# Flag: --separator -# -# Example: -# separator="->": 'Shell-> bash' -# separator=" =": 'WM = dwm' -separator=":" - - -# Color Blocks - - -# Color block range -# The range of colors to print. -# -# Default: '0', '15' -# Values: 'num' -# Flag: --block_range -# -# Example: -# -# Display colors 0-7 in the blocks. (8 colors) -# neofetch --block_range 0 7 -# -# Display colors 0-15 in the blocks. (16 colors) -# neofetch --block_range 0 15 -block_range=(0 15) - -# Toggle color blocks -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --color_blocks -color_blocks="on" - -# Color block width in spaces -# -# Default: '3' -# Values: 'num' -# Flag: --block_width -block_width=3 - -# Color block height in lines -# -# Default: '1' -# Values: 'num' -# Flag: --block_height -block_height=1 - -# Color Alignment -# -# Default: 'auto' -# Values: 'auto', 'num' -# Flag: --col_offset -# -# Number specifies how far from the left side of the terminal (in spaces) to -# begin printing the columns, in case you want to e.g. center them under your -# text. -# Example: -# col_offset="auto" - Default behavior of neofetch -# col_offset=7 - Leave 7 spaces then print the colors -col_offset="auto" - -# Progress Bars - - -# Bar characters -# -# Default: '-', '=' -# Values: 'string', 'string' -# Flag: --bar_char -# -# Example: -# neofetch --bar_char 'elapsed' 'total' -# neofetch --bar_char '-' '=' -bar_char_elapsed="-" -bar_char_total="=" - -# Toggle Bar border -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --bar_border -bar_border="on" - -# Progress bar length in spaces -# Number of chars long to make the progress bars. -# -# Default: '15' -# Values: 'num' -# Flag: --bar_length -bar_length=15 - -# Progress bar colors -# When set to distro, uses your distro's logo colors. -# -# Default: 'distro', 'distro' -# Values: 'distro', 'num' -# Flag: --bar_colors -# -# Example: -# neofetch --bar_colors 3 4 -# neofetch --bar_colors distro 5 -bar_color_elapsed="distro" -bar_color_total="distro" - - -# Info display -# Display a bar with the info. -# -# Default: 'off' -# Values: 'bar', 'infobar', 'barinfo', 'off' -# Flags: --cpu_display -# --memory_display -# --battery_display -# --disk_display -# -# Example: -# bar: '[---=======]' -# infobar: 'info [---=======]' -# barinfo: '[---=======] info' -# off: 'info' -cpu_display="off" -memory_display="off" -battery_display="off" -disk_display="off" - - -# Backend Settings - - -# Image backend. -# -# Default: 'ascii' -# Values: 'ascii', 'caca', 'chafa', 'jp2a', 'iterm2', 'off', -# 'pot', 'termpix', 'pixterm', 'tycat', 'w3m', 'kitty' -# Flag: --backend -image_backend="ascii" - -# Image Source -# -# Which image or ascii file to display. -# -# Default: 'auto' -# Values: 'auto', 'ascii', 'wallpaper', '/path/to/img', '/path/to/ascii', '/path/to/dir/' -# 'command output (neofetch --ascii "$(fortune | cowsay -W 30)")' -# Flag: --source -# -# NOTE: 'auto' will pick the best image source for whatever image backend is used. -# In ascii mode, distro ascii art will be used and in an image mode, your -# wallpaper will be used. -image_source="auto" - - -# Ascii Options - - -# Ascii distro -# Which distro's ascii art to display. -# -# Default: 'auto' -# Values: 'auto', 'distro_name' -# Flag: --ascii_distro -# NOTE: AIX, Alpine, Anarchy, Android, Antergos, antiX, "AOSC OS", -# "AOSC OS/Retro", Apricity, ArcoLinux, ArchBox, ARCHlabs, -# ArchStrike, XFerience, ArchMerge, Arch, Artix, Arya, Bedrock, -# Bitrig, BlackArch, BLAG, BlankOn, BlueLight, bonsai, BSD, -# BunsenLabs, Calculate, Carbs, CentOS, Chakra, ChaletOS, -# Chapeau, Chrom*, Cleanjaro, ClearOS, Clear_Linux, Clover, -# Condres, Container_Linux, CRUX, Cucumber, Debian, Deepin, -# DesaOS, Devuan, DracOS, DarkOs, DragonFly, Drauger, Elementary, -# EndeavourOS, Endless, EuroLinux, Exherbo, Fedora, Feren, FreeBSD, -# FreeMiNT, Frugalware, Funtoo, GalliumOS, Garuda, Gentoo, Pentoo, -# gNewSense, GNOME, GNU, GoboLinux, Grombyang, Guix, Haiku, Huayra, -# Hyperbola, janus, Kali, KaOS, KDE_neon, Kibojoe, Kogaion, -# Korora, KSLinux, Kubuntu, LEDE, LFS, Linux_Lite, -# LMDE, Lubuntu, Lunar, macos, Mageia, MagpieOS, Mandriva, -# Manjaro, Maui, Mer, Minix, LinuxMint, MX_Linux, Namib, -# Neptune, NetBSD, Netrunner, Nitrux, NixOS, Nurunner, -# NuTyX, OBRevenge, OpenBSD, openEuler, OpenIndiana, openmamba, -# OpenMandriva, OpenStage, OpenWrt, osmc, Oracle, OS Elbrus, PacBSD, -# Parabola, Pardus, Parrot, Parsix, TrueOS, PCLinuxOS, Peppermint, -# popos, Porteus, PostMarketOS, Proxmox, Puppy, PureOS, Qubes, Radix, -# Raspbian, Reborn_OS, Redstar, Redcore, Redhat, Refracted_Devuan, -# Regata, Rosa, sabotage, Sabayon, Sailfish, SalentOS, Scientific, -# Septor, SereneLinux, SharkLinux, Siduction, Slackware, SliTaz, -# SmartOS, Solus, Source_Mage, Sparky, Star, SteamOS, SunOS, -# openSUSE_Leap, openSUSE_Tumbleweed, openSUSE, SwagArch, Tails, -# Trisquel, Ubuntu-Budgie, Ubuntu-GNOME, Ubuntu-MATE, Ubuntu-Studio, -# Ubuntu, Venom, Void, Obarun, windows10, Windows7, Xubuntu, Zorin, -# and IRIX have ascii logos -# NOTE: Arch, Ubuntu, Redhat, and Dragonfly have 'old' logo variants. -# Use '{distro name}_old' to use the old logos. -# NOTE: Ubuntu has flavor variants. -# Change this to Lubuntu, Kubuntu, Xubuntu, Ubuntu-GNOME, -# Ubuntu-Studio, Ubuntu-Mate or Ubuntu-Budgie to use the flavors. -# NOTE: Arcolinux, Dragonfly, Fedora, Alpine, Arch, Ubuntu, -# CRUX, Debian, Gentoo, FreeBSD, Mac, NixOS, OpenBSD, android, -# Antrix, CentOS, Cleanjaro, ElementaryOS, GUIX, Hyperbola, -# Manjaro, MXLinux, NetBSD, Parabola, POP_OS, PureOS, -# Slackware, SunOS, LinuxLite, OpenSUSE, Raspbian, -# postmarketOS, and Void have a smaller logo variant. -# Use '{distro name}_small' to use the small variants. -ascii_distro="auto" - -# Ascii Colors -# -# Default: 'distro' -# Values: 'distro', 'num' 'num' 'num' 'num' 'num' 'num' -# Flag: --ascii_colors -# -# Example: -# ascii_colors=(distro) - Ascii is colored based on Distro colors. -# ascii_colors=(4 6 1 8 8 6) - Ascii is colored using these colors. -ascii_colors=(distro) - -# Bold ascii logo -# Whether or not to bold the ascii logo. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --ascii_bold -ascii_bold="on" - - -# Image Options - - -# Image loop -# Setting this to on will make neofetch redraw the image constantly until -# Ctrl+C is pressed. This fixes display issues in some terminal emulators. -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --loop -image_loop="off" - -# Thumbnail directory -# -# Default: '~/.cache/thumbnails/neofetch' -# Values: 'dir' -thumbnail_dir="${XDG_CACHE_HOME:-${HOME}/.cache}/thumbnails/neofetch" - -# Crop mode -# -# Default: 'normal' -# Values: 'normal', 'fit', 'fill' -# Flag: --crop_mode -# -# See this wiki page to learn about the fit and fill options. -# https://github.com/dylanaraps/neofetch/wiki/What-is-Waifu-Crop%3F -crop_mode="normal" - -# Crop offset -# Note: Only affects 'normal' crop mode. -# -# Default: 'center' -# Values: 'northwest', 'north', 'northeast', 'west', 'center' -# 'east', 'southwest', 'south', 'southeast' -# Flag: --crop_offset -crop_offset="center" - -# Image size -# The image is half the terminal width by default. -# -# Default: 'auto' -# Values: 'auto', '00px', '00%', 'none' -# Flags: --image_size -# --size -image_size="auto" - -# Gap between image and text -# -# Default: '3' -# Values: 'num', '-num' -# Flag: --gap -gap=3 - -# Image offsets -# Only works with the w3m backend. -# -# Default: '0' -# Values: 'px' -# Flags: --xoffset -# --yoffset -yoffset=0 -xoffset=0 - -# Image background color -# Only works with the w3m backend. -# -# Default: '' -# Values: 'color', 'blue' -# Flag: --bg_color -background_color= - - -# Misc Options - -# Stdout mode -# Turn off all colors and disables image backend (ASCII/Image). -# Useful for piping into another command. -# Default: 'off' -# Values: 'on', 'off' -stdout="off" diff --git a/nvim/.neoconf.json b/nvim/.neoconf.json deleted file mode 100644 index 7c48087..0000000 --- a/nvim/.neoconf.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "neodev": { - "library": { - "enabled": true, - "plugins": true - } - }, - "neoconf": { - "plugins": { - "lua_ls": { - "enabled": true - } - } - } -} diff --git a/nvim/lazy-lock.json b/nvim/lazy-lock.json index 2c8f8ee..17fa3cc 100644 --- a/nvim/lazy-lock.json +++ b/nvim/lazy-lock.json @@ -1,39 +1,39 @@ { - "LazyVim": { "branch": "main", "commit": "6055e59613b406f9c4312c95e56f604fb839a97e" }, + "LazyVim": { "branch": "main", "commit": "c10948c50b18fae7f256433afdef09e432410480" }, "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, - "catppuccin": { "branch": "main", "commit": "384f304c8b04664c9e0091fbfb3923c5f97c1bcf" }, - "conform.nvim": { "branch": "master", "commit": "086a40dc7ed8242c03be9f47fbcee68699cc2395" }, - "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, + "catppuccin": { "branch": "main", "commit": "05e8787020dcfdb937bf2ff23855ea2415b4e072" }, + "conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" }, + "flash.nvim": { "branch": "main", "commit": "b6346946d10d07998efee029fb0f7a593806d0cd" }, "flexoki": { "branch": "main", "commit": "c3e2251e813d29d885a7cbbe9808a7af234d845d" }, "friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, - "gitsigns.nvim": { "branch": "main", "commit": "7c4faa3540d0781a28588cafbd4dd187a28ac6e3" }, - "grug-far.nvim": { "branch": "main", "commit": "37d0eafc6a2dcf8641aa19fc2a8db5e391b7f16b" }, - "gruvbox.nvim": { "branch": "main", "commit": "334d5fd49fc8033f26408425366c66c6390c57bb" }, - "kanagawa.nvim": { "branch": "master", "commit": "aef7f5cec0a40dbe7f3304214850c472e2264b10" }, + "gitsigns.nvim": { "branch": "main", "commit": "eb60cc7b94c46005237fd34170d76f3a089a90aa" }, + "grug-far.nvim": { "branch": "main", "commit": "beb2ce490b72048c587e8a3d919cba1b4608bb04" }, + "gruvbox.nvim": { "branch": "main", "commit": "154eb5ff5b96d0641307113fa385eaf0d36d9796" }, + "kanagawa.nvim": { "branch": "master", "commit": "bb85e4bfc8d89b0e62c8fa53ccdd13d12e2f77b3" }, "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, "lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" }, - "lualine.nvim": { "branch": "master", "commit": "8811f3f3f4dc09d740c67e9ce399e7a541e2e5b2" }, - "mason-lspconfig.nvim": { "branch": "main", "commit": "25f609e7fca78af7cede4f9fa3af8a94b1c4950b" }, - "mason.nvim": { "branch": "main", "commit": "b03fb0f20bc1d43daf558cda981a2be22e73ac42" }, - "mini.ai": { "branch": "main", "commit": "43eb2074843950a3a25aae56a5f41362ec043bfa" }, - "mini.icons": { "branch": "main", "commit": "7fdae2443a0e2910015ca39ad74b50524ee682d3" }, - "mini.pairs": { "branch": "main", "commit": "42387c7fe68fc0b6e95eaf37f1bb76e7bffaa0d9" }, + "lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "47059d71b42d74b0a1e9f61c1d99d301039c3b5b" }, + "mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" }, + "mini.ai": { "branch": "main", "commit": "25248c6aa002391936a6200f12d1466015987133" }, + "mini.icons": { "branch": "main", "commit": "98faae31e9be1cc054ae63485e58ceb185efcad0" }, + "mini.pairs": { "branch": "main", "commit": "b1fd9df3bb4a41c8f45778a3859ee80ef6b367e3" }, "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, "nord.nvim": { "branch": "master", "commit": "80c1e5321505aeb22b7a9f23eb82f1e193c12470" }, "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, - "nvim-lint": { "branch": "master", "commit": "4b03656c09c1561f89b6aa0665c15d292ba9499d" }, - "nvim-lspconfig": { "branch": "master", "commit": "bedca8b426b2fee0ccac596d167d71bbe971253f" }, + "nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" }, + "nvim-lspconfig": { "branch": "master", "commit": "d6ac7a0d46555d44bf21f4f9c65f5201f818b475" }, "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, "nvim-treesitter-textobjects": { "branch": "main", "commit": "851e865342e5a4cb1ae23d31caf6e991e1c99f1e" }, - "nvim-ts-autotag": { "branch": "main", "commit": "8e1c0a389f20bf7f5b0dd0e00306c1247bda2595" }, + "nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" }, "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, - "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" }, - "rose-pine": { "branch": "main", "commit": "cf2a288696b03d0934da713d66c6d71557b5c997" }, - "snacks.nvim": { "branch": "main", "commit": "ad9ede6a9cddf16cedbd31b8932d6dcdee9b716e" }, + "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, + "rose-pine": { "branch": "main", "commit": "ff483051a47e27d84bdef47703538df1ed9f4a47" }, + "snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" }, "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, "tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" }, "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, - "ts-comments.nvim": { "branch": "main", "commit": "123a9fb12e7229342f807ec9e6de478b1102b041" }, + "ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" }, "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } } diff --git a/nvim/lua/config/autocmds.lua b/nvim/lua/config/autocmds.lua index 41c5c95..a5a8366 100644 --- a/nvim/lua/config/autocmds.lua +++ b/nvim/lua/config/autocmds.lua @@ -46,7 +46,6 @@ clear_backgrounds() local theme_group = vim.api.nvim_create_augroup("macarchy_theme_refresh", { clear = true }) local theme = require("config.theme") local last_colorscheme_stamp = theme.stamp() -local theme_timer = nil local function refresh_configured_colorscheme() vim.schedule(function() @@ -71,39 +70,13 @@ local function refresh_configured_colorscheme() end) end -local function start_theme_timer() - local uv = vim.uv or vim.loop - - if theme_timer then - return - end - - theme_timer = uv.new_timer() - if not theme_timer then - return - end - - theme_timer:start(150, 150, refresh_configured_colorscheme) -end - -start_theme_timer() - +-- theme-switch sends WINCH to running Neovim processes. VimResized handles +-- that signal, while the other events cover ordinary focus changes. vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold", "VimResized" }, { group = theme_group, callback = refresh_configured_colorscheme, }) -vim.api.nvim_create_autocmd("VimLeavePre", { - group = theme_group, - callback = function() - if theme_timer then - theme_timer:stop() - theme_timer:close() - theme_timer = nil - end - end, -}) - local dashboard_filetypes = { alpha = true, dashboard = true, diff --git a/nvim/lua/config/theme.lua b/nvim/lua/config/theme.lua index c708e10..73629c7 100644 --- a/nvim/lua/config/theme.lua +++ b/nvim/lua/config/theme.lua @@ -1,17 +1,11 @@ local M = {} -M.state_file = vim.fn.expand("~/.config/themes/.nvim-colorscheme") +local config_home = vim.env.XDG_CONFIG_HOME or vim.fn.expand("~/.config") +local generated_dir = vim.env.MACARCHY_GENERATED_DIR or (config_home .. "/macarchy/generated") +M.state_file = generated_dir .. "/nvim-colorscheme" local fallback = "gruvbox" -local aliases = { - awakening = "gruvbox", - blackgold = "gruvbox", - carbonfox = "tokyonight-night", - ["matte-black"] = "gruvbox", - midnight = "tokyonight-night", -} - local plugins = { ["catppuccin"] = "catppuccin", ["flexoki"] = "flexoki", @@ -28,7 +22,7 @@ local plugins = { local function clean_name(value) value = tostring(value or ""):match("^%s*(.-)%s*$") if value:match("^[%w%._%-]+$") then - return aliases[value] or value + return value end end diff --git a/raycast-theme/.gitignore b/raycast-theme/.gitignore new file mode 100644 index 0000000..bc22d82 --- /dev/null +++ b/raycast-theme/.gitignore @@ -0,0 +1,6 @@ +/node_modules + +# Generated by Raycast from package.json. +raycast-env.d.ts +.raycast/ +dist/ diff --git a/raycast-theme/.nvmrc b/raycast-theme/.nvmrc new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/raycast-theme/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/raycast-theme/.prettierrc b/raycast-theme/.prettierrc new file mode 100644 index 0000000..fc0f503 --- /dev/null +++ b/raycast-theme/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "singleQuote": false +} diff --git a/raycast-theme/README.md b/raycast-theme/README.md new file mode 100644 index 0000000..d59d66c --- /dev/null +++ b/raycast-theme/README.md @@ -0,0 +1,36 @@ +# Raycast theme switcher + +This extension shows your installed themes in Raycast and passes the selected name to `theme-switch`. The shell command remains responsible for applying the theme. + +By default, it looks for: + +- the command at `~/.local/bin/theme-switch` +- themes in `~/.config/themes` +- theme state in `~/.local/state/macarchy/themes` + +All three paths can be changed in the extension preferences. `~` and `$HOME` are expanded before the paths are used, and the theme and state directories are passed to `theme-switch` when a selection is applied. If Macarchy was installed with a custom `XDG_STATE_HOME`, set Theme State Directory to `$XDG_STATE_HOME/macarchy/themes` using its full path. + +## Install it + +Install the theme switcher and at least one theme first: + +```sh +./install themes raycast +cd "${XDG_DATA_HOME:-$HOME/.local/share}/macarchy/raycast-theme" +fnm use --install-if-missing +npm ci +npm run dev +``` + +The installer keeps this development source under the XDG data directory. Raycast writes the compiled command to `~/.config/raycast/extensions/theme-switcher`. That second directory is generated runtime output. Do not run `npm install` or edit source there, because a local React copy beside the command prevents Raycast from loading it correctly. + +When working from a repository clone instead, run the same Node and npm commands from `raycast-theme/` in the clone. + +The included `.nvmrc` selects Node.js 24 when you use fnm or another compatible version manager. If you do not use one, make sure `node --version` satisfies the version in `package.json` before installing dependencies. + +Raycast will open the extension in development mode. Use `npm run lint` and `npm run build` before publishing changes. +The build command writes its output to the ignored `dist/` directory and does not replace your active Raycast command. + +## Publishing + +The manifest uses `macarchy` as a neutral project author. Raycast only accepts an existing Store handle when publishing, so a maintainer must replace this value with their own Raycast handle first. Until then, `npm run validate:store` reports the author during manifest validation; local linting and builds still work normally. diff --git a/raycast-theme/assets/README.md b/raycast-theme/assets/README.md new file mode 100644 index 0000000..c153167 --- /dev/null +++ b/raycast-theme/assets/README.md @@ -0,0 +1,9 @@ +# Raycast icon + +`command-icon.svg` is the editable source for Macarchy's Raycast extension icon. It is original project artwork covered by the repository's MIT licence. + +Raycast expects a 512 by 512 PNG. Regenerate it on macOS with: + +```sh +sips -s format png command-icon.svg --out command-icon.png +``` diff --git a/raycast-theme/assets/command-icon.png b/raycast-theme/assets/command-icon.png index 54aed68..e02dcbd 100644 Binary files a/raycast-theme/assets/command-icon.png and b/raycast-theme/assets/command-icon.png differ diff --git a/raycast-theme/assets/command-icon.svg b/raycast-theme/assets/command-icon.svg new file mode 100644 index 0000000..bef202f --- /dev/null +++ b/raycast-theme/assets/command-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/raycast-theme/eslint.config.mjs b/raycast-theme/eslint.config.mjs new file mode 100644 index 0000000..a177096 --- /dev/null +++ b/raycast-theme/eslint.config.mjs @@ -0,0 +1,4 @@ +import { defineConfig } from "eslint/config"; +import raycastConfig from "@raycast/eslint-config"; + +export default defineConfig([...raycastConfig]); diff --git a/raycast-theme/package-lock.json b/raycast-theme/package-lock.json new file mode 100644 index 0000000..90c8c72 --- /dev/null +++ b/raycast-theme/package-lock.json @@ -0,0 +1,2742 @@ +{ + "name": "theme-switcher", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "theme-switcher", + "license": "MIT", + "dependencies": { + "@raycast/api": "^1.104.22" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.2.0", + "@types/node": "^22.20.1", + "@types/react": "19.2.17", + "eslint": "^10.7.0", + "prettier": "^3.9.0", + "typescript": "^6.0.3" + }, + "engines": { + "node": ">=22.22.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@oclif/core": { + "version": "4.11.14", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.14.tgz", + "integrity": "sha512-cZ5Ktd+rT0PO+o7KBH4vRFTgg+xMLf8F41WK39p8MkXEViZA/Qqe+4lzZT6102zgUxMORET1HtF9t5w8CB3tnQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "ansis": "^3.17.0", + "clean-stack": "^3.0.1", + "cli-spinners": "^2.9.2", + "debug": "^4.4.3", + "ejs": "^3.1.10", + "get-package-type": "^0.1.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "lilconfig": "^3.1.3", + "minimatch": "^10.2.5", + "semver": "^7.8.1", + "string-width": "^4.2.3", + "supports-color": "^8", + "tinyglobby": "^0.2.17", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "3.2.53", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.53.tgz", + "integrity": "sha512-cGAgN9ujTDxa6C84d6XNVsmoFnDFfHwdiykAnAfym3oBLxoXBNYMZhmfnZ8KBWDy/r/DYf11BrGJwqKl2P0iSA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4", + "ansis": "^3.16.0", + "debug": "^4.4.1", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "6.2.53", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.53.tgz", + "integrity": "sha512-njx2nTH87EQEEuz4ShNtL0gzzN981MRkDPqScbu+Tkd7NpIv30OHdTjQK1GzGtkf+V2RvSYIrX+LrLsUVh9DJw==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "3.2.88", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.88.tgz", + "integrity": "sha512-5YTKSpuV77910/iGnGsj0fr9kOazCy/h1brki1CocbfawdvaVPedcwCH25wMcdRfo8mFD656bG9/kdki/+Wb9A==", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.10.1", + "@oclif/core": "^4.11.7", + "ansis": "^3.17.0", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@raycast/api": { + "version": "1.104.22", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.104.22.tgz", + "integrity": "sha512-t4HwubTb1qpbaxdP8mu62Ugd1qYgcOM4Lc4+7EsxPPm7g3g5xlarH+MG+/kQEGrUukctGwrNL8S6VCVnuGIutw==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4.8.4", + "@oclif/plugin-autocomplete": "^3.2.40", + "@oclif/plugin-help": "^6.2.37", + "@oclif/plugin-not-found": "^3.2.74", + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "esbuild": "^0.27.3", + "react": "19.0.0" + }, + "bin": { + "ray": "bin/run.js" + }, + "engines": { + "node": ">=22.22.2" + }, + "peerDependencies": { + "@types/node": "22.19.17", + "@types/react": "19.0.10", + "react-devtools": "6.1.1" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react-devtools": { + "optional": true + } + } + }, + "node_modules/@raycast/api/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@raycast/api/node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@raycast/eslint-config": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-2.2.0.tgz", + "integrity": "sha512-uS/UDvM+3HfABgYhLYnIv0y+IVu+rGiZZIMwQsO5EGrcf2/TX/yioTzfezAQqhdszW4ViCuHbv8BddIagDKZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/js": "^9.39.4", + "@raycast/eslint-plugin": "^2.2.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.7.0", + "typescript-eslint": "^8.62.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "prettier": ">=2", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@raycast/eslint-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.2.0.tgz", + "integrity": "sha512-ixO3PoRAEAZZcSQeRPvvDS/vs09D47tXdyXrs7hctoxxPtweXgJ6I24kjCD5GuuuQS5hGTiuL+Uy6hIg6a/a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.62.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/raycast-theme/package.json b/raycast-theme/package.json index 4e94ecf..b283e67 100644 --- a/raycast-theme/package.json +++ b/raycast-theme/package.json @@ -1,31 +1,76 @@ { + "$schema": "https://www.raycast.com/schemas/extension.json", "name": "theme-switcher", "title": "Theme Switcher", - "description": "Apply Macarchy themes through ~/.local/bin/theme-switch", + "description": "Choose and apply an installed Macarchy theme", "icon": "command-icon.png", "author": "macarchy", - "license": "UNLICENSED", + "platforms": [ + "macOS" + ], + "categories": [ + "System" + ], + "license": "MIT", + "preferences": [ + { + "name": "themeSwitcherPath", + "title": "Theme Switcher", + "description": "Path to the installed theme-switch command", + "type": "textfield", + "required": false, + "default": "~/.local/bin/theme-switch", + "placeholder": "~/.local/bin/theme-switch" + }, + { + "name": "themesDirectory", + "title": "Themes Directory", + "description": "Directory containing the installed themes", + "type": "textfield", + "required": false, + "default": "~/.config/themes", + "placeholder": "~/.config/themes" + }, + { + "name": "themeStateDirectory", + "title": "Theme State Directory", + "description": "Directory containing Macarchy's current theme and wallpaper choices", + "type": "textfield", + "required": false, + "default": "~/.local/state/macarchy/themes", + "placeholder": "~/.local/state/macarchy/themes" + } + ], "commands": [ { "name": "switch-theme", "title": "Switch Theme", - "description": "Pick a theme to apply across your system", + "description": "Choose an installed theme and apply it", "mode": "view" } ], "dependencies": { - "@raycast/api": "^1.93.3" + "@raycast/api": "^1.104.22" }, "devDependencies": { - "@raycast/eslint-config": "^1.0.11", - "@types/node": "22.13.14", - "@types/react": "19.0.12", - "eslint": "^9.23.0", - "typescript": "^5.8.2" + "@raycast/eslint-config": "^2.2.0", + "@types/node": "^22.20.1", + "@types/react": "19.2.17", + "eslint": "^10.7.0", + "prettier": "^3.9.0", + "typescript": "^6.0.3" }, "scripts": { - "build": "ray build", + "build": "ray build --output dist", + "predev": "node scripts/assert-runtime-clean.mjs", "dev": "ray develop", - "lint": "ray lint" + "format": "prettier --write .", + "lint": "eslint src && prettier --check . && tsc --noEmit", + "lint:fix": "eslint src --fix && prettier --write .", + "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test test/*.test.ts", + "validate:store": "ray lint" + }, + "engines": { + "node": ">=22.22.2" } } diff --git a/raycast-theme/scripts/assert-runtime-clean.mjs b/raycast-theme/scripts/assert-runtime-clean.mjs new file mode 100644 index 0000000..eab256d --- /dev/null +++ b/raycast-theme/scripts/assert-runtime-clean.mjs @@ -0,0 +1,14 @@ +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const manifest = JSON.parse(readFileSync(join(scriptDirectory, "..", "package.json"), "utf8")); +const runtimeDependencies = join(homedir(), ".config", "raycast", "extensions", manifest.name, "node_modules"); + +if (existsSync(runtimeDependencies)) { + console.error(`Raycast runtime dependencies were found at ${runtimeDependencies}.`); + console.error("Run the Macarchy Raycast installer again before starting development."); + process.exit(1); +} diff --git a/raycast-theme/src/backgrounds.ts b/raycast-theme/src/backgrounds.ts new file mode 100644 index 0000000..8465258 --- /dev/null +++ b/raycast-theme/src/backgrounds.ts @@ -0,0 +1,91 @@ +import { readdir, readFile, stat } from "node:fs/promises"; +import { basename, extname, join } from "node:path"; + +export type Background = { + aliasPaths: string[]; + name: string; + path: string; + relativePath: string; +}; + +type BackgroundCandidate = Background & { + directory: string; + size: number; +}; + +const IMAGE_EXTENSIONS = new Set([".heic", ".jpeg", ".jpg", ".png", ".webp"]); + +function titleFromBackgroundName(name: string): string { + return basename(name, extname(name)) + .replace(/^\d+[-_.\s]*/, "") + .replace(/[-_.]+/g, " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +async function hasSameContents(left: BackgroundCandidate, right: BackgroundCandidate): Promise { + if (left.size !== right.size) return false; + const [leftContents, rightContents] = await Promise.all([readFile(left.path), readFile(right.path)]); + return leftContents.equals(rightContents); +} + +export async function loadBackgrounds(themeDirectory: string): Promise { + const candidates: BackgroundCandidate[] = []; + + // A pack can keep images at its root, under backgrounds/, or in both. Read + // the subdirectory first so an identical root-level compatibility copy does + // not appear as a second choice. + for (const directory of ["backgrounds", ""]) { + let entries; + try { + entries = await readdir(join(themeDirectory, directory), { withFileTypes: true }); + } catch (error) { + const code = error instanceof Error && "code" in error ? error.code : undefined; + if (code === "ENOENT") continue; + throw error; + } + + for (const entry of entries) { + if (!entry.isFile() || entry.name.startsWith(".") || !IMAGE_EXTENSIONS.has(extname(entry.name).toLowerCase())) { + continue; + } + + const relativePath = directory ? `${directory}/${entry.name}` : entry.name; + const path = join(themeDirectory, relativePath); + const details = await stat(path); + candidates.push({ + aliasPaths: [], + directory, + name: titleFromBackgroundName(entry.name), + path, + relativePath, + size: details.size, + }); + } + } + + const unique: BackgroundCandidate[] = []; + for (const candidate of candidates) { + let duplicate = false; + for (const existing of unique) { + if (candidate.directory !== existing.directory && (await hasSameContents(candidate, existing))) { + existing.aliasPaths.push(candidate.relativePath); + duplicate = true; + break; + } + } + if (!duplicate) unique.push(candidate); + } + + return unique + .map((background) => ({ + aliasPaths: background.aliasPaths, + name: background.name, + path: background.path, + relativePath: background.relativePath, + })) + .sort((left, right) => left.relativePath.localeCompare(right.relativePath)); +} + +export function backgroundMatchesPath(background: Background, relativePath: string): boolean { + return background.relativePath === relativePath || background.aliasPaths.includes(relativePath); +} diff --git a/raycast-theme/src/paths.ts b/raycast-theme/src/paths.ts new file mode 100644 index 0000000..48385e7 --- /dev/null +++ b/raycast-theme/src/paths.ts @@ -0,0 +1,22 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +export const DEFAULT_PATHS = { + themeStateDirectory: "~/.local/state/macarchy/themes", + themeSwitcherPath: "~/.local/bin/theme-switch", + themesDirectory: "~/.config/themes", +} as const; + +export function expandHome(value: string, homeDirectory = homedir()): string { + const path = value.trim(); + + if (path === "~" || path === "$HOME") return homeDirectory; + if (path.startsWith("~/")) return join(homeDirectory, path.slice(2)); + if (path.startsWith("$HOME/")) return join(homeDirectory, path.slice(6)); + return path; +} + +export function resolvePreferencePath(value: unknown, fallback: string, homeDirectory = homedir()): string { + const configuredPath = typeof value === "string" ? value.trim() : ""; + return expandHome(configuredPath || fallback, homeDirectory); +} diff --git a/raycast-theme/src/switch-theme.tsx b/raycast-theme/src/switch-theme.tsx new file mode 100644 index 0000000..4716470 --- /dev/null +++ b/raycast-theme/src/switch-theme.tsx @@ -0,0 +1,479 @@ +import { + Action, + ActionPanel, + Color, + Grid, + Icon, + Keyboard, + List, + Toast, + closeMainWindow, + getPreferenceValues, + openExtensionPreferences, + showToast, +} from "@raycast/api"; +import { execFile } from "node:child_process"; +import { readdir, readFile } from "node:fs/promises"; +import { isAbsolute, join } from "node:path"; +import { useCallback, useEffect, useState } from "react"; +import { backgroundMatchesPath, loadBackgrounds, type Background } from "./backgrounds"; +import { DEFAULT_PATHS, resolvePreferencePath } from "./paths"; +import { normaliseLegacyThemeName } from "./theme-utils"; + +type Preferences = { + themeStateDirectory?: string; + themeSwitcherPath?: string; + themesDirectory?: string; +}; + +type Theme = { + accent: string; + backgrounds: Background[]; + foreground: string; + isDark: boolean; + name: string; + palette: string[]; + selectedBackground?: Background; + title: string; +}; + +type ThemeState = { + currentTheme: string; + themes: Theme[]; +}; + +function titleFromName(name: string): string { + return name + .split(/[-_\s]+/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +function isSafeRelativePath(path: string): boolean { + return path.length > 0 && !isAbsolute(path) && !path.split("/").includes(".."); +} + +function parseThemeEnvironment(contents: string): Record { + const values: Record = {}; + + for (const rawLine of contents.split("\n")) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + + const separator = line.indexOf("="); + if (separator === -1) continue; + + const key = line.slice(0, separator).trim(); + let value = line.slice(separator + 1).trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + values[key] = value; + } + + return values; +} + +function parseGhosttyColours(contents: string): { accent: string; foreground: string; palette: string[] } { + let accent = "#888888"; + let foreground = "#ffffff"; + const palette: string[] = []; + + for (const rawLine of contents.split("\n")) { + const line = rawLine.trim(); + const foregroundMatch = line.match(/^foreground\s*=\s*(#[0-9a-f]{6})$/i); + const paletteMatch = line.match(/^palette\s*=\s*(\d+)\s*=\s*(#[0-9a-f]{6})$/i); + if (foregroundMatch) foreground = foregroundMatch[1]; + if (paletteMatch) palette[Number(paletteMatch[1])] = paletteMatch[2]; + } + + accent = palette[4] || accent; + return { accent, foreground, palette }; +} + +function borderColour(value: string | undefined): string | undefined { + const match = value?.match(/^0xff([0-9a-f]{6})$/i); + return match ? `#${match[1]}` : undefined; +} + +async function readOptional(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch (error) { + const code = error instanceof Error && "code" in error ? error.code : undefined; + if (code === "ENOENT") return ""; + throw error; + } +} + +async function loadThemeState(themesDirectory: string, themeStateDirectory: string): Promise { + let entries; + try { + entries = await readdir(themesDirectory, { withFileTypes: true }); + } catch (error) { + const code = error instanceof Error && "code" in error ? error.code : undefined; + if (code === "ENOENT") { + throw new Error( + `Themes directory was not found at ${themesDirectory}. Install the themes or update the extension preferences.`, + ); + } + if (code === "EACCES") { + throw new Error( + `Themes directory cannot be read at ${themesDirectory}. Check its permissions or update the extension preferences.`, + ); + } + throw error; + } + + const [currentTheme, legacyCurrentTheme, backgroundStateText, legacyBackgroundStateText] = await Promise.all([ + readOptional(join(themeStateDirectory, "current")), + readOptional(join(themesDirectory, ".current")), + readOptional(join(themeStateDirectory, "backgrounds.json")), + readOptional(join(themesDirectory, ".backgrounds.json")), + ]); + + // Keep existing installations useful until a theme is next applied and its + // state is migrated out of the theme pack. + const selectedTheme = currentTheme || legacyCurrentTheme; + const selectedBackgroundState = backgroundStateText || legacyBackgroundStateText; + let backgroundState: Record = {}; + if (selectedBackgroundState) { + try { + const parsed: unknown = JSON.parse(selectedBackgroundState); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + backgroundState = Object.fromEntries( + Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === "string"), + ); + } + } catch { + // A damaged preference file should not hide otherwise valid themes. + } + } + + const themes = await Promise.all( + entries + .filter((entry) => !entry.name.startsWith(".") && (entry.isDirectory() || entry.isSymbolicLink())) + .map(async (entry): Promise => { + const directory = join(themesDirectory, entry.name); + const [environmentText, ghosttyText, backgrounds] = await Promise.all([ + readOptional(join(directory, "theme.env")), + readOptional(join(directory, "ghostty.conf")), + loadBackgrounds(directory), + ]); + const environment = parseThemeEnvironment(environmentText); + const ghostty = parseGhosttyColours(ghosttyText); + const selectedPath = backgroundState[entry.name] || environment.WALLPAPER || ""; + const selectedBackground = isSafeRelativePath(selectedPath) + ? backgrounds.find((background) => backgroundMatchesPath(background, selectedPath)) + : undefined; + + return { + accent: + ghostty.accent === "#888888" ? borderColour(environment.BORDER_ACTIVE) || ghostty.accent : ghostty.accent, + backgrounds, + foreground: ghostty.foreground, + isDark: environment.DARK_MODE !== "false", + name: entry.name, + palette: ghostty.palette, + selectedBackground: selectedBackground || backgrounds[0], + title: titleFromName(entry.name), + }; + }), + ); + + return { + currentTheme: normaliseLegacyThemeName(selectedTheme, themes), + themes: themes.sort((left, right) => left.name.localeCompare(right.name)), + }; +} + +function runThemeSwitcher( + executable: string, + arguments_: string[], + themesDirectory: string, + themeStateDirectory: string, +): Promise { + return new Promise((resolve, reject) => { + execFile( + executable, + arguments_, + { + env: { + ...process.env, + MACARCHY_THEMES_DIR: themesDirectory, + MACARCHY_THEME_RUNTIME_DIR: themeStateDirectory, + }, + }, + (error, stdout, stderr) => { + if (!error) { + resolve(); + return; + } + + if ("code" in error && error.code === "ENOENT") { + reject( + new Error(`Theme switcher was not found at ${executable}. Install it or update the extension preferences.`), + ); + return; + } + if ("code" in error && error.code === "EACCES") { + reject(new Error(`Theme switcher is not executable at ${executable}. Check its permissions.`)); + return; + } + + reject(new Error(stderr.trim() || stdout.trim() || error.message)); + }, + ); + }); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function wallpaperMarkdown(theme: Theme): string { + if (!theme.selectedBackground) return "_No wallpaper is included with this theme._"; + const image = encodeURI(`file://${theme.selectedBackground.path}`); + return `![${theme.selectedBackground.name}](${image}?raycast-width=720&raycast-height=405)`; +} + +function themeMarkdown(theme: Theme): string { + return [ + `# ${theme.title}`, + "", + wallpaperMarkdown(theme), + "", + theme.selectedBackground ? `Background \`${theme.selectedBackground.name}\`` : "", + `Accent \`${theme.accent}\``, + "macOS Accent `Multicolour`", + "Highlight `System`", + `Text \`${theme.foreground}\``, + ].join("\n"); +} + +function lsPreviewColours(theme: Theme) { + return { + archive: theme.palette[1] || theme.accent, + device: theme.palette[3] || theme.accent, + directory: theme.palette[4] || theme.accent, + executable: theme.palette[2] || theme.accent, + normal: theme.foreground, + symlink: theme.palette[6] || theme.accent, + }; +} + +function BackgroundPicker(props: { + executable: string; + onApplied: () => Promise; + theme: Theme; + themesDirectory: string; + themeStateDirectory: string; +}) { + const [selected, setSelected] = useState(props.theme.selectedBackground?.relativePath); + const backgrounds = selected + ? [ + ...props.theme.backgrounds.filter((background) => background.relativePath === selected), + ...props.theme.backgrounds.filter((background) => background.relativePath !== selected), + ] + : props.theme.backgrounds; + + async function applyBackground(background: Background) { + const toast = await showToast({ style: Toast.Style.Animated, title: `Applying ${background.name}` }); + try { + await runThemeSwitcher( + props.executable, + ["--background", props.theme.name, background.relativePath], + props.themesDirectory, + props.themeStateDirectory, + ); + setSelected(background.relativePath); + toast.style = Toast.Style.Success; + toast.title = `${background.name} selected`; + await props.onApplied(); + await closeMainWindow(); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Could not change the wallpaper"; + toast.message = errorMessage(error); + } + } + + return ( + + {backgrounds.map((background) => ( + + applyBackground(background)} /> + + } + /> + ))} + + ); +} + +export default function Command() { + const preferences = getPreferenceValues(); + const themesDirectory = resolvePreferencePath(preferences.themesDirectory, DEFAULT_PATHS.themesDirectory); + const themeStateDirectory = resolvePreferencePath(preferences.themeStateDirectory, DEFAULT_PATHS.themeStateDirectory); + const executable = resolvePreferencePath(preferences.themeSwitcherPath, DEFAULT_PATHS.themeSwitcherPath); + const [state, setState] = useState({ currentTheme: "", themes: [] }); + const [loadError, setLoadError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + const refresh = useCallback(async () => { + setIsLoading(true); + setLoadError(undefined); + try { + setState(await loadThemeState(themesDirectory, themeStateDirectory)); + } catch (error) { + setState({ currentTheme: "", themes: [] }); + setLoadError(errorMessage(error)); + } finally { + setIsLoading(false); + } + }, [themesDirectory, themeStateDirectory]); + + useEffect(() => { + void refresh(); + }, [refresh]); + + async function applyTheme(theme: Theme) { + const toast = await showToast({ style: Toast.Style.Animated, title: `Applying ${theme.title}` }); + try { + await runThemeSwitcher(executable, [theme.name], themesDirectory, themeStateDirectory); + toast.style = Toast.Style.Success; + toast.title = `${theme.title} applied`; + await refresh(); + await closeMainWindow(); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = `Could not apply ${theme.title}`; + toast.message = errorMessage(error); + } + } + + const activeTheme = state.themes.find((theme) => theme.name === state.currentTheme) || state.themes[0]; + + return ( + + + + + + } + /> + {activeTheme ? ( + } + actions={ + + + } + /> + + } + /> + ) : null} + {state.themes.map((theme) => { + const isCurrent = theme.name === state.currentTheme; + return ( + 0 ? [{ text: `${theme.backgrounds.length} bg` }] : []), + ]} + detail={ + + + + + + + + + + + + + + + + + + } + /> + } + actions={ + + applyTheme(theme)} /> + {theme.backgrounds.length > 0 ? ( + + } + /> + ) : null} + + + + } + /> + ); + })} + + ); +} diff --git a/raycast-theme/src/theme-utils.ts b/raycast-theme/src/theme-utils.ts new file mode 100644 index 0000000..c247292 --- /dev/null +++ b/raycast-theme/src/theme-utils.ts @@ -0,0 +1,20 @@ +export type NamedTheme = { + name: string; +}; + +const LEGACY_THEME_ALIASES: Record = { + lumon: "cool-blue", + turbonite: "amber-metal", +}; + +export function normaliseLegacyThemeName(name: string, themes: NamedTheme[]): string { + const current = name.trim(); + + // Original theme packs are still valid. Only use the newer name when the + // recorded pack is no longer installed. + if (themes.some((theme) => theme.name === current)) return current; + + const replacement = LEGACY_THEME_ALIASES[current]; + if (replacement && themes.some((theme) => theme.name === replacement)) return replacement; + return current; +} diff --git a/raycast-theme/switch-theme.js b/raycast-theme/switch-theme.js deleted file mode 100644 index 1759077..0000000 --- a/raycast-theme/switch-theme.js +++ /dev/null @@ -1,606 +0,0 @@ -"use strict"; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// src/switch-theme.tsx -var switch_theme_exports = {}; -__export(switch_theme_exports, { - default: () => Command -}); -module.exports = __toCommonJS(switch_theme_exports); -var import_api = require("@raycast/api"); -var import_fs = require("fs"); -var import_os = require("os"); -var import_path = require("path"); -var import_child_process = require("child_process"); -var import_react = require("react"); -var import_jsx_runtime = require("react/jsx-runtime"); -var THEMES_DIR = (0, import_path.join)((0, import_os.homedir)(), ".config", "themes"); -var THEME_BACKGROUND_STATE = (0, import_path.join)(THEMES_DIR, ".backgrounds.json"); -var GHOSTTY_CONFIG = (0, import_path.join)((0, import_os.homedir)(), ".config", "ghostty", "config"); -var NVIM_COLORSCHEME_FILE = (0, import_path.join)( - (0, import_os.homedir)(), - ".config", - "nvim", - "lua", - "plugins", - "colorscheme.lua" -); -var CODEX_CONFIG = (0, import_path.join)((0, import_os.homedir)(), ".codex", "config.toml"); -var THEME_SWITCH_BIN = (0, import_path.join)((0, import_os.homedir)(), ".local", "bin", "theme-switch"); -var PATH = `/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin`; -function parseThemeEnv(dir) { - const envPath = (0, import_path.join)(dir, "theme.env"); - if (!(0, import_fs.existsSync)(envPath)) return {}; - const vars = {}; - for (const line of (0, import_fs.readFileSync)(envPath, "utf-8").split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq === -1) continue; - const key = trimmed.slice(0, eq); - let val = trimmed.slice(eq + 1); - if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) { - val = val.slice(1, -1); - } - vars[key] = val; - } - return vars; -} -function getBorderHex(env) { - const ba = env.BORDER_ACTIVE || ""; - if (ba.startsWith("0xff")) return "#" + ba.slice(4); - return "#888888"; -} -function normalizeHexColor(value) { - if (!value) return void 0; - if (/^#[0-9a-fA-F]{6}$/.test(value)) return value; - if (/^0x[0-9a-fA-F]{8}$/.test(value)) return `#${value.slice(4)}`; - if (/^0x[0-9a-fA-F]{6}$/.test(value)) return `#${value.slice(2)}`; - if (/^[0-9a-fA-F]{6}$/.test(value)) return `#${value}`; - return void 0; -} -function getRgb(hex) { - return [ - Number.parseInt(hex.slice(1, 3), 16), - Number.parseInt(hex.slice(3, 5), 16), - Number.parseInt(hex.slice(5, 7), 16) - ]; -} -var APPLE_ACCENT_PRESETS = [ - { id: 0, name: "Red", color: "#ff3b30" }, - { id: 1, name: "Orange", color: "#ff9500" }, - { id: 2, name: "Yellow", color: "#ffcc00" }, - { id: 3, name: "Green", color: "#34c759" }, - { id: 4, name: "Blue", color: "#007aff" }, - { id: 5, name: "Purple", color: "#af52de" }, - { id: 6, name: "Pink", color: "#ff2d55" }, - { id: 7, name: "Graphite", color: "#8e8e93" } -]; -function getAppleAccentNameForId(id) { - if (id === -1) return "Multicolour"; - return APPLE_ACCENT_PRESETS.find((preset) => preset.id === id)?.name; -} -function resolveAppleAccentId(value) { - if (!value) return void 0; - if (/^-?\d+$/.test(value)) { - const id = Number(value); - return id >= -1 && id <= 7 ? id : void 0; - } - const key = value.toLowerCase().replace(/[\s_-]/g, ""); - const names = { - multicolor: -1, - multicolour: -1, - red: 0, - orange: 1, - yellow: 2, - green: 3, - blue: 4, - purple: 5, - pink: 6, - graphite: 7, - grey: 7, - gray: 7 - }; - return names[key]; -} -function getNearestAppleAccentName(hex) { - const [r, g, b] = getRgb(hex); - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const chroma = max - min; - if (chroma < 24) return "Graphite"; - let hue; - if (max === r) { - hue = 60 * (g - b) / chroma; - if (hue < 0) hue += 360; - } else if (max === g) { - hue = 120 + 60 * (b - r) / chroma; - } else { - hue = 240 + 60 * (r - g) / chroma; - } - if (hue < 15 || hue >= 345) return "Red"; - if (hue < 40) return "Orange"; - if (hue < 75) return "Yellow"; - if (hue < 165) return "Green"; - if (hue < 255) return "Blue"; - if (hue < 290) return "Purple"; - return "Pink"; -} -function getGhosttyColor(dir, key, fallback) { - const ghosttyPath = (0, import_path.join)(dir, "ghostty.conf"); - if (!(0, import_fs.existsSync)(ghosttyPath)) return fallback; - for (const line of (0, import_fs.readFileSync)(ghosttyPath, "utf-8").split("\n")) { - const trimmed = line.trim(); - if (!trimmed.startsWith(`${key} = `)) continue; - const value = trimmed.slice(`${key} = `.length).trim(); - if (/^#[0-9a-fA-F]{6}$/.test(value)) return value; - } - return fallback; -} -function getGhosttyPalette(dir) { - const ghosttyPath = (0, import_path.join)(dir, "ghostty.conf"); - if (!(0, import_fs.existsSync)(ghosttyPath)) return []; - const palette = []; - for (const line of (0, import_fs.readFileSync)(ghosttyPath, "utf-8").split("\n")) { - const trimmed = line.trim(); - const match = trimmed.match(/^palette\s*=\s*(\d+)=\s*(#[0-9a-fA-F]{6})$/); - if (!match) continue; - palette[Number(match[1])] = match[2]; - } - return palette; -} -function getThemeAccent(dir, env) { - const palette = getGhosttyPalette(dir); - return palette[4] || getBorderHex(env); -} -function getSystemAccentHex(dir, env) { - return normalizeHexColor(env.SYSTEM_ACCENT_COLOR) || normalizeHexColor(env.FIREFOX_ACCENT) || normalizeHexColor(env.BORDER_ACTIVE) || getThemeAccent(dir, env); -} -function getAppleAccentName(dir, env) { - return "Multicolour"; -} -function getHighlightColor(dir, env) { - return "System"; -} -function getBackgroundState() { - if (!(0, import_fs.existsSync)(THEME_BACKGROUND_STATE)) return {}; - try { - const parsed = JSON.parse((0, import_fs.readFileSync)(THEME_BACKGROUND_STATE, "utf-8")); - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; - } catch { - return {}; - } -} -function writeBackgroundState(state) { - (0, import_fs.writeFileSync)(THEME_BACKGROUND_STATE, `${JSON.stringify(state, null, 2)} -`); -} -function isImageFile(fileName) { - return [".jpg", ".jpeg", ".png", ".webp", ".heic"].includes( - (0, import_path.extname)(fileName).toLowerCase() - ); -} -function prettifyBackgroundName(fileName) { - return (0, import_path.basename)(fileName, (0, import_path.extname)(fileName)).replace(/^\d+[-_.\s]*/, "").replace(/[-_.]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); -} -function getThemeBackgrounds(theme) { - const bgDir = (0, import_path.join)(THEMES_DIR, theme.name, "backgrounds"); - if (!(0, import_fs.existsSync)(bgDir)) return []; - return (0, import_fs.readdirSync)(bgDir, { withFileTypes: true }).filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isImageFile(entry.name)).map((entry) => { - const relativePath = `backgrounds/${entry.name}`; - const previewPath = (0, import_path.join)( - bgDir, - ".raycast-blur", - `${(0, import_path.basename)(entry.name, (0, import_path.extname)(entry.name))}.jpg` - ); - const path = (0, import_path.join)(bgDir, entry.name); - return { - themeName: theme.name, - relativePath, - path, - previewPath: (0, import_fs.existsSync)(previewPath) ? previewPath : path, - displayName: prettifyBackgroundName(entry.name) - }; - }).sort((a, b) => a.relativePath.localeCompare(b.relativePath)); -} -function getSelectedBackground(theme) { - const backgrounds = getThemeBackgrounds(theme); - const state = getBackgroundState(); - const selectedRelative = state[theme.name] || theme.env.WALLPAPER; - return backgrounds.find( - (background) => background.relativePath === selectedRelative - ) || backgrounds[0]; -} -function getSketchybarAccent(dir, env) { - const palette = getGhosttyPalette(dir); - return palette[4] || getBorderHex(env); -} -function getThemes() { - if (!(0, import_fs.existsSync)(THEMES_DIR)) return []; - return (0, import_fs.readdirSync)(THEMES_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() || d.isSymbolicLink()).map((d) => { - const dir = (0, import_path.join)(THEMES_DIR, d.name); - const env = parseThemeEnv(dir); - const bgDir = (0, import_path.join)(dir, "backgrounds"); - const bgCount = (0, import_fs.existsSync)(bgDir) ? (0, import_fs.readdirSync)(bgDir, { withFileTypes: true }).filter( - (entry) => entry.isFile() && isImageFile(entry.name) - ).length : 0; - const selectedWallpaper = getBackgroundState()[d.name] || env.WALLPAPER || ""; - return { - name: d.name, - displayName: d.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "), - env, - hasWallpaper: (0, import_fs.existsSync)((0, import_path.join)(dir, selectedWallpaper)), - wallpaperPath: (0, import_fs.existsSync)((0, import_path.join)(dir, selectedWallpaper)) ? (0, import_path.join)(dir, selectedWallpaper) : void 0, - accentColor: getThemeAccent(dir, env), - sketchybarAccentColor: getSketchybarAccent(dir, env), - textColor: getGhosttyColor( - dir, - "foreground", - env.DARK_MODE === "true" ? "#ffffff" : "#000000" - ), - palette: getGhosttyPalette(dir), - isDark: env.DARK_MODE === "true", - bgCount, - appleAccentName: getAppleAccentName(dir, env), - highlightColor: getHighlightColor(dir, env) - }; - }).sort((a, b) => a.name.localeCompare(b.name)); -} -function getCurrentTheme() { - const path = (0, import_path.join)(THEMES_DIR, ".current"); - if (!(0, import_fs.existsSync)(path)) return ""; - return (0, import_fs.readFileSync)(path, "utf-8").trim(); -} -function spawnThemeApplyWorker(theme) { - const workerSource = String.raw` -const { existsSync } = require("fs"); -const { homedir } = require("os"); -const { join } = require("path"); -const { spawn } = require("child_process"); - -const theme = JSON.parse(process.argv[1]); -const THEME_SWITCH_BIN = join(homedir(), ".local", "bin", "theme-switch"); -const PATH = ${JSON.stringify(PATH)}; - -function runThemeSwitch(args) { - return new Promise((resolve, reject) => { - const child = spawn(THEME_SWITCH_BIN, args, { - env: { ...process.env, HOME: homedir(), PATH }, - stdio: "ignore", - }); - child.on("error", reject); - child.on("exit", (code) => { - if (code === 0) resolve(); - else reject(new Error("theme-switch exited " + code)); - }); - }); -} - -async function main() { - const { name } = theme; - if (!existsSync(THEME_SWITCH_BIN)) process.exit(1); - // Run synchronously: theme-switch writes ~/.config/themes/.current only after Ghostty/Raycast/etc. finish. - // (Backgrounding with & caused the UI to show "active" before emulators actually reloaded.) - await runThemeSwitch([name]); -} - -main().catch(() => process.exit(1)); -`; - const worker = (0, import_child_process.spawn)( - process.execPath, - ["-e", workerSource, JSON.stringify(theme)], - { - detached: true, - stdio: "ignore" - } - ); - worker.unref(); -} -function spawnBackgroundApplyWorker(theme, background) { - const workerSource = String.raw` -const { homedir } = require("os"); -const { join } = require("path"); -const { spawn } = require("child_process"); - -const themeName = process.argv[1]; -const backgroundPath = process.argv[2]; -const THEME_SWITCH_BIN = join(homedir(), ".local", "bin", "theme-switch"); -const PATH = ${JSON.stringify(PATH)}; - -const child = spawn(THEME_SWITCH_BIN, ["--background", themeName, backgroundPath], { - detached: true, - env: { ...process.env, HOME: homedir(), PATH }, - stdio: "ignore", -}); -child.unref(); -`; - const worker = (0, import_child_process.spawn)( - process.execPath, - ["-e", workerSource, theme.name, background.relativePath], - { - detached: true, - stdio: "ignore" - } - ); - worker.unref(); -} -function BackgroundPicker(props) { - const allBackgrounds = getThemeBackgrounds(props.theme); - const selected = getSelectedBackground(props.theme); - const backgrounds = selected ? [ - selected, - ...allBackgrounds.filter( - (bg) => bg.relativePath !== selected.relativePath - ) - ] : allBackgrounds; - return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.Grid, - { - columns: 2, - inset: import_api.Grid.Inset.Small, - fit: import_api.Grid.Fit.Fill, - aspectRatio: "16/9", - navigationTitle: `Switch Background (${props.theme.displayName})`, - searchBarPlaceholder: "Search backgrounds...", - children: backgrounds.map((background) => { - const isSelected = background.relativePath === selected?.relativePath; - return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.Grid.Item, - { - title: background.displayName, - accessory: isSelected ? { icon: import_api.Icon.CheckCircle, tooltip: "Current" } : void 0, - content: { source: background.previewPath }, - actions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_api.ActionPanel, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.Action, - { - title: "Use Background", - icon: import_api.Icon.Image, - onAction: async () => { - const state = getBackgroundState(); - state[props.theme.name] = background.relativePath; - writeBackgroundState(state); - props.onSelect(props.theme.name, background.relativePath); - spawnBackgroundApplyWorker(props.theme, background); - await (0, import_api.closeMainWindow)(); - } - } - ) }) - }, - background.relativePath - ); - }) - } - ); -} -function Command() { - const themes = getThemes(); - const [current, setCurrent] = (0, import_react.useState)(() => getCurrentTheme()); - const [pendingTheme, setPendingTheme] = (0, import_react.useState)(null); - const [backgroundVersion, setBackgroundVersion] = (0, import_react.useState)(0); - (0, import_react.useEffect)(() => { - const syncCurrentTheme = () => { - const next = getCurrentTheme(); - setCurrent((prev) => prev === next ? prev : next); - }; - syncCurrentTheme(); - const interval = setInterval(syncCurrentTheme, 750); - return () => clearInterval(interval); - }, []); - function getThemePreview(theme) { - const selectedBackground = getSelectedBackground(theme); - const wallpaperPreview = selectedBackground ? `![${theme.displayName}](${encodeURI(`file://${selectedBackground.previewPath}`)}?raycast-width=720&raycast-height=405)` : "_No wallpaper preview available_"; - const lines = [ - `# ${theme.displayName}`, - "", - wallpaperPreview, - "", - selectedBackground ? `Background \`${selectedBackground.displayName}\`` : "", - `Accent \`${theme.accentColor}\``, - `macOS Accent \`${theme.appleAccentName}\``, - `Highlight \`${theme.highlightColor}\``, - `Text \`${theme.textColor}\`` - ]; - return lines.join("\n"); - } - function getLsPreviewColors(theme) { - return { - directory: theme.palette[4] || theme.accentColor, - symlink: theme.palette[6] || theme.accentColor, - executable: theme.palette[2] || theme.accentColor, - archive: theme.palette[1] || theme.accentColor, - device: theme.palette[3] || theme.accentColor, - normal: theme.textColor - }; - } - const activeTheme = themes.find((theme) => theme.name === current) || themes[0]; - const activeBackground = activeTheme ? getSelectedBackground(activeTheme) : void 0; - function getBackgroundPreviewMarkdown(theme) { - const selected = getSelectedBackground(theme); - if (!selected) return "_No background options available_"; - return `![${selected.displayName}](${encodeURI(`file://${selected.previewPath}`)}?raycast-width=720&raycast-height=405)`; - } - return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_api.List, { isShowingDetail: true, searchBarPlaceholder: "Search themes...", children: [ - activeTheme ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item, - { - title: "Switch Background", - subtitle: activeTheme.displayName, - icon: import_api.Icon.AppWindowGrid2x2, - accessories: [{ text: `${activeTheme.bgCount} options` }], - detail: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail, - { - markdown: getBackgroundPreviewMarkdown(activeTheme) - } - ), - actions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_api.ActionPanel, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.Action.Push, - { - title: "Open Backgrounds", - icon: import_api.Icon.Folder, - target: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - BackgroundPicker, - { - theme: activeTheme, - onSelect: () => setBackgroundVersion((version) => version + 1) - } - ) - } - ) }) - }, - `switch-background-${backgroundVersion}` - ) : null, - themes.map((theme) => { - const isCurrent = theme.name === (pendingTheme || current); - const accessories = []; - if (isCurrent) - accessories.push({ tag: { value: "active", color: import_api.Color.Green } }); - accessories.push({ icon: theme.isDark ? import_api.Icon.Moon : import_api.Icon.Sun }); - if (theme.bgCount > 0) - accessories.push({ text: `${theme.bgCount} bg` }); - return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item, - { - title: theme.displayName, - icon: { - source: import_api.Icon.Circle, - tintColor: theme.accentColor - }, - accessories, - detail: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail, - { - markdown: getThemePreview(theme), - metadata: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_api.List.Item.Detail.Metadata, { children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.Label, - { - title: "Mode", - text: theme.isDark ? "Dark" : "Light" - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_api.List.Item.Detail.Metadata.Separator, {}), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.Label, - { - title: "Wallpapers", - text: String( - theme.bgCount || (theme.hasWallpaper ? 1 : 0) - ) - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_api.List.Item.Detail.Metadata.Separator, {}), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.Label, - { - title: "macOS Accent", - text: theme.appleAccentName - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.Label, - { - title: "Highlight", - text: theme.highlightColor - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_api.List.Item.Detail.Metadata.Separator, {}), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_api.List.Item.Detail.Metadata.TagList, { title: "ls -la", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.TagList.Item, - { - text: "dir/", - color: getLsPreviewColors(theme).directory - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.TagList.Item, - { - text: "link@", - color: getLsPreviewColors(theme).symlink - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.TagList.Item, - { - text: "exec*", - color: getLsPreviewColors(theme).executable - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.TagList.Item, - { - text: "archive", - color: getLsPreviewColors(theme).archive - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.TagList.Item, - { - text: "device", - color: getLsPreviewColors(theme).device - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.List.Item.Detail.Metadata.TagList.Item, - { - text: "file", - color: getLsPreviewColors(theme).normal - } - ) - ] }) - ] }) - } - ), - actions: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_api.ActionPanel, { children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.Action, - { - title: "Apply Theme", - icon: import_api.Icon.Brush, - shortcut: { modifiers: [], key: "return" }, - onAction: async () => { - spawnThemeApplyWorker(theme); - await (0, import_api.closeMainWindow)(); - } - } - ), - theme.bgCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - import_api.Action.Push, - { - title: "Switch Background", - icon: import_api.Icon.Folder, - shortcut: { modifiers: ["cmd"], key: "b" }, - target: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - BackgroundPicker, - { - theme, - onSelect: () => setBackgroundVersion((version) => version + 1) - } - ) - } - ) : null - ] }) - }, - theme.name - ); - }) - ] }); -} -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vLi4vLi4vRGV2ZWxvcGVyL3RoZW1lLXN3aXRjaGVyL3NyYy9zd2l0Y2gtdGhlbWUudHN4Il0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQge1xuICBBY3Rpb25QYW5lbCxcbiAgQWN0aW9uLFxuICBMaXN0LFxuICBHcmlkLFxuICBJY29uLFxuICBDb2xvcixcbiAgY2xvc2VNYWluV2luZG93LFxufSBmcm9tIFwiQHJheWNhc3QvYXBpXCI7XG5pbXBvcnQgeyByZWFkZGlyU3luYywgcmVhZEZpbGVTeW5jLCBleGlzdHNTeW5jLCB3cml0ZUZpbGVTeW5jIH0gZnJvbSBcImZzXCI7XG5pbXBvcnQgeyBob21lZGlyIH0gZnJvbSBcIm9zXCI7XG5pbXBvcnQgeyBiYXNlbmFtZSwgZXh0bmFtZSwgam9pbiB9IGZyb20gXCJwYXRoXCI7XG5pbXBvcnQgeyBleGVjLCBzcGF3biwgdHlwZSBDaGlsZFByb2Nlc3MgfSBmcm9tIFwiY2hpbGRfcHJvY2Vzc1wiO1xuaW1wb3J0IHsgdXNlRWZmZWN0LCB1c2VTdGF0ZSB9IGZyb20gXCJyZWFjdFwiO1xuXG5jb25zdCBUSEVNRVNfRElSID0gam9pbihob21lZGlyKCksIFwiLnRoZW1lc1wiKTtcbmNvbnN0IFRIRU1FX0JBQ0tHUk9VTkRfU1RBVEUgPSBqb2luKFRIRU1FU19ESVIsIFwiLmJhY2tncm91bmRzLmpzb25cIik7XG5jb25zdCBHSE9TVFRZX0NPTkZJRyA9IGpvaW4oaG9tZWRpcigpLCBcIi5jb25maWdcIiwgXCJnaG9zdHR5XCIsIFwiY29uZmlnXCIpO1xuY29uc3QgTlZJTV9DT0xPUlNDSEVNRV9GSUxFID0gam9pbihcbiAgaG9tZWRpcigpLFxuICBcIi5jb25maWdcIixcbiAgXCJudmltXCIsXG4gIFwibHVhXCIsXG4gIFwicGx1Z2luc1wiLFxuICBcImNvbG9yc2NoZW1lLmx1YVwiLFxuKTtcbmNvbnN0IENPREVYX0NPTkZJRyA9IGpvaW4oaG9tZWRpcigpLCBcIi5jb2RleFwiLCBcImNvbmZpZy50b21sXCIpO1xuY29uc3QgQVBQTFlfU0tFVENIWUJBUl9USEVNRSA9IGpvaW4oXG4gIGhvbWVkaXIoKSxcbiAgXCIubG9jYWxcIixcbiAgXCJiaW5cIixcbiAgXCJhcHBseS1za2V0Y2h5YmFyLXRoZW1lXCIsXG4pO1xuY29uc3QgVEhFTUVfU1dJVENIX0JJTiA9IGpvaW4oaG9tZWRpcigpLCBcIi5sb2NhbFwiLCBcImJpblwiLCBcInRoZW1lLXN3aXRjaFwiKTtcbmNvbnN0IFJBWUNBU1RfQlVORExFX0lEID0gXCJjb20ucmF5Y2FzdC5tYWNvc1wiO1xuY29uc3QgUEFUSCA9IGAvdXNyL2JpbjovYmluOi91c3Ivc2Jpbjovc2Jpbjovb3B0L2hvbWVicmV3L2JpbjovdXNyL2xvY2FsL2JpbmA7XG5cbmludGVyZmFjZSBUaGVtZUVudiB7XG4gIERBUktfTU9ERT86IHN0cmluZztcbiAgV0FMTFBBUEVSPzogc3RyaW5nO1xuICBHSE9TVFRZX0NPTkY/OiBzdHJpbmc7XG4gIE5WSU1fQ09MT1JTQ0hFTUU/OiBzdHJpbmc7XG4gIEJPUkRFUl9BQ1RJVkU/OiBzdHJpbmc7XG4gIEJPUkRFUl9JTkFDVElWRT86IHN0cmluZztcbiAgQk9SREVSX1dJRFRIPzogc3RyaW5nO1xuICBGSVJFRk9YX0FDQ0VOVD86IHN0cmluZztcbiAgU1lTVEVNX0FDQ0VOVF9DT0xPUj86IHN0cmluZztcbiAgQVBQTEVfQUNDRU5UPzogc3RyaW5nO1xuICBBUFBMRV9ISUdITElHSFQ/OiBzdHJpbmc7XG59XG5cbmludGVyZmFjZSBUaGVtZUVudHJ5IHtcbiAgbmFtZTogc3RyaW5nO1xuICBkaXNwbGF5TmFtZTogc3RyaW5nO1xuICBlbnY6IFRoZW1lRW52O1xuICBoYXNXYWxscGFwZXI6IGJvb2xlYW47XG4gIHdhbGxwYXBlclBhdGg/OiBzdHJpbmc7XG4gIGFjY2VudENvbG9yOiBzdHJpbmc7XG4gIHNrZXRjaHliYXJBY2NlbnRDb2xvcjogc3RyaW5nO1xuICB0ZXh0Q29sb3I6IHN0cmluZztcbiAgcGFsZXR0ZTogc3RyaW5nW107XG4gIGlzRGFyazogYm9vbGVhbjtcbiAgYmdDb3VudDogbnVtYmVyO1xuICBhcHBsZUFjY2VudE5hbWU6IHN0cmluZztcbiAgaGlnaGxpZ2h0Q29sb3I6IHN0cmluZztcbn1cblxuaW50ZXJmYWNlIEJhY2tncm91bmRFbnRyeSB7XG4gIHRoZW1lTmFtZTogc3RyaW5nO1xuICByZWxhdGl2ZVBhdGg6IHN0cmluZztcbiAgcGF0aDogc3RyaW5nO1xuICBwcmV2aWV3UGF0aDogc3RyaW5nO1xuICBkaXNwbGF5TmFtZTogc3RyaW5nO1xufVxuXG50eXBlIEJhY2tncm91bmRTdGF0ZSA9IFJlY29yZDxzdHJpbmcsIHN0cmluZz47XG5cbmZ1bmN0aW9uIHBhcnNlVGhlbWVFbnYoZGlyOiBzdHJpbmcpOiBUaGVtZUVudiB7XG4gIGNvbnN0IGVudlBhdGggPSBqb2luKGRpciwgXCJ0aGVtZS5lbnZcIik7XG4gIGlmICghZXhpc3RzU3luYyhlbnZQYXRoKSkgcmV0dXJuIHt9O1xuICBjb25zdCB2YXJzOiBUaGVtZUVudiA9IHt9O1xuICBmb3IgKGNvbnN0IGxpbmUgb2YgcmVhZEZpbGVTeW5jKGVudlBhdGgsIFwidXRmLThcIikuc3BsaXQoXCJcXG5cIikpIHtcbiAgICBjb25zdCB0cmltbWVkID0gbGluZS50cmltKCk7XG4gICAgaWYgKCF0cmltbWVkIHx8IHRyaW1tZWQuc3RhcnRzV2l0aChcIiNcIikpIGNvbnRpbnVlO1xuICAgIGNvbnN0IGVxID0gdHJpbW1lZC5pbmRleE9mKFwiPVwiKTtcbiAgICBpZiAoZXEgPT09IC0xKSBjb250aW51ZTtcbiAgICBjb25zdCBrZXkgPSB0cmltbWVkLnNsaWNlKDAsIGVxKSBhcyBrZXlvZiBUaGVtZUVudjtcbiAgICBsZXQgdmFsID0gdHJpbW1lZC5zbGljZShlcSArIDEpO1xuICAgIGlmIChcbiAgICAgICh2YWwuc3RhcnRzV2l0aCgnXCInKSAmJiB2YWwuZW5kc1dpdGgoJ1wiJykpIHx8XG4gICAgICAodmFsLnN0YXJ0c1dpdGgoXCInXCIpICYmIHZhbC5lbmRzV2l0aChcIidcIikpXG4gICAgKSB7XG4gICAgICB2YWwgPSB2YWwuc2xpY2UoMSwgLTEpO1xuICAgIH1cbiAgICB2YXJzW2tleV0gPSB2YWw7XG4gIH1cbiAgcmV0dXJuIHZhcnM7XG59XG5cbmZ1bmN0aW9uIGdldEJvcmRlckhleChlbnY6IFRoZW1lRW52KTogc3RyaW5nIHtcbiAgY29uc3QgYmEgPSBlbnYuQk9SREVSX0FDVElWRSB8fCBcIlwiO1xuICAvLyAweGZmUlJHR0JCIC0+ICNSUkdHQkJcbiAgaWYgKGJhLnN0YXJ0c1dpdGgoXCIweGZmXCIpKSByZXR1cm4gXCIjXCIgKyBiYS5zbGljZSg0KTtcbiAgcmV0dXJuIFwiIzg4ODg4OFwiO1xufVxuXG5mdW5jdGlvbiBub3JtYWxpemVIZXhDb2xvcih2YWx1ZTogc3RyaW5nIHwgdW5kZWZpbmVkKTogc3RyaW5nIHwgdW5kZWZpbmVkIHtcbiAgaWYgKCF2YWx1ZSkgcmV0dXJuIHVuZGVmaW5lZDtcbiAgaWYgKC9eI1swLTlhLWZBLUZdezZ9JC8udGVzdCh2YWx1ZSkpIHJldHVybiB2YWx1ZTtcbiAgaWYgKC9eMHhbMC05YS1mQS1GXXs4fSQvLnRlc3QodmFsdWUpKSByZXR1cm4gYCMke3ZhbHVlLnNsaWNlKDQpfWA7XG4gIGlmICgvXjB4WzAtOWEtZkEtRl17Nn0kLy50ZXN0KHZhbHVlKSkgcmV0dXJuIGAjJHt2YWx1ZS5zbGljZSgyKX1gO1xuICBpZiAoL15bMC05YS1mQS1GXXs2fSQvLnRlc3QodmFsdWUpKSByZXR1cm4gYCMke3ZhbHVlfWA7XG4gIHJldHVybiB1bmRlZmluZWQ7XG59XG5cbmZ1bmN0aW9uIGdldFJnYihoZXg6IHN0cmluZyk6IFtudW1iZXIsIG51bWJlciwgbnVtYmVyXSB7XG4gIHJldHVybiBbXG4gICAgTnVtYmVyLnBhcnNlSW50KGhleC5zbGljZSgxLCAzKSwgMTYpLFxuICAgIE51bWJlci5wYXJzZUludChoZXguc2xpY2UoMywgNSksIDE2KSxcbiAgICBOdW1iZXIucGFyc2VJbnQoaGV4LnNsaWNlKDUsIDcpLCAxNiksXG4gIF07XG59XG5cbnR5cGUgQXBwbGVBY2NlbnRQcmVzZXQgPSB7XG4gIGlkOiBudW1iZXI7XG4gIG5hbWU6IHN0cmluZztcbiAgY29sb3I6IHN0cmluZztcbn07XG5cbmNvbnN0IEFQUExFX0FDQ0VOVF9QUkVTRVRTOiBBcHBsZUFjY2VudFByZXNldFtdID0gW1xuICB7IGlkOiAwLCBuYW1lOiBcIlJlZFwiLCBjb2xvcjogXCIjZmYzYjMwXCIgfSxcbiAgeyBpZDogMSwgbmFtZTogXCJPcmFuZ2VcIiwgY29sb3I6IFwiI2ZmOTUwMFwiIH0sXG4gIHsgaWQ6IDIsIG5hbWU6IFwiWWVsbG93XCIsIGNvbG9yOiBcIiNmZmNjMDBcIiB9LFxuICB7IGlkOiAzLCBuYW1lOiBcIkdyZWVuXCIsIGNvbG9yOiBcIiMzNGM3NTlcIiB9LFxuICB7IGlkOiA0LCBuYW1lOiBcIkJsdWVcIiwgY29sb3I6IFwiIzAwN2FmZlwiIH0sXG4gIHsgaWQ6IDUsIG5hbWU6IFwiUHVycGxlXCIsIGNvbG9yOiBcIiNhZjUyZGVcIiB9LFxuICB7IGlkOiA2LCBuYW1lOiBcIlBpbmtcIiwgY29sb3I6IFwiI2ZmMmQ1NVwiIH0sXG4gIHsgaWQ6IDcsIG5hbWU6IFwiR3JhcGhpdGVcIiwgY29sb3I6IFwiIzhlOGU5M1wiIH0sXG5dO1xuXG5mdW5jdGlvbiBnZXRBcHBsZUFjY2VudE5hbWVGb3JJZChpZDogbnVtYmVyKTogc3RyaW5nIHwgdW5kZWZpbmVkIHtcbiAgaWYgKGlkID09PSAtMSkgcmV0dXJuIFwiTXVsdGljb2xvdXJcIjtcbiAgcmV0dXJuIEFQUExFX0FDQ0VOVF9QUkVTRVRTLmZpbmQoKHByZXNldCkgPT4gcHJlc2V0LmlkID09PSBpZCk/Lm5hbWU7XG59XG5cbmZ1bmN0aW9uIHJlc29sdmVBcHBsZUFjY2VudElkKHZhbHVlOiBzdHJpbmcgfCB1bmRlZmluZWQpOiBudW1iZXIgfCB1bmRlZmluZWQge1xuICBpZiAoIXZhbHVlKSByZXR1cm4gdW5kZWZpbmVkO1xuICBpZiAoL14tP1xcZCskLy50ZXN0KHZhbHVlKSkge1xuICAgIGNvbnN0IGlkID0gTnVtYmVyKHZhbHVlKTtcbiAgICByZXR1cm4gaWQgPj0gLTEgJiYgaWQgPD0gNyA/IGlkIDogdW5kZWZpbmVkO1xuICB9XG5cbiAgY29uc3Qga2V5ID0gdmFsdWUudG9Mb3dlckNhc2UoKS5yZXBsYWNlKC9bXFxzXy1dL2csIFwiXCIpO1xuICBjb25zdCBuYW1lczogUmVjb3JkPHN0cmluZywgbnVtYmVyPiA9IHtcbiAgICBtdWx0aWNvbG9yOiAtMSxcbiAgICBtdWx0aWNvbG91cjogLTEsXG4gICAgcmVkOiAwLFxuICAgIG9yYW5nZTogMSxcbiAgICB5ZWxsb3c6IDIsXG4gICAgZ3JlZW46IDMsXG4gICAgYmx1ZTogNCxcbiAgICBwdXJwbGU6IDUsXG4gICAgcGluazogNixcbiAgICBncmFwaGl0ZTogNyxcbiAgICBncmV5OiA3LFxuICAgIGdyYXk6IDcsXG4gIH07XG5cbiAgcmV0dXJuIG5hbWVzW2tleV07XG59XG5cbmZ1bmN0aW9uIGdldE5lYXJlc3RBcHBsZUFjY2VudE5hbWUoaGV4OiBzdHJpbmcpOiBzdHJpbmcge1xuICBjb25zdCBbciwgZywgYl0gPSBnZXRSZ2IoaGV4KTtcbiAgY29uc3QgbWF4ID0gTWF0aC5tYXgociwgZywgYik7XG4gIGNvbnN0IG1pbiA9IE1hdGgubWluKHIsIGcsIGIpO1xuICBjb25zdCBjaHJvbWEgPSBtYXggLSBtaW47XG5cbiAgaWYgKGNocm9tYSA8IDI0KSByZXR1cm4gXCJHcmFwaGl0ZVwiO1xuXG4gIGxldCBodWU6IG51bWJlcjtcbiAgaWYgKG1heCA9PT0gcikge1xuICAgIGh1ZSA9ICg2MCAqIChnIC0gYikpIC8gY2hyb21hO1xuICAgIGlmIChodWUgPCAwKSBodWUgKz0gMzYwO1xuICB9IGVsc2UgaWYgKG1heCA9PT0gZykge1xuICAgIGh1ZSA9IDEyMCArICg2MCAqIChiIC0gcikpIC8gY2hyb21hO1xuICB9IGVsc2Uge1xuICAgIGh1ZSA9IDI0MCArICg2MCAqIChyIC0gZykpIC8gY2hyb21hO1xuICB9XG5cbiAgaWYgKGh1ZSA8IDE1IHx8IGh1ZSA+PSAzNDUpIHJldHVybiBcIlJlZFwiO1xuICBpZiAoaHVlIDwgNDApIHJldHVybiBcIk9yYW5nZVwiO1xuICBpZiAoaHVlIDwgNzUpIHJldHVybiBcIlllbGxvd1wiO1xuICBpZiAoaHVlIDwgMTY1KSByZXR1cm4gXCJHcmVlblwiO1xuICBpZiAoaHVlIDwgMjU1KSByZXR1cm4gXCJCbHVlXCI7XG4gIGlmIChodWUgPCAyOTApIHJldHVybiBcIlB1cnBsZVwiO1xuICByZXR1cm4gXCJQaW5rXCI7XG59XG5cbmZ1bmN0aW9uIGdldEdob3N0dHlDb2xvcihkaXI6IHN0cmluZywga2V5OiBzdHJpbmcsIGZhbGxiYWNrOiBzdHJpbmcpOiBzdHJpbmcge1xuICBjb25zdCBnaG9zdHR5UGF0aCA9IGpvaW4oZGlyLCBcImdob3N0dHkuY29uZlwiKTtcbiAgaWYgKCFleGlzdHNTeW5jKGdob3N0dHlQYXRoKSkgcmV0dXJuIGZhbGxiYWNrO1xuICBmb3IgKGNvbnN0IGxpbmUgb2YgcmVhZEZpbGVTeW5jKGdob3N0dHlQYXRoLCBcInV0Zi04XCIpLnNwbGl0KFwiXFxuXCIpKSB7XG4gICAgY29uc3QgdHJpbW1lZCA9IGxpbmUudHJpbSgpO1xuICAgIGlmICghdHJpbW1lZC5zdGFydHNXaXRoKGAke2tleX0gPSBgKSkgY29udGludWU7XG4gICAgY29uc3QgdmFsdWUgPSB0cmltbWVkLnNsaWNlKGAke2tleX0gPSBgLmxlbmd0aCkudHJpbSgpO1xuICAgIGlmICgvXiNbMC05YS1mQS1GXXs2fSQvLnRlc3QodmFsdWUpKSByZXR1cm4gdmFsdWU7XG4gIH1cbiAgcmV0dXJuIGZhbGxiYWNrO1xufVxuXG5mdW5jdGlvbiBnZXRHaG9zdHR5UGFsZXR0ZShkaXI6IHN0cmluZyk6IHN0cmluZ1tdIHtcbiAgY29uc3QgZ2hvc3R0eVBhdGggPSBqb2luKGRpciwgXCJnaG9zdHR5LmNvbmZcIik7XG4gIGlmICghZXhpc3RzU3luYyhnaG9zdHR5UGF0aCkpIHJldHVybiBbXTtcbiAgY29uc3QgcGFsZXR0ZTogc3RyaW5nW10gPSBbXTtcbiAgZm9yIChjb25zdCBsaW5lIG9mIHJlYWRGaWxlU3luYyhnaG9zdHR5UGF0aCwgXCJ1dGYtOFwiKS5zcGxpdChcIlxcblwiKSkge1xuICAgIGNvbnN0IHRyaW1tZWQgPSBsaW5lLnRyaW0oKTtcbiAgICBjb25zdCBtYXRjaCA9IHRyaW1tZWQubWF0Y2goL15wYWxldHRlXFxzKj1cXHMqKFxcZCspPVxccyooI1swLTlhLWZBLUZdezZ9KSQvKTtcbiAgICBpZiAoIW1hdGNoKSBjb250aW51ZTtcbiAgICBwYWxldHRlW051bWJlcihtYXRjaFsxXSldID0gbWF0Y2hbMl07XG4gIH1cbiAgcmV0dXJuIHBhbGV0dGU7XG59XG5cbmZ1bmN0aW9uIGdldFRoZW1lQWNjZW50KGRpcjogc3RyaW5nLCBlbnY6IFRoZW1lRW52KTogc3RyaW5nIHtcbiAgY29uc3QgcGFsZXR0ZSA9IGdldEdob3N0dHlQYWxldHRlKGRpcik7XG4gIHJldHVybiBwYWxldHRlWzRdIHx8IGdldEJvcmRlckhleChlbnYpO1xufVxuXG5mdW5jdGlvbiBnZXRTeXN0ZW1BY2NlbnRIZXgoZGlyOiBzdHJpbmcsIGVudjogVGhlbWVFbnYpOiBzdHJpbmcge1xuICByZXR1cm4gKFxuICAgIG5vcm1hbGl6ZUhleENvbG9yKGVudi5TWVNURU1fQUNDRU5UX0NPTE9SKSB8fFxuICAgIG5vcm1hbGl6ZUhleENvbG9yKGVudi5GSVJFRk9YX0FDQ0VOVCkgfHxcbiAgICBub3JtYWxpemVIZXhDb2xvcihlbnYuQk9SREVSX0FDVElWRSkgfHxcbiAgICBnZXRUaGVtZUFjY2VudChkaXIsIGVudilcbiAgKTtcbn1cblxuZnVuY3Rpb24gZ2V0QXBwbGVBY2NlbnROYW1lKGRpcjogc3RyaW5nLCBlbnY6IFRoZW1lRW52KTogc3RyaW5nIHtcbiAgY29uc3QgZXhwbGljaXRJZCA9IHJlc29sdmVBcHBsZUFjY2VudElkKGVudi5BUFBMRV9BQ0NFTlQpO1xuICBpZiAoZXhwbGljaXRJZCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgcmV0dXJuIGdldEFwcGxlQWNjZW50TmFtZUZvcklkKGV4cGxpY2l0SWQpIHx8IFwiQmx1ZVwiO1xuICB9XG5cbiAgY29uc3QgZXhwbGljaXRIZXggPSBub3JtYWxpemVIZXhDb2xvcihlbnYuQVBQTEVfQUNDRU5UKTtcbiAgcmV0dXJuIGdldE5lYXJlc3RBcHBsZUFjY2VudE5hbWUoZXhwbGljaXRIZXggfHwgZ2V0U3lzdGVtQWNjZW50SGV4KGRpciwgZW52KSk7XG59XG5cbmZ1bmN0aW9uIGdldEhpZ2hsaWdodENvbG9yKGRpcjogc3RyaW5nLCBlbnY6IFRoZW1lRW52KTogc3RyaW5nIHtcbiAgY29uc3QgYWNjZW50ID0gZ2V0U3lzdGVtQWNjZW50SGV4KGRpciwgZW52KTtcbiAgcmV0dXJuIChcbiAgICBub3JtYWxpemVIZXhDb2xvcihlbnYuQVBQTEVfSElHSExJR0hUKSB8fFxuICAgIG5vcm1hbGl6ZUhleENvbG9yKGdldEdob3N0dHlDb2xvcihkaXIsIFwic2VsZWN0aW9uLWJhY2tncm91bmRcIiwgYWNjZW50KSkgfHxcbiAgICBhY2NlbnRcbiAgKTtcbn1cblxuZnVuY3Rpb24gZ2V0QmFja2dyb3VuZFN0YXRlKCk6IEJhY2tncm91bmRTdGF0ZSB7XG4gIGlmICghZXhpc3RzU3luYyhUSEVNRV9CQUNLR1JPVU5EX1NUQVRFKSkgcmV0dXJuIHt9O1xuICB0cnkge1xuICAgIGNvbnN0IHBhcnNlZCA9IEpTT04ucGFyc2UocmVhZEZpbGVTeW5jKFRIRU1FX0JBQ0tHUk9VTkRfU1RBVEUsIFwidXRmLThcIikpO1xuICAgIHJldHVybiBwYXJzZWQgJiYgdHlwZW9mIHBhcnNlZCA9PT0gXCJvYmplY3RcIiAmJiAhQXJyYXkuaXNBcnJheShwYXJzZWQpXG4gICAgICA/IHBhcnNlZFxuICAgICAgOiB7fTtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIHt9O1xuICB9XG59XG5cbmZ1bmN0aW9uIHdyaXRlQmFja2dyb3VuZFN0YXRlKHN0YXRlOiBCYWNrZ3JvdW5kU3RhdGUpIHtcbiAgd3JpdGVGaWxlU3luYyhUSEVNRV9CQUNLR1JPVU5EX1NUQVRFLCBgJHtKU09OLnN0cmluZ2lmeShzdGF0ZSwgbnVsbCwgMil9XFxuYCk7XG59XG5cbmZ1bmN0aW9uIGlzSW1hZ2VGaWxlKGZpbGVOYW1lOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgcmV0dXJuIFtcIi5qcGdcIiwgXCIuanBlZ1wiLCBcIi5wbmdcIiwgXCIud2VicFwiLCBcIi5oZWljXCJdLmluY2x1ZGVzKFxuICAgIGV4dG5hbWUoZmlsZU5hbWUpLnRvTG93ZXJDYXNlKCksXG4gICk7XG59XG5cbmZ1bmN0aW9uIHByZXR0aWZ5QmFja2dyb3VuZE5hbWUoZmlsZU5hbWU6IHN0cmluZyk6IHN0cmluZyB7XG4gIHJldHVybiBiYXNlbmFtZShmaWxlTmFtZSwgZXh0bmFtZShmaWxlTmFtZSkpXG4gICAgLnJlcGxhY2UoL15cXGQrWy1fLlxcc10qLywgXCJcIilcbiAgICAucmVwbGFjZSgvWy1fLl0rL2csIFwiIFwiKVxuICAgIC5yZXBsYWNlKC9cXGJcXHcvZywgKGxldHRlcikgPT4gbGV0dGVyLnRvVXBwZXJDYXNlKCkpO1xufVxuXG5mdW5jdGlvbiBnZXRUaGVtZUJhY2tncm91bmRzKHRoZW1lOiBUaGVtZUVudHJ5KTogQmFja2dyb3VuZEVudHJ5W10ge1xuICBjb25zdCBiZ0RpciA9IGpvaW4oVEhFTUVTX0RJUiwgdGhlbWUubmFtZSwgXCJiYWNrZ3JvdW5kc1wiKTtcbiAgaWYgKCFleGlzdHNTeW5jKGJnRGlyKSkgcmV0dXJuIFtdO1xuXG4gIHJldHVybiByZWFkZGlyU3luYyhiZ0RpciwgeyB3aXRoRmlsZVR5cGVzOiB0cnVlIH0pXG4gICAgLmZpbHRlcigoZW50cnkpID0+IGVudHJ5LmlzRmlsZSgpICYmICFlbnRyeS5uYW1lLnN0YXJ0c1dpdGgoXCIuXCIpKVxuICAgIC5maWx0ZXIoKGVudHJ5KSA9PiBpc0ltYWdlRmlsZShlbnRyeS5uYW1lKSlcbiAgICAubWFwKChlbnRyeSkgPT4ge1xuICAgICAgY29uc3QgcmVsYXRpdmVQYXRoID0gYGJhY2tncm91bmRzLyR7ZW50cnkubmFtZX1gO1xuICAgICAgY29uc3QgcHJldmlld1BhdGggPSBqb2luKFxuICAgICAgICBiZ0RpcixcbiAgICAgICAgXCIucmF5Y2FzdC1ibHVyXCIsXG4gICAgICAgIGAke2Jhc2VuYW1lKGVudHJ5Lm5hbWUsIGV4dG5hbWUoZW50cnkubmFtZSkpfS5qcGdgLFxuICAgICAgKTtcbiAgICAgIGNvbnN0IHBhdGggPSBqb2luKGJnRGlyLCBlbnRyeS5uYW1lKTtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIHRoZW1lTmFtZTogdGhlbWUubmFtZSxcbiAgICAgICAgcmVsYXRpdmVQYXRoLFxuICAgICAgICBwYXRoLFxuICAgICAgICBwcmV2aWV3UGF0aDogZXhpc3RzU3luYyhwcmV2aWV3UGF0aCkgPyBwcmV2aWV3UGF0aCA6IHBhdGgsXG4gICAgICAgIGRpc3BsYXlOYW1lOiBwcmV0dGlmeUJhY2tncm91bmROYW1lKGVudHJ5Lm5hbWUpLFxuICAgICAgfTtcbiAgICB9KVxuICAgIC5zb3J0KChhLCBiKSA9PiBhLnJlbGF0aXZlUGF0aC5sb2NhbGVDb21wYXJlKGIucmVsYXRpdmVQYXRoKSk7XG59XG5cbmZ1bmN0aW9uIGdldFNlbGVjdGVkQmFja2dyb3VuZCh0aGVtZTogVGhlbWVFbnRyeSk6IEJhY2tncm91bmRFbnRyeSB8IHVuZGVmaW5lZCB7XG4gIGNvbnN0IGJhY2tncm91bmRzID0gZ2V0VGhlbWVCYWNrZ3JvdW5kcyh0aGVtZSk7XG4gIGNvbnN0IHN0YXRlID0gZ2V0QmFja2dyb3VuZFN0YXRlKCk7XG4gIGNvbnN0IHNlbGVjdGVkUmVsYXRpdmUgPSBzdGF0ZVt0aGVtZS5uYW1lXSB8fCB0aGVtZS5lbnYuV0FMTFBBUEVSO1xuICByZXR1cm4gKFxuICAgIGJhY2tncm91bmRzLmZpbmQoXG4gICAgICAoYmFja2dyb3VuZCkgPT4gYmFja2dyb3VuZC5yZWxhdGl2ZVBhdGggPT09IHNlbGVjdGVkUmVsYXRpdmUsXG4gICAgKSB8fCBiYWNrZ3JvdW5kc1swXVxuICApO1xufVxuXG5mdW5jdGlvbiBnZXRTa2V0Y2h5YmFyQWNjZW50KGRpcjogc3RyaW5nLCBlbnY6IFRoZW1lRW52KTogc3RyaW5nIHtcbiAgY29uc3QgcGFsZXR0ZSA9IGdldEdob3N0dHlQYWxldHRlKGRpcik7XG4gIHJldHVybiBwYWxldHRlWzRdIHx8IGdldEJvcmRlckhleChlbnYpO1xufVxuXG5mdW5jdGlvbiB0b01hY29zQ29sb3IoaGV4OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gL14jWzAtOWEtZkEtRl17Nn0kLy50ZXN0KGhleCkgPyBgMHhmZiR7aGV4LnNsaWNlKDEpfWAgOiBcIjB4ZmZmZmZmZmZcIjtcbn1cblxuZnVuY3Rpb24gZ2V0UmF5Y2FzdFRoZW1lSWQodGhlbWU6IFRoZW1lRW50cnkpOiBzdHJpbmcge1xuICByZXR1cm4gdGhlbWUuaXNEYXJrID8gXCJidW5kbGVkLXJheWNhc3QtZGFya1wiIDogXCJidW5kbGVkLXJheWNhc3QtbGlnaHRcIjtcbn1cblxuZnVuY3Rpb24gZ2V0VGhlbWVzKCk6IFRoZW1lRW50cnlbXSB7XG4gIGlmICghZXhpc3RzU3luYyhUSEVNRVNfRElSKSkgcmV0dXJuIFtdO1xuICByZXR1cm4gcmVhZGRpclN5bmMoVEhFTUVTX0RJUiwgeyB3aXRoRmlsZVR5cGVzOiB0cnVlIH0pXG4gICAgLmZpbHRlcigoZCkgPT4gZC5pc0RpcmVjdG9yeSgpKVxuICAgIC5tYXAoKGQpID0+IHtcbiAgICAgIGNvbnN0IGRpciA9IGpvaW4oVEhFTUVTX0RJUiwgZC5uYW1lKTtcbiAgICAgIGNvbnN0IGVudiA9IHBhcnNlVGhlbWVFbnYoZGlyKTtcbiAgICAgIGNvbnN0IGJnRGlyID0gam9pbihkaXIsIFwiYmFja2dyb3VuZHNcIik7XG4gICAgICBjb25zdCBiZ0NvdW50ID0gZXhpc3RzU3luYyhiZ0RpcilcbiAgICAgICAgPyByZWFkZGlyU3luYyhiZ0RpciwgeyB3aXRoRmlsZVR5cGVzOiB0cnVlIH0pLmZpbHRlcihcbiAgICAgICAgICAgIChlbnRyeSkgPT4gZW50cnkuaXNGaWxlKCkgJiYgaXNJbWFnZUZpbGUoZW50cnkubmFtZSksXG4gICAgICAgICAgKS5sZW5ndGhcbiAgICAgICAgOiAwO1xuICAgICAgY29uc3Qgc2VsZWN0ZWRXYWxscGFwZXIgPVxuICAgICAgICBnZXRCYWNrZ3JvdW5kU3RhdGUoKVtkLm5hbWVdIHx8IGVudi5XQUxMUEFQRVIgfHwgXCJcIjtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIG5hbWU6IGQubmFtZSxcbiAgICAgICAgZGlzcGxheU5hbWU6IGQubmFtZVxuICAgICAgICAgIC5zcGxpdChcIi1cIilcbiAgICAgICAgICAubWFwKCh3KSA9PiB3LmNoYXJBdCgwKS50b1VwcGVyQ2FzZSgpICsgdy5zbGljZSgxKSlcbiAgICAgICAgICAuam9pbihcIiBcIiksXG4gICAgICAgIGVudixcbiAgICAgICAgaGFzV2FsbHBhcGVyOiBleGlzdHNTeW5jKGpvaW4oZGlyLCBzZWxlY3RlZFdhbGxwYXBlcikpLFxuICAgICAgICB3YWxscGFwZXJQYXRoOiBleGlzdHNTeW5jKGpvaW4oZGlyLCBzZWxlY3RlZFdhbGxwYXBlcikpXG4gICAgICAgICAgPyBqb2luKGRpciwgc2VsZWN0ZWRXYWxscGFwZXIpXG4gICAgICAgICAgOiB1bmRlZmluZWQsXG4gICAgICAgIGFjY2VudENvbG9yOiBnZXRUaGVtZUFjY2VudChkaXIsIGVudiksXG4gICAgICAgIHNrZXRjaHliYXJBY2NlbnRDb2xvcjogZ2V0U2tldGNoeWJhckFjY2VudChkaXIsIGVudiksXG4gICAgICAgIHRleHRDb2xvcjogZ2V0R2hvc3R0eUNvbG9yKFxuICAgICAgICAgIGRpcixcbiAgICAgICAgICBcImZvcmVncm91bmRcIixcbiAgICAgICAgICBlbnYuREFSS19NT0RFID09PSBcInRydWVcIiA/IFwiI2ZmZmZmZlwiIDogXCIjMDAwMDAwXCIsXG4gICAgICAgICksXG4gICAgICAgIHBhbGV0dGU6IGdldEdob3N0dHlQYWxldHRlKGRpciksXG4gICAgICAgIGlzRGFyazogZW52LkRBUktfTU9ERSA9PT0gXCJ0cnVlXCIsXG4gICAgICAgIGJnQ291bnQsXG4gICAgICAgIGFwcGxlQWNjZW50TmFtZTogZ2V0QXBwbGVBY2NlbnROYW1lKGRpciwgZW52KSxcbiAgICAgICAgaGlnaGxpZ2h0Q29sb3I6IGdldEhpZ2hsaWdodENvbG9yKGRpciwgZW52KSxcbiAgICAgIH07XG4gICAgfSlcbiAgICAuc29ydCgoYSwgYikgPT4gYS5uYW1lLmxvY2FsZUNvbXBhcmUoYi5uYW1lKSk7XG59XG5cbmZ1bmN0aW9uIGdldEN1cnJlbnRUaGVtZSgpOiBzdHJpbmcge1xuICBjb25zdCBwYXRoID0gam9pbihUSEVNRVNfRElSLCBcIi5jdXJyZW50XCIpO1xuICBpZiAoIWV4aXN0c1N5bmMocGF0aCkpIHJldHVybiBcIlwiO1xuICByZXR1cm4gcmVhZEZpbGVTeW5jKHBhdGgsIFwidXRmLThcIikudHJpbSgpO1xufVxuXG5mdW5jdGlvbiBtYXBDb2RleFRoZW1lKHRoZW1lOiBUaGVtZUVudHJ5KTogc3RyaW5nIHtcbiAgY29uc3QgbmFtZSA9IHRoZW1lLm5hbWUudG9Mb3dlckNhc2UoKTtcbiAgY29uc3QgZGlyZWN0TWFwOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge1xuICAgIGNhdHBwdWNjaW46IFwiY2F0cHB1Y2Npbi1tb2NoYVwiLFxuICAgIFwiY2F0cHB1Y2Npbi1sYXR0ZVwiOiBcImNhdHBwdWNjaW4tbGF0dGVcIixcbiAgICBncnV2Ym94OiBcImdydXZib3gtZGFya1wiLFxuICAgIG5vcmQ6IFwibm9yZFwiLFxuICAgIGV2ZXJmb3Jlc3Q6IFwiemVuYnVyblwiLFxuICB9O1xuICBpZiAoZGlyZWN0TWFwW25hbWVdKSByZXR1cm4gZGlyZWN0TWFwW25hbWVdO1xuICByZXR1cm4gdGhlbWUuaXNEYXJrID8gXCJvbmUtaGFsZi1kYXJrXCIgOiBcIm9uZS1oYWxmLWxpZ2h0XCI7XG59XG5cbmZ1bmN0aW9uIHNldENvZGV4VGhlbWUodGhlbWU6IFRoZW1lRW50cnkpIHtcbiAgaWYgKCFleGlzdHNTeW5jKENPREVYX0NPTkZJRykpIHJldHVybjtcbiAgY29uc3QgY29kZXhUaGVtZSA9IG1hcENvZGV4VGhlbWUodGhlbWUpO1xuICBjb25zdCBvcmlnaW5hbCA9IHJlYWRGaWxlU3luYyhDT0RFWF9DT05GSUcsIFwidXRmLThcIik7XG4gIGNvbnN0IGxpbmVzID0gb3JpZ2luYWwuc3BsaXQoXCJcXG5cIik7XG5cbiAgbGV0IGluVHVpID0gZmFsc2U7XG4gIGxldCB0dWlIZWFkZXJJbmRleCA9IC0xO1xuICBsZXQgdGhlbWVTZXQgPSBmYWxzZTtcblxuICBmb3IgKGxldCBpID0gMDsgaSA8IGxpbmVzLmxlbmd0aDsgaSsrKSB7XG4gICAgY29uc3QgbGluZSA9IGxpbmVzW2ldO1xuICAgIGlmICgvXlxcWy4qXFxdJC8udGVzdChsaW5lLnRyaW0oKSkpIHtcbiAgICAgIGluVHVpID0gbGluZS50cmltKCkgPT09IFwiW3R1aV1cIjtcbiAgICAgIGlmIChpblR1aSkgdHVpSGVhZGVySW5kZXggPSBpO1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGlmIChpblR1aSAmJiAvXlxccyp0aGVtZVxccyo9Ly50ZXN0KGxpbmUpKSB7XG4gICAgICBsaW5lc1tpXSA9IGB0aGVtZSA9IFwiJHtjb2RleFRoZW1lfVwiYDtcbiAgICAgIHRoZW1lU2V0ID0gdHJ1ZTtcbiAgICAgIGJyZWFrO1xuICAgIH1cbiAgfVxuXG4gIGlmICghdGhlbWVTZXQgJiYgdHVpSGVhZGVySW5kZXggIT09IC0xKSB7XG4gICAgbGluZXMuc3BsaWNlKHR1aUhlYWRlckluZGV4ICsgMSwgMCwgYHRoZW1lID0gXCIke2NvZGV4VGhlbWV9XCJgKTtcbiAgfSBlbHNlIGlmICghdGhlbWVTZXQgJiYgdHVpSGVhZGVySW5kZXggPT09IC0xKSB7XG4gICAgaWYgKGxpbmVzLmxlbmd0aCA+IDAgJiYgbGluZXNbbGluZXMubGVuZ3RoIC0gMV0udHJpbSgpICE9PSBcIlwiKVxuICAgICAgbGluZXMucHVzaChcIlwiKTtcbiAgICBsaW5lcy5wdXNoKFwiW3R1aV1cIik7XG4gICAgbGluZXMucHVzaChgdGhlbWUgPSBcIiR7Y29kZXhUaGVtZX1cImApO1xuICB9XG5cbiAgd3JpdGVGaWxlU3luYyhDT0RFWF9DT05GSUcsIGxpbmVzLmpvaW4oXCJcXG5cIikucmVwbGFjZSgvXFxuKiQvLCBcIlxcblwiKSk7XG59XG5cbmZ1bmN0aW9uIHNwYXduVGhlbWVBcHBseVdvcmtlcih0aGVtZTogVGhlbWVFbnRyeSkge1xuICBjb25zdCB3b3JrZXJTb3VyY2UgPSBTdHJpbmcucmF3YFxuY29uc3QgeyBleGlzdHNTeW5jIH0gPSByZXF1aXJlKFwiZnNcIik7XG5jb25zdCB7IGhvbWVkaXIgfSA9IHJlcXVpcmUoXCJvc1wiKTtcbmNvbnN0IHsgam9pbiB9ID0gcmVxdWlyZShcInBhdGhcIik7XG5jb25zdCB7IHNwYXduIH0gPSByZXF1aXJlKFwiY2hpbGRfcHJvY2Vzc1wiKTtcblxuY29uc3QgdGhlbWUgPSBKU09OLnBhcnNlKHByb2Nlc3MuYXJndlsxXSk7XG5jb25zdCBUSEVNRV9TV0lUQ0hfQklOID0gam9pbihob21lZGlyKCksIFwiLmxvY2FsXCIsIFwiYmluXCIsIFwidGhlbWUtc3dpdGNoXCIpO1xuY29uc3QgUEFUSCA9ICR7SlNPTi5zdHJpbmdpZnkoUEFUSCl9O1xuXG5mdW5jdGlvbiBydW5UaGVtZVN3aXRjaChhcmdzKSB7XG4gIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgY29uc3QgY2hpbGQgPSBzcGF3bihUSEVNRV9TV0lUQ0hfQklOLCBhcmdzLCB7XG4gICAgICBlbnY6IHsgLi4ucHJvY2Vzcy5lbnYsIEhPTUU6IGhvbWVkaXIoKSwgUEFUSCB9LFxuICAgICAgc3RkaW86IFwiaWdub3JlXCIsXG4gICAgfSk7XG4gICAgY2hpbGQub24oXCJlcnJvclwiLCByZWplY3QpO1xuICAgIGNoaWxkLm9uKFwiZXhpdFwiLCAoY29kZSkgPT4ge1xuICAgICAgaWYgKGNvZGUgPT09IDApIHJlc29sdmUoKTtcbiAgICAgIGVsc2UgcmVqZWN0KG5ldyBFcnJvcihcInRoZW1lLXN3aXRjaCBleGl0ZWQgXCIgKyBjb2RlKSk7XG4gICAgfSk7XG4gIH0pO1xufVxuXG5hc3luYyBmdW5jdGlvbiBtYWluKCkge1xuICBjb25zdCB7IG5hbWUgfSA9IHRoZW1lO1xuICBpZiAoIWV4aXN0c1N5bmMoVEhFTUVfU1dJVENIX0JJTikpIHByb2Nlc3MuZXhpdCgxKTtcbiAgLy8gUnVuIHN5bmNocm9ub3VzbHk6IHRoZW1lLXN3aXRjaCB3cml0ZXMgfi8udGhlbWVzLy5jdXJyZW50IG9ubHkgYWZ0ZXIgR2hvc3R0eS9SYXljYXN0L2V0Yy4gZmluaXNoLlxuICAvLyAoQmFja2dyb3VuZGluZyB3aXRoICYgY2F1c2VkIHRoZSBVSSB0byBzaG93IFwiYWN0aXZlXCIgYmVmb3JlIGVtdWxhdG9ycyBhY3R1YWxseSByZWxvYWRlZC4pXG4gIGF3YWl0IHJ1blRoZW1lU3dpdGNoKFtuYW1lXSk7XG59XG5cbm1haW4oKS5jYXRjaCgoKSA9PiBwcm9jZXNzLmV4aXQoMSkpO1xuYDtcblxuICBjb25zdCB3b3JrZXIgPSBzcGF3bihcbiAgICBwcm9jZXNzLmV4ZWNQYXRoLFxuICAgIFtcIi1lXCIsIHdvcmtlclNvdXJjZSwgSlNPTi5zdHJpbmdpZnkodGhlbWUpXSxcbiAgICB7XG4gICAgICBkZXRhY2hlZDogdHJ1ZSxcbiAgICAgIHN0ZGlvOiBcImlnbm9yZVwiLFxuICAgIH0sXG4gICk7XG4gIHdvcmtlci51bnJlZigpO1xufVxuXG5mdW5jdGlvbiBzcGF3bkJhY2tncm91bmRBcHBseVdvcmtlcihcbiAgdGhlbWU6IFRoZW1lRW50cnksXG4gIGJhY2tncm91bmQ6IEJhY2tncm91bmRFbnRyeSxcbikge1xuICBjb25zdCB3b3JrZXJTb3VyY2UgPSBTdHJpbmcucmF3YFxuY29uc3QgeyBob21lZGlyIH0gPSByZXF1aXJlKFwib3NcIik7XG5jb25zdCB7IGpvaW4gfSA9IHJlcXVpcmUoXCJwYXRoXCIpO1xuY29uc3QgeyBzcGF3biB9ID0gcmVxdWlyZShcImNoaWxkX3Byb2Nlc3NcIik7XG5cbmNvbnN0IHRoZW1lTmFtZSA9IHByb2Nlc3MuYXJndlsxXTtcbmNvbnN0IGJhY2tncm91bmRQYXRoID0gcHJvY2Vzcy5hcmd2WzJdO1xuY29uc3QgVEhFTUVfU1dJVENIX0JJTiA9IGpvaW4oaG9tZWRpcigpLCBcIi5sb2NhbFwiLCBcImJpblwiLCBcInRoZW1lLXN3aXRjaFwiKTtcbmNvbnN0IFBBVEggPSAke0pTT04uc3RyaW5naWZ5KFBBVEgpfTtcblxuY29uc3QgY2hpbGQgPSBzcGF3bihUSEVNRV9TV0lUQ0hfQklOLCBbXCItLWJhY2tncm91bmRcIiwgdGhlbWVOYW1lLCBiYWNrZ3JvdW5kUGF0aF0sIHtcbiAgZGV0YWNoZWQ6IHRydWUsXG4gIGVudjogeyAuLi5wcm9jZXNzLmVudiwgSE9NRTogaG9tZWRpcigpLCBQQVRIIH0sXG4gIHN0ZGlvOiBcImlnbm9yZVwiLFxufSk7XG5jaGlsZC51bnJlZigpO1xuYDtcblxuICBjb25zdCB3b3JrZXIgPSBzcGF3bihcbiAgICBwcm9jZXNzLmV4ZWNQYXRoLFxuICAgIFtcIi1lXCIsIHdvcmtlclNvdXJjZSwgdGhlbWUubmFtZSwgYmFja2dyb3VuZC5yZWxhdGl2ZVBhdGhdLFxuICAgIHtcbiAgICAgIGRldGFjaGVkOiB0cnVlLFxuICAgICAgc3RkaW86IFwiaWdub3JlXCIsXG4gICAgfSxcbiAgKTtcbiAgd29ya2VyLnVucmVmKCk7XG59XG5cbmZ1bmN0aW9uIEJhY2tncm91bmRQaWNrZXIocHJvcHM6IHtcbiAgdGhlbWU6IFRoZW1lRW50cnk7XG4gIG9uU2VsZWN0OiAodGhlbWVOYW1lOiBzdHJpbmcsIHJlbGF0aXZlUGF0aDogc3RyaW5nKSA9PiB2b2lkO1xufSkge1xuICBjb25zdCBhbGxCYWNrZ3JvdW5kcyA9IGdldFRoZW1lQmFja2dyb3VuZHMocHJvcHMudGhlbWUpO1xuICBjb25zdCBzZWxlY3RlZCA9IGdldFNlbGVjdGVkQmFja2dyb3VuZChwcm9wcy50aGVtZSk7XG5cbiAgY29uc3QgYmFja2dyb3VuZHMgPSBzZWxlY3RlZFxuICAgID8gW1xuICAgICAgICBzZWxlY3RlZCxcbiAgICAgICAgLi4uYWxsQmFja2dyb3VuZHMuZmlsdGVyKFxuICAgICAgICAgIChiZykgPT4gYmcucmVsYXRpdmVQYXRoICE9PSBzZWxlY3RlZC5yZWxhdGl2ZVBhdGgsXG4gICAgICAgICksXG4gICAgICBdXG4gICAgOiBhbGxCYWNrZ3JvdW5kcztcblxuICByZXR1cm4gKFxuICAgIDxHcmlkXG4gICAgICBjb2x1bW5zPXsyfVxuICAgICAgaW5zZXQ9e0dyaWQuSW5zZXQuU21hbGx9XG4gICAgICBmaXQ9e0dyaWQuRml0LkZpbGx9XG4gICAgICBhc3BlY3RSYXRpbz1cIjE2LzlcIlxuICAgICAgbmF2aWdhdGlvblRpdGxlPXtgU3dpdGNoIEJhY2tncm91bmQgKCR7cHJvcHMudGhlbWUuZGlzcGxheU5hbWV9KWB9XG4gICAgICBzZWFyY2hCYXJQbGFjZWhvbGRlcj1cIlNlYXJjaCBiYWNrZ3JvdW5kcy4uLlwiXG4gICAgPlxuICAgICAge2JhY2tncm91bmRzLm1hcCgoYmFja2dyb3VuZCkgPT4ge1xuICAgICAgICBjb25zdCBpc1NlbGVjdGVkID0gYmFja2dyb3VuZC5yZWxhdGl2ZVBhdGggPT09IHNlbGVjdGVkPy5yZWxhdGl2ZVBhdGg7XG4gICAgICAgIHJldHVybiAoXG4gICAgICAgICAgPEdyaWQuSXRlbVxuICAgICAgICAgICAga2V5PXtiYWNrZ3JvdW5kLnJlbGF0aXZlUGF0aH1cbiAgICAgICAgICAgIHRpdGxlPXtiYWNrZ3JvdW5kLmRpc3BsYXlOYW1lfVxuICAgICAgICAgICAgYWNjZXNzb3J5PXtcbiAgICAgICAgICAgICAgaXNTZWxlY3RlZFxuICAgICAgICAgICAgICAgID8geyBpY29uOiBJY29uLkNoZWNrQ2lyY2xlLCB0b29sdGlwOiBcIkN1cnJlbnRcIiB9XG4gICAgICAgICAgICAgICAgOiB1bmRlZmluZWRcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnRlbnQ9e3sgc291cmNlOiBiYWNrZ3JvdW5kLnBhdGggfX1cbiAgICAgICAgICAgIGFjdGlvbnM9e1xuICAgICAgICAgICAgICA8QWN0aW9uUGFuZWw+XG4gICAgICAgICAgICAgICAgPEFjdGlvblxuICAgICAgICAgICAgICAgICAgdGl0bGU9XCJVc2UgQmFja2dyb3VuZFwiXG4gICAgICAgICAgICAgICAgICBpY29uPXtJY29uLkltYWdlfVxuICAgICAgICAgICAgICAgICAgb25BY3Rpb249e2FzeW5jICgpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgY29uc3Qgc3RhdGUgPSBnZXRCYWNrZ3JvdW5kU3RhdGUoKTtcbiAgICAgICAgICAgICAgICAgICAgc3RhdGVbcHJvcHMudGhlbWUubmFtZV0gPSBiYWNrZ3JvdW5kLnJlbGF0aXZlUGF0aDtcbiAgICAgICAgICAgICAgICAgICAgd3JpdGVCYWNrZ3JvdW5kU3RhdGUoc3RhdGUpO1xuICAgICAgICAgICAgICAgICAgICBwcm9wcy5vblNlbGVjdChwcm9wcy50aGVtZS5uYW1lLCBiYWNrZ3JvdW5kLnJlbGF0aXZlUGF0aCk7XG4gICAgICAgICAgICAgICAgICAgIHNwYXduQmFja2dyb3VuZEFwcGx5V29ya2VyKHByb3BzLnRoZW1lLCBiYWNrZ3JvdW5kKTtcbiAgICAgICAgICAgICAgICAgICAgYXdhaXQgY2xvc2VNYWluV2luZG93KCk7XG4gICAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgIDwvQWN0aW9uUGFuZWw+XG4gICAgICAgICAgICB9XG4gICAgICAgICAgLz5cbiAgICAgICAgKTtcbiAgICAgIH0pfVxuICAgIDwvR3JpZD5cbiAgKTtcbn1cblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gQ29tbWFuZCgpIHtcbiAgY29uc3QgdGhlbWVzID0gZ2V0VGhlbWVzKCk7XG4gIGNvbnN0IFtjdXJyZW50LCBzZXRDdXJyZW50XSA9IHVzZVN0YXRlKCgpID0+IGdldEN1cnJlbnRUaGVtZSgpKTtcbiAgY29uc3QgW3BlbmRpbmdUaGVtZSwgc2V0UGVuZGluZ1RoZW1lXSA9IHVzZVN0YXRlPHN0cmluZyB8IG51bGw+KG51bGwpO1xuICBjb25zdCBbYmFja2dyb3VuZFZlcnNpb24sIHNldEJhY2tncm91bmRWZXJzaW9uXSA9IHVzZVN0YXRlKDApO1xuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgY29uc3Qgc3luY0N1cnJlbnRUaGVtZSA9ICgpID0+IHtcbiAgICAgIGNvbnN0IG5leHQgPSBnZXRDdXJyZW50VGhlbWUoKTtcbiAgICAgIHNldEN1cnJlbnQoKHByZXYpID0+IChwcmV2ID09PSBuZXh0ID8gcHJldiA6IG5leHQpKTtcbiAgICB9O1xuXG4gICAgc3luY0N1cnJlbnRUaGVtZSgpO1xuICAgIGNvbnN0IGludGVydmFsID0gc2V0SW50ZXJ2YWwoc3luY0N1cnJlbnRUaGVtZSwgNzUwKTtcbiAgICByZXR1cm4gKCkgPT4gY2xlYXJJbnRlcnZhbChpbnRlcnZhbCk7XG4gIH0sIFtdKTtcblxuICBmdW5jdGlvbiBnZXRUaGVtZVByZXZpZXcodGhlbWU6IFRoZW1lRW50cnkpOiBzdHJpbmcge1xuICAgIGNvbnN0IHNlbGVjdGVkQmFja2dyb3VuZCA9IGdldFNlbGVjdGVkQmFja2dyb3VuZCh0aGVtZSk7XG4gICAgY29uc3Qgd2FsbHBhcGVyUHJldmlldyA9IHNlbGVjdGVkQmFja2dyb3VuZFxuICAgICAgPyBgIVske3RoZW1lLmRpc3BsYXlOYW1lfV0oJHtlbmNvZGVVUkkoYGZpbGU6Ly8ke3NlbGVjdGVkQmFja2dyb3VuZC5wcmV2aWV3UGF0aH1gKX0/cmF5Y2FzdC13aWR0aD03MjAmcmF5Y2FzdC1oZWlnaHQ9NDA1KWBcbiAgICAgIDogXCJfTm8gd2FsbHBhcGVyIHByZXZpZXcgYXZhaWxhYmxlX1wiO1xuICAgIGNvbnN0IGxpbmVzID0gW1xuICAgICAgYCMgJHt0aGVtZS5kaXNwbGF5TmFtZX1gLFxuICAgICAgXCJcIixcbiAgICAgIHdhbGxwYXBlclByZXZpZXcsXG4gICAgICBcIlwiLFxuICAgICAgc2VsZWN0ZWRCYWNrZ3JvdW5kXG4gICAgICAgID8gYEJhY2tncm91bmQgXFxgJHtzZWxlY3RlZEJhY2tncm91bmQuZGlzcGxheU5hbWV9XFxgYFxuICAgICAgICA6IFwiXCIsXG4gICAgICBgQWNjZW50IFxcYCR7dGhlbWUuYWNjZW50Q29sb3J9XFxgYCxcbiAgICAgIGBtYWNPUyBBY2NlbnQgXFxgJHt0aGVtZS5hcHBsZUFjY2VudE5hbWV9XFxgYCxcbiAgICAgIGBIaWdobGlnaHQgXFxgJHt0aGVtZS5oaWdobGlnaHRDb2xvcn1cXGBgLFxuICAgICAgYFRleHQgXFxgJHt0aGVtZS50ZXh0Q29sb3J9XFxgYCxcbiAgICBdO1xuXG4gICAgcmV0dXJuIGxpbmVzLmpvaW4oXCJcXG5cIik7XG4gIH1cblxuICBmdW5jdGlvbiBnZXRMc1ByZXZpZXdDb2xvcnModGhlbWU6IFRoZW1lRW50cnkpIHtcbiAgICByZXR1cm4ge1xuICAgICAgZGlyZWN0b3J5OiB0aGVtZS5wYWxldHRlWzRdIHx8IHRoZW1lLmFjY2VudENvbG9yLFxuICAgICAgc3ltbGluazogdGhlbWUucGFsZXR0ZVs2XSB8fCB0aGVtZS5hY2NlbnRDb2xvcixcbiAgICAgIGV4ZWN1dGFibGU6IHRoZW1lLnBhbGV0dGVbMl0gfHwgdGhlbWUuYWNjZW50Q29sb3IsXG4gICAgICBhcmNoaXZlOiB0aGVtZS5wYWxldHRlWzFdIHx8IHRoZW1lLmFjY2VudENvbG9yLFxuICAgICAgZGV2aWNlOiB0aGVtZS5wYWxldHRlWzNdIHx8IHRoZW1lLmFjY2VudENvbG9yLFxuICAgICAgbm9ybWFsOiB0aGVtZS50ZXh0Q29sb3IsXG4gICAgfTtcbiAgfVxuXG4gIGNvbnN0IGFjdGl2ZVRoZW1lID1cbiAgICB0aGVtZXMuZmluZCgodGhlbWUpID0+IHRoZW1lLm5hbWUgPT09IGN1cnJlbnQpIHx8IHRoZW1lc1swXTtcbiAgY29uc3QgYWN0aXZlQmFja2dyb3VuZCA9IGFjdGl2ZVRoZW1lXG4gICAgPyBnZXRTZWxlY3RlZEJhY2tncm91bmQoYWN0aXZlVGhlbWUpXG4gICAgOiB1bmRlZmluZWQ7XG5cbiAgZnVuY3Rpb24gZ2V0QmFja2dyb3VuZFByZXZpZXdNYXJrZG93bih0aGVtZTogVGhlbWVFbnRyeSk6IHN0cmluZyB7XG4gICAgY29uc3Qgc2VsZWN0ZWQgPSBnZXRTZWxlY3RlZEJhY2tncm91bmQodGhlbWUpO1xuICAgIGlmICghc2VsZWN0ZWQpIHJldHVybiBcIl9ObyBiYWNrZ3JvdW5kIG9wdGlvbnMgYXZhaWxhYmxlX1wiO1xuICAgIHJldHVybiBgIVske3NlbGVjdGVkLmRpc3BsYXlOYW1lfV0oJHtlbmNvZGVVUkkoYGZpbGU6Ly8ke3NlbGVjdGVkLnByZXZpZXdQYXRofWApfT9yYXljYXN0LXdpZHRoPTcyMCZyYXljYXN0LWhlaWdodD00MDUpYDtcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPExpc3QgaXNTaG93aW5nRGV0YWlsIHNlYXJjaEJhclBsYWNlaG9sZGVyPVwiU2VhcmNoIHRoZW1lcy4uLlwiPlxuICAgICAge2FjdGl2ZVRoZW1lID8gKFxuICAgICAgICA8TGlzdC5JdGVtXG4gICAgICAgICAga2V5PXtgc3dpdGNoLWJhY2tncm91bmQtJHtiYWNrZ3JvdW5kVmVyc2lvbn1gfVxuICAgICAgICAgIHRpdGxlPVwiU3dpdGNoIEJhY2tncm91bmRcIlxuICAgICAgICAgIHN1YnRpdGxlPXthY3RpdmVUaGVtZS5kaXNwbGF5TmFtZX1cbiAgICAgICAgICBpY29uPXtJY29uLkFwcFdpbmRvd0dyaWQyeDJ9XG4gICAgICAgICAgYWNjZXNzb3JpZXM9e1t7IHRleHQ6IGAke2FjdGl2ZVRoZW1lLmJnQ291bnR9IG9wdGlvbnNgIH1dfVxuICAgICAgICAgIGRldGFpbD17XG4gICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbFxuICAgICAgICAgICAgICBtYXJrZG93bj17Z2V0QmFja2dyb3VuZFByZXZpZXdNYXJrZG93bihhY3RpdmVUaGVtZSl9XG4gICAgICAgICAgICAvPlxuICAgICAgICAgIH1cbiAgICAgICAgICBhY3Rpb25zPXtcbiAgICAgICAgICAgIDxBY3Rpb25QYW5lbD5cbiAgICAgICAgICAgICAgPEFjdGlvbi5QdXNoXG4gICAgICAgICAgICAgICAgdGl0bGU9XCJPcGVuIEJhY2tncm91bmRzXCJcbiAgICAgICAgICAgICAgICBpY29uPXtJY29uLkZvbGRlcn1cbiAgICAgICAgICAgICAgICB0YXJnZXQ9e1xuICAgICAgICAgICAgICAgICAgPEJhY2tncm91bmRQaWNrZXJcbiAgICAgICAgICAgICAgICAgICAgdGhlbWU9e2FjdGl2ZVRoZW1lfVxuICAgICAgICAgICAgICAgICAgICBvblNlbGVjdD17KCkgPT5cbiAgICAgICAgICAgICAgICAgICAgICBzZXRCYWNrZ3JvdW5kVmVyc2lvbigodmVyc2lvbikgPT4gdmVyc2lvbiArIDEpXG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAvPlxuICAgICAgICAgICAgPC9BY3Rpb25QYW5lbD5cbiAgICAgICAgICB9XG4gICAgICAgIC8+XG4gICAgICApIDogbnVsbH1cbiAgICAgIHt0aGVtZXMubWFwKCh0aGVtZSkgPT4ge1xuICAgICAgICBjb25zdCBpc0N1cnJlbnQgPSB0aGVtZS5uYW1lID09PSAocGVuZGluZ1RoZW1lIHx8IGN1cnJlbnQpO1xuICAgICAgICBjb25zdCBhY2Nlc3NvcmllczogTGlzdC5JdGVtLkFjY2Vzc29yeVtdID0gW107XG4gICAgICAgIGlmIChpc0N1cnJlbnQpXG4gICAgICAgICAgYWNjZXNzb3JpZXMucHVzaCh7IHRhZzogeyB2YWx1ZTogXCJhY3RpdmVcIiwgY29sb3I6IENvbG9yLkdyZWVuIH0gfSk7XG4gICAgICAgIGFjY2Vzc29yaWVzLnB1c2goeyBpY29uOiB0aGVtZS5pc0RhcmsgPyBJY29uLk1vb24gOiBJY29uLlN1biB9KTtcbiAgICAgICAgaWYgKHRoZW1lLmJnQ291bnQgPiAwKVxuICAgICAgICAgIGFjY2Vzc29yaWVzLnB1c2goeyB0ZXh0OiBgJHt0aGVtZS5iZ0NvdW50fSBiZ2AgfSk7XG5cbiAgICAgICAgcmV0dXJuIChcbiAgICAgICAgICA8TGlzdC5JdGVtXG4gICAgICAgICAgICBrZXk9e3RoZW1lLm5hbWV9XG4gICAgICAgICAgICB0aXRsZT17dGhlbWUuZGlzcGxheU5hbWV9XG4gICAgICAgICAgICBpY29uPXt7XG4gICAgICAgICAgICAgIHNvdXJjZTogSWNvbi5DaXJjbGUsXG4gICAgICAgICAgICAgIHRpbnRDb2xvcjogdGhlbWUuYWNjZW50Q29sb3IgYXMgQ29sb3IsXG4gICAgICAgICAgICB9fVxuICAgICAgICAgICAgYWNjZXNzb3JpZXM9e2FjY2Vzc29yaWVzfVxuICAgICAgICAgICAgZGV0YWlsPXtcbiAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWxcbiAgICAgICAgICAgICAgICBtYXJrZG93bj17Z2V0VGhlbWVQcmV2aWV3KHRoZW1lKX1cbiAgICAgICAgICAgICAgICBtZXRhZGF0YT17XG4gICAgICAgICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YT5cbiAgICAgICAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWwuTWV0YWRhdGEuTGFiZWxcbiAgICAgICAgICAgICAgICAgICAgICB0aXRsZT1cIk1vZGVcIlxuICAgICAgICAgICAgICAgICAgICAgIHRleHQ9e3RoZW1lLmlzRGFyayA/IFwiRGFya1wiIDogXCJMaWdodFwifVxuICAgICAgICAgICAgICAgICAgICAvPlxuICAgICAgICAgICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YS5TZXBhcmF0b3IgLz5cbiAgICAgICAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWwuTWV0YWRhdGEuTGFiZWxcbiAgICAgICAgICAgICAgICAgICAgICB0aXRsZT1cIldhbGxwYXBlcnNcIlxuICAgICAgICAgICAgICAgICAgICAgIHRleHQ9e1N0cmluZyhcbiAgICAgICAgICAgICAgICAgICAgICAgIHRoZW1lLmJnQ291bnQgfHwgKHRoZW1lLmhhc1dhbGxwYXBlciA/IDEgOiAwKSxcbiAgICAgICAgICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgICAgICAgICAvPlxuICAgICAgICAgICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YS5TZXBhcmF0b3IgLz5cbiAgICAgICAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWwuTWV0YWRhdGEuTGFiZWxcbiAgICAgICAgICAgICAgICAgICAgICB0aXRsZT1cIm1hY09TIEFjY2VudFwiXG4gICAgICAgICAgICAgICAgICAgICAgdGV4dD17dGhlbWUuYXBwbGVBY2NlbnROYW1lfVxuICAgICAgICAgICAgICAgICAgICAvPlxuICAgICAgICAgICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YS5MYWJlbFxuICAgICAgICAgICAgICAgICAgICAgIHRpdGxlPVwiSGlnaGxpZ2h0XCJcbiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PXt0aGVtZS5oaWdobGlnaHRDb2xvcn1cbiAgICAgICAgICAgICAgICAgICAgLz5cbiAgICAgICAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWwuTWV0YWRhdGEuU2VwYXJhdG9yIC8+XG4gICAgICAgICAgICAgICAgICAgIDxMaXN0Lkl0ZW0uRGV0YWlsLk1ldGFkYXRhLlRhZ0xpc3QgdGl0bGU9XCJscyAtbGFcIj5cbiAgICAgICAgICAgICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YS5UYWdMaXN0Lkl0ZW1cbiAgICAgICAgICAgICAgICAgICAgICAgIHRleHQ9XCJkaXIvXCJcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbG9yPXtnZXRMc1ByZXZpZXdDb2xvcnModGhlbWUpLmRpcmVjdG9yeX1cbiAgICAgICAgICAgICAgICAgICAgICAvPlxuICAgICAgICAgICAgICAgICAgICAgIDxMaXN0Lkl0ZW0uRGV0YWlsLk1ldGFkYXRhLlRhZ0xpc3QuSXRlbVxuICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD1cImxpbmtAXCJcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbG9yPXtnZXRMc1ByZXZpZXdDb2xvcnModGhlbWUpLnN5bWxpbmt9XG4gICAgICAgICAgICAgICAgICAgICAgLz5cbiAgICAgICAgICAgICAgICAgICAgICA8TGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YS5UYWdMaXN0Lkl0ZW1cbiAgICAgICAgICAgICAgICAgICAgICAgIHRleHQ9XCJleGVjKlwiXG4gICAgICAgICAgICAgICAgICAgICAgICBjb2xvcj17Z2V0THNQcmV2aWV3Q29sb3JzKHRoZW1lKS5leGVjdXRhYmxlfVxuICAgICAgICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWwuTWV0YWRhdGEuVGFnTGlzdC5JdGVtXG4gICAgICAgICAgICAgICAgICAgICAgICB0ZXh0PVwiYXJjaGl2ZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBjb2xvcj17Z2V0THNQcmV2aWV3Q29sb3JzKHRoZW1lKS5hcmNoaXZlfVxuICAgICAgICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgICAgICAgICAgPExpc3QuSXRlbS5EZXRhaWwuTWV0YWRhdGEuVGFnTGlzdC5JdGVtXG4gICAgICAgICAgICAgICAgICAgICAgICB0ZXh0PVwiZGV2aWNlXCJcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbG9yPXtnZXRMc1ByZXZpZXdDb2xvcnModGhlbWUpLmRldmljZX1cbiAgICAgICAgICAgICAgICAgICAgICAvPlxuICAgICAgICAgICAgICAgICAgICAgIDxMaXN0Lkl0ZW0uRGV0YWlsLk1ldGFkYXRhLlRhZ0xpc3QuSXRlbVxuICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD1cImZpbGVcIlxuICAgICAgICAgICAgICAgICAgICAgICAgY29sb3I9e2dldExzUHJldmlld0NvbG9ycyh0aGVtZSkubm9ybWFsfVxuICAgICAgICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgICAgICAgIDwvTGlzdC5JdGVtLkRldGFpbC5NZXRhZGF0YS5UYWdMaXN0PlxuICAgICAgICAgICAgICAgICAgPC9MaXN0Lkl0ZW0uRGV0YWlsLk1ldGFkYXRhPlxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgLz5cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGFjdGlvbnM9e1xuICAgICAgICAgICAgICA8QWN0aW9uUGFuZWw+XG4gICAgICAgICAgICAgICAgPEFjdGlvblxuICAgICAgICAgICAgICAgICAgdGl0bGU9XCJBcHBseSBUaGVtZVwiXG4gICAgICAgICAgICAgICAgICBpY29uPXtJY29uLkJydXNofVxuICAgICAgICAgICAgICAgICAgc2hvcnRjdXQ9e3sgbW9kaWZpZXJzOiBbXSwga2V5OiBcInJldHVyblwiIH19XG4gICAgICAgICAgICAgICAgICBvbkFjdGlvbj17YXN5bmMgKCkgPT4ge1xuICAgICAgICAgICAgICAgICAgICBzcGF3blRoZW1lQXBwbHlXb3JrZXIodGhlbWUpO1xuICAgICAgICAgICAgICAgICAgICBhd2FpdCBjbG9zZU1haW5XaW5kb3coKTtcbiAgICAgICAgICAgICAgICAgIH19XG4gICAgICAgICAgICAgICAgLz5cbiAgICAgICAgICAgICAgICB7dGhlbWUuYmdDb3VudCA+IDAgPyAoXG4gICAgICAgICAgICAgICAgICA8QWN0aW9uLlB1c2hcbiAgICAgICAgICAgICAgICAgICAgdGl0bGU9XCJTd2l0Y2ggQmFja2dyb3VuZFwiXG4gICAgICAgICAgICAgICAgICAgIGljb249e0ljb24uRm9sZGVyfVxuICAgICAgICAgICAgICAgICAgICBzaG9ydGN1dD17eyBtb2RpZmllcnM6IFtcImNtZFwiXSwga2V5OiBcImJcIiB9fVxuICAgICAgICAgICAgICAgICAgICB0YXJnZXQ9e1xuICAgICAgICAgICAgICAgICAgICAgIDxCYWNrZ3JvdW5kUGlja2VyXG4gICAgICAgICAgICAgICAgICAgICAgICB0aGVtZT17dGhlbWV9XG4gICAgICAgICAgICAgICAgICAgICAgICBvblNlbGVjdD17KCkgPT5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgc2V0QmFja2dyb3VuZFZlcnNpb24oKHZlcnNpb24pID0+IHZlcnNpb24gKyAxKVxuICAgICAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgIC8+XG4gICAgICAgICAgICAgICAgKSA6IG51bGx9XG4gICAgICAgICAgICAgIDwvQWN0aW9uUGFuZWw+XG4gICAgICAgICAgICB9XG4gICAgICAgICAgLz5cbiAgICAgICAgKTtcbiAgICAgIH0pfVxuICAgIDwvTGlzdD5cbiAgKTtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBLGlCQVFPO0FBQ1AsZ0JBQXFFO0FBQ3JFLGdCQUF3QjtBQUN4QixrQkFBd0M7QUFDeEMsMkJBQStDO0FBQy9DLG1CQUFvQztBQTJoQnBCO0FBemhCaEIsSUFBTSxpQkFBYSxzQkFBSyxtQkFBUSxHQUFHLFNBQVM7QUFDNUMsSUFBTSw2QkFBeUIsa0JBQUssWUFBWSxtQkFBbUI7QUFDbkUsSUFBTSxxQkFBaUIsc0JBQUssbUJBQVEsR0FBRyxXQUFXLFdBQVcsUUFBUTtBQUNyRSxJQUFNLDRCQUF3QjtBQUFBLE1BQzVCLG1CQUFRO0FBQUEsRUFDUjtBQUFBLEVBQ0E7QUFBQSxFQUNBO0FBQUEsRUFDQTtBQUFBLEVBQ0E7QUFDRjtBQUNBLElBQU0sbUJBQWUsc0JBQUssbUJBQVEsR0FBRyxVQUFVLGFBQWE7QUFDNUQsSUFBTSw2QkFBeUI7QUFBQSxNQUM3QixtQkFBUTtBQUFBLEVBQ1I7QUFBQSxFQUNBO0FBQUEsRUFDQTtBQUNGO0FBQ0EsSUFBTSx1QkFBbUIsc0JBQUssbUJBQVEsR0FBRyxVQUFVLE9BQU8sY0FBYztBQUV4RSxJQUFNLE9BQU87QUEwQ2IsU0FBUyxjQUFjLEtBQXVCO0FBQzVDLFFBQU0sY0FBVSxrQkFBSyxLQUFLLFdBQVc7QUFDckMsTUFBSSxLQUFDLHNCQUFXLE9BQU8sRUFBRyxRQUFPLENBQUM7QUFDbEMsUUFBTSxPQUFpQixDQUFDO0FBQ3hCLGFBQVcsWUFBUSx3QkFBYSxTQUFTLE9BQU8sRUFBRSxNQUFNLElBQUksR0FBRztBQUM3RCxVQUFNLFVBQVUsS0FBSyxLQUFLO0FBQzFCLFFBQUksQ0FBQyxXQUFXLFFBQVEsV0FBVyxHQUFHLEVBQUc7QUFDekMsVUFBTSxLQUFLLFFBQVEsUUFBUSxHQUFHO0FBQzlCLFFBQUksT0FBTyxHQUFJO0FBQ2YsVUFBTSxNQUFNLFFBQVEsTUFBTSxHQUFHLEVBQUU7QUFDL0IsUUFBSSxNQUFNLFFBQVEsTUFBTSxLQUFLLENBQUM7QUFDOUIsUUFDRyxJQUFJLFdBQVcsR0FBRyxLQUFLLElBQUksU0FBUyxHQUFHLEtBQ3ZDLElBQUksV0FBVyxHQUFHLEtBQUssSUFBSSxTQUFTLEdBQUcsR0FDeEM7QUFDQSxZQUFNLElBQUksTUFBTSxHQUFHLEVBQUU7QUFBQSxJQUN2QjtBQUNBLFNBQUssR0FBRyxJQUFJO0FBQUEsRUFDZDtBQUNBLFNBQU87QUFDVDtBQUVBLFNBQVMsYUFBYSxLQUF1QjtBQUMzQyxRQUFNLEtBQUssSUFBSSxpQkFBaUI7QUFFaEMsTUFBSSxHQUFHLFdBQVcsTUFBTSxFQUFHLFFBQU8sTUFBTSxHQUFHLE1BQU0sQ0FBQztBQUNsRCxTQUFPO0FBQ1Q7QUFFQSxTQUFTLGtCQUFrQixPQUErQztBQUN4RSxNQUFJLENBQUMsTUFBTyxRQUFPO0FBQ25CLE1BQUksb0JBQW9CLEtBQUssS0FBSyxFQUFHLFFBQU87QUFDNUMsTUFBSSxxQkFBcUIsS0FBSyxLQUFLLEVBQUcsUUFBTyxJQUFJLE1BQU0sTUFBTSxDQUFDLENBQUM7QUFDL0QsTUFBSSxxQkFBcUIsS0FBSyxLQUFLLEVBQUcsUUFBTyxJQUFJLE1BQU0sTUFBTSxDQUFDLENBQUM7QUFDL0QsTUFBSSxtQkFBbUIsS0FBSyxLQUFLLEVBQUcsUUFBTyxJQUFJLEtBQUs7QUFDcEQsU0FBTztBQUNUO0FBRUEsU0FBUyxPQUFPLEtBQXVDO0FBQ3JELFNBQU87QUFBQSxJQUNMLE9BQU8sU0FBUyxJQUFJLE1BQU0sR0FBRyxDQUFDLEdBQUcsRUFBRTtBQUFBLElBQ25DLE9BQU8sU0FBUyxJQUFJLE1BQU0sR0FBRyxDQUFDLEdBQUcsRUFBRTtBQUFBLElBQ25DLE9BQU8sU0FBUyxJQUFJLE1BQU0sR0FBRyxDQUFDLEdBQUcsRUFBRTtBQUFBLEVBQ3JDO0FBQ0Y7QUFRQSxJQUFNLHVCQUE0QztBQUFBLEVBQ2hELEVBQUUsSUFBSSxHQUFHLE1BQU0sT0FBTyxPQUFPLFVBQVU7QUFBQSxFQUN2QyxFQUFFLElBQUksR0FBRyxNQUFNLFVBQVUsT0FBTyxVQUFVO0FBQUEsRUFDMUMsRUFBRSxJQUFJLEdBQUcsTUFBTSxVQUFVLE9BQU8sVUFBVTtBQUFBLEVBQzFDLEVBQUUsSUFBSSxHQUFHLE1BQU0sU0FBUyxPQUFPLFVBQVU7QUFBQSxFQUN6QyxFQUFFLElBQUksR0FBRyxNQUFNLFFBQVEsT0FBTyxVQUFVO0FBQUEsRUFDeEMsRUFBRSxJQUFJLEdBQUcsTUFBTSxVQUFVLE9BQU8sVUFBVTtBQUFBLEVBQzFDLEVBQUUsSUFBSSxHQUFHLE1BQU0sUUFBUSxPQUFPLFVBQVU7QUFBQSxFQUN4QyxFQUFFLElBQUksR0FBRyxNQUFNLFlBQVksT0FBTyxVQUFVO0FBQzlDO0FBRUEsU0FBUyx3QkFBd0IsSUFBZ0M7QUFDL0QsTUFBSSxPQUFPLEdBQUksUUFBTztBQUN0QixTQUFPLHFCQUFxQixLQUFLLENBQUMsV0FBVyxPQUFPLE9BQU8sRUFBRSxHQUFHO0FBQ2xFO0FBRUEsU0FBUyxxQkFBcUIsT0FBK0M7QUFDM0UsTUFBSSxDQUFDLE1BQU8sUUFBTztBQUNuQixNQUFJLFVBQVUsS0FBSyxLQUFLLEdBQUc7QUFDekIsVUFBTSxLQUFLLE9BQU8sS0FBSztBQUN2QixXQUFPLE1BQU0sTUFBTSxNQUFNLElBQUksS0FBSztBQUFBLEVBQ3BDO0FBRUEsUUFBTSxNQUFNLE1BQU0sWUFBWSxFQUFFLFFBQVEsV0FBVyxFQUFFO0FBQ3JELFFBQU0sUUFBZ0M7QUFBQSxJQUNwQyxZQUFZO0FBQUEsSUFDWixhQUFhO0FBQUEsSUFDYixLQUFLO0FBQUEsSUFDTCxRQUFRO0FBQUEsSUFDUixRQUFRO0FBQUEsSUFDUixPQUFPO0FBQUEsSUFDUCxNQUFNO0FBQUEsSUFDTixRQUFRO0FBQUEsSUFDUixNQUFNO0FBQUEsSUFDTixVQUFVO0FBQUEsSUFDVixNQUFNO0FBQUEsSUFDTixNQUFNO0FBQUEsRUFDUjtBQUVBLFNBQU8sTUFBTSxHQUFHO0FBQ2xCO0FBRUEsU0FBUywwQkFBMEIsS0FBcUI7QUFDdEQsUUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLElBQUksT0FBTyxHQUFHO0FBQzVCLFFBQU0sTUFBTSxLQUFLLElBQUksR0FBRyxHQUFHLENBQUM7QUFDNUIsUUFBTSxNQUFNLEtBQUssSUFBSSxHQUFHLEdBQUcsQ0FBQztBQUM1QixRQUFNLFNBQVMsTUFBTTtBQUVyQixNQUFJLFNBQVMsR0FBSSxRQUFPO0FBRXhCLE1BQUk7QUFDSixNQUFJLFFBQVEsR0FBRztBQUNiLFVBQU8sTUFBTSxJQUFJLEtBQU07QUFDdkIsUUFBSSxNQUFNLEVBQUcsUUFBTztBQUFBLEVBQ3RCLFdBQVcsUUFBUSxHQUFHO0FBQ3BCLFVBQU0sTUFBTyxNQUFNLElBQUksS0FBTTtBQUFBLEVBQy9CLE9BQU87QUFDTCxVQUFNLE1BQU8sTUFBTSxJQUFJLEtBQU07QUFBQSxFQUMvQjtBQUVBLE1BQUksTUFBTSxNQUFNLE9BQU8sSUFBSyxRQUFPO0FBQ25DLE1BQUksTUFBTSxHQUFJLFFBQU87QUFDckIsTUFBSSxNQUFNLEdBQUksUUFBTztBQUNyQixNQUFJLE1BQU0sSUFBSyxRQUFPO0FBQ3RCLE1BQUksTUFBTSxJQUFLLFFBQU87QUFDdEIsTUFBSSxNQUFNLElBQUssUUFBTztBQUN0QixTQUFPO0FBQ1Q7QUFFQSxTQUFTLGdCQUFnQixLQUFhLEtBQWEsVUFBMEI7QUFDM0UsUUFBTSxrQkFBYyxrQkFBSyxLQUFLLGNBQWM7QUFDNUMsTUFBSSxLQUFDLHNCQUFXLFdBQVcsRUFBRyxRQUFPO0FBQ3JDLGFBQVcsWUFBUSx3QkFBYSxhQUFhLE9BQU8sRUFBRSxNQUFNLElBQUksR0FBRztBQUNqRSxVQUFNLFVBQVUsS0FBSyxLQUFLO0FBQzFCLFFBQUksQ0FBQyxRQUFRLFdBQVcsR0FBRyxHQUFHLEtBQUssRUFBRztBQUN0QyxVQUFNLFFBQVEsUUFBUSxNQUFNLEdBQUcsR0FBRyxNQUFNLE1BQU0sRUFBRSxLQUFLO0FBQ3JELFFBQUksb0JBQW9CLEtBQUssS0FBSyxFQUFHLFFBQU87QUFBQSxFQUM5QztBQUNBLFNBQU87QUFDVDtBQUVBLFNBQVMsa0JBQWtCLEtBQXVCO0FBQ2hELFFBQU0sa0JBQWMsa0JBQUssS0FBSyxjQUFjO0FBQzVDLE1BQUksS0FBQyxzQkFBVyxXQUFXLEVBQUcsUUFBTyxDQUFDO0FBQ3RDLFFBQU0sVUFBb0IsQ0FBQztBQUMzQixhQUFXLFlBQVEsd0JBQWEsYUFBYSxPQUFPLEVBQUUsTUFBTSxJQUFJLEdBQUc7QUFDakUsVUFBTSxVQUFVLEtBQUssS0FBSztBQUMxQixVQUFNLFFBQVEsUUFBUSxNQUFNLDRDQUE0QztBQUN4RSxRQUFJLENBQUMsTUFBTztBQUNaLFlBQVEsT0FBTyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksTUFBTSxDQUFDO0FBQUEsRUFDckM7QUFDQSxTQUFPO0FBQ1Q7QUFFQSxTQUFTLGVBQWUsS0FBYSxLQUF1QjtBQUMxRCxRQUFNLFVBQVUsa0JBQWtCLEdBQUc7QUFDckMsU0FBTyxRQUFRLENBQUMsS0FBSyxhQUFhLEdBQUc7QUFDdkM7QUFFQSxTQUFTLG1CQUFtQixLQUFhLEtBQXVCO0FBQzlELFNBQ0Usa0JBQWtCLElBQUksbUJBQW1CLEtBQ3pDLGtCQUFrQixJQUFJLGNBQWMsS0FDcEMsa0JBQWtCLElBQUksYUFBYSxLQUNuQyxlQUFlLEtBQUssR0FBRztBQUUzQjtBQUVBLFNBQVMsbUJBQW1CLEtBQWEsS0FBdUI7QUFDOUQsUUFBTSxhQUFhLHFCQUFxQixJQUFJLFlBQVk7QUFDeEQsTUFBSSxlQUFlLFFBQVc7QUFDNUIsV0FBTyx3QkFBd0IsVUFBVSxLQUFLO0FBQUEsRUFDaEQ7QUFFQSxRQUFNLGNBQWMsa0JBQWtCLElBQUksWUFBWTtBQUN0RCxTQUFPLDBCQUEwQixlQUFlLG1CQUFtQixLQUFLLEdBQUcsQ0FBQztBQUM5RTtBQUVBLFNBQVMsa0JBQWtCLEtBQWEsS0FBdUI7QUFDN0QsUUFBTSxTQUFTLG1CQUFtQixLQUFLLEdBQUc7QUFDMUMsU0FDRSxrQkFBa0IsSUFBSSxlQUFlLEtBQ3JDLGtCQUFrQixnQkFBZ0IsS0FBSyx3QkFBd0IsTUFBTSxDQUFDLEtBQ3RFO0FBRUo7QUFFQSxTQUFTLHFCQUFzQztBQUM3QyxNQUFJLEtBQUMsc0JBQVcsc0JBQXNCLEVBQUcsUUFBTyxDQUFDO0FBQ2pELE1BQUk7QUFDRixVQUFNLFNBQVMsS0FBSyxVQUFNLHdCQUFhLHdCQUF3QixPQUFPLENBQUM7QUFDdkUsV0FBTyxVQUFVLE9BQU8sV0FBVyxZQUFZLENBQUMsTUFBTSxRQUFRLE1BQU0sSUFDaEUsU0FDQSxDQUFDO0FBQUEsRUFDUCxRQUFRO0FBQ04sV0FBTyxDQUFDO0FBQUEsRUFDVjtBQUNGO0FBRUEsU0FBUyxxQkFBcUIsT0FBd0I7QUFDcEQsK0JBQWMsd0JBQXdCLEdBQUcsS0FBSyxVQUFVLE9BQU8sTUFBTSxDQUFDLENBQUM7QUFBQSxDQUFJO0FBQzdFO0FBRUEsU0FBUyxZQUFZLFVBQTJCO0FBQzlDLFNBQU8sQ0FBQyxRQUFRLFNBQVMsUUFBUSxTQUFTLE9BQU8sRUFBRTtBQUFBLFFBQ2pELHFCQUFRLFFBQVEsRUFBRSxZQUFZO0FBQUEsRUFDaEM7QUFDRjtBQUVBLFNBQVMsdUJBQXVCLFVBQTBCO0FBQ3hELGFBQU8sc0JBQVMsY0FBVSxxQkFBUSxRQUFRLENBQUMsRUFDeEMsUUFBUSxnQkFBZ0IsRUFBRSxFQUMxQixRQUFRLFdBQVcsR0FBRyxFQUN0QixRQUFRLFNBQVMsQ0FBQyxXQUFXLE9BQU8sWUFBWSxDQUFDO0FBQ3REO0FBRUEsU0FBUyxvQkFBb0IsT0FBc0M7QUFDakUsUUFBTSxZQUFRLGtCQUFLLFlBQVksTUFBTSxNQUFNLGFBQWE7QUFDeEQsTUFBSSxLQUFDLHNCQUFXLEtBQUssRUFBRyxRQUFPLENBQUM7QUFFaEMsYUFBTyx1QkFBWSxPQUFPLEVBQUUsZUFBZSxLQUFLLENBQUMsRUFDOUMsT0FBTyxDQUFDLFVBQVUsTUFBTSxPQUFPLEtBQUssQ0FBQyxNQUFNLEtBQUssV0FBVyxHQUFHLENBQUMsRUFDL0QsT0FBTyxDQUFDLFVBQVUsWUFBWSxNQUFNLElBQUksQ0FBQyxFQUN6QyxJQUFJLENBQUMsVUFBVTtBQUNkLFVBQU0sZUFBZSxlQUFlLE1BQU0sSUFBSTtBQUM5QyxVQUFNLGtCQUFjO0FBQUEsTUFDbEI7QUFBQSxNQUNBO0FBQUEsTUFDQSxPQUFHLHNCQUFTLE1BQU0sVUFBTSxxQkFBUSxNQUFNLElBQUksQ0FBQyxDQUFDO0FBQUEsSUFDOUM7QUFDQSxVQUFNLFdBQU8sa0JBQUssT0FBTyxNQUFNLElBQUk7QUFDbkMsV0FBTztBQUFBLE1BQ0wsV0FBVyxNQUFNO0FBQUEsTUFDakI7QUFBQSxNQUNBO0FBQUEsTUFDQSxpQkFBYSxzQkFBVyxXQUFXLElBQUksY0FBYztBQUFBLE1BQ3JELGFBQWEsdUJBQXVCLE1BQU0sSUFBSTtBQUFBLElBQ2hEO0FBQUEsRUFDRixDQUFDLEVBQ0EsS0FBSyxDQUFDLEdBQUcsTUFBTSxFQUFFLGFBQWEsY0FBYyxFQUFFLFlBQVksQ0FBQztBQUNoRTtBQUVBLFNBQVMsc0JBQXNCLE9BQWdEO0FBQzdFLFFBQU0sY0FBYyxvQkFBb0IsS0FBSztBQUM3QyxRQUFNLFFBQVEsbUJBQW1CO0FBQ2pDLFFBQU0sbUJBQW1CLE1BQU0sTUFBTSxJQUFJLEtBQUssTUFBTSxJQUFJO0FBQ3hELFNBQ0UsWUFBWTtBQUFBLElBQ1YsQ0FBQyxlQUFlLFdBQVcsaUJBQWlCO0FBQUEsRUFDOUMsS0FBSyxZQUFZLENBQUM7QUFFdEI7QUFFQSxTQUFTLG9CQUFvQixLQUFhLEtBQXVCO0FBQy9ELFFBQU0sVUFBVSxrQkFBa0IsR0FBRztBQUNyQyxTQUFPLFFBQVEsQ0FBQyxLQUFLLGFBQWEsR0FBRztBQUN2QztBQVVBLFNBQVMsWUFBMEI7QUFDakMsTUFBSSxLQUFDLHNCQUFXLFVBQVUsRUFBRyxRQUFPLENBQUM7QUFDckMsYUFBTyx1QkFBWSxZQUFZLEVBQUUsZUFBZSxLQUFLLENBQUMsRUFDbkQsT0FBTyxDQUFDLE1BQU0sRUFBRSxZQUFZLENBQUMsRUFDN0IsSUFBSSxDQUFDLE1BQU07QUFDVixVQUFNLFVBQU0sa0JBQUssWUFBWSxFQUFFLElBQUk7QUFDbkMsVUFBTSxNQUFNLGNBQWMsR0FBRztBQUM3QixVQUFNLFlBQVEsa0JBQUssS0FBSyxhQUFhO0FBQ3JDLFVBQU0sY0FBVSxzQkFBVyxLQUFLLFFBQzVCLHVCQUFZLE9BQU8sRUFBRSxlQUFlLEtBQUssQ0FBQyxFQUFFO0FBQUEsTUFDMUMsQ0FBQyxVQUFVLE1BQU0sT0FBTyxLQUFLLFlBQVksTUFBTSxJQUFJO0FBQUEsSUFDckQsRUFBRSxTQUNGO0FBQ0osVUFBTSxvQkFDSixtQkFBbUIsRUFBRSxFQUFFLElBQUksS0FBSyxJQUFJLGFBQWE7QUFDbkQsV0FBTztBQUFBLE1BQ0wsTUFBTSxFQUFFO0FBQUEsTUFDUixhQUFhLEVBQUUsS0FDWixNQUFNLEdBQUcsRUFDVCxJQUFJLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxFQUFFLFlBQVksSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDLEVBQ2pELEtBQUssR0FBRztBQUFBLE1BQ1g7QUFBQSxNQUNBLGtCQUFjLDBCQUFXLGtCQUFLLEtBQUssaUJBQWlCLENBQUM7QUFBQSxNQUNyRCxtQkFBZSwwQkFBVyxrQkFBSyxLQUFLLGlCQUFpQixDQUFDLFFBQ2xELGtCQUFLLEtBQUssaUJBQWlCLElBQzNCO0FBQUEsTUFDSixhQUFhLGVBQWUsS0FBSyxHQUFHO0FBQUEsTUFDcEMsdUJBQXVCLG9CQUFvQixLQUFLLEdBQUc7QUFBQSxNQUNuRCxXQUFXO0FBQUEsUUFDVDtBQUFBLFFBQ0E7QUFBQSxRQUNBLElBQUksY0FBYyxTQUFTLFlBQVk7QUFBQSxNQUN6QztBQUFBLE1BQ0EsU0FBUyxrQkFBa0IsR0FBRztBQUFBLE1BQzlCLFFBQVEsSUFBSSxjQUFjO0FBQUEsTUFDMUI7QUFBQSxNQUNBLGlCQUFpQixtQkFBbUIsS0FBSyxHQUFHO0FBQUEsTUFDNUMsZ0JBQWdCLGtCQUFrQixLQUFLLEdBQUc7QUFBQSxJQUM1QztBQUFBLEVBQ0YsQ0FBQyxFQUNBLEtBQUssQ0FBQyxHQUFHLE1BQU0sRUFBRSxLQUFLLGNBQWMsRUFBRSxJQUFJLENBQUM7QUFDaEQ7QUFFQSxTQUFTLGtCQUEwQjtBQUNqQyxRQUFNLFdBQU8sa0JBQUssWUFBWSxVQUFVO0FBQ3hDLE1BQUksS0FBQyxzQkFBVyxJQUFJLEVBQUcsUUFBTztBQUM5QixhQUFPLHdCQUFhLE1BQU0sT0FBTyxFQUFFLEtBQUs7QUFDMUM7QUFtREEsU0FBUyxzQkFBc0IsT0FBbUI7QUFDaEQsUUFBTSxlQUFlLE9BQU87QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBLGVBUWYsS0FBSyxVQUFVLElBQUksQ0FBQztBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBMkJqQyxRQUFNLGFBQVM7QUFBQSxJQUNiLFFBQVE7QUFBQSxJQUNSLENBQUMsTUFBTSxjQUFjLEtBQUssVUFBVSxLQUFLLENBQUM7QUFBQSxJQUMxQztBQUFBLE1BQ0UsVUFBVTtBQUFBLE1BQ1YsT0FBTztBQUFBLElBQ1Q7QUFBQSxFQUNGO0FBQ0EsU0FBTyxNQUFNO0FBQ2Y7QUFFQSxTQUFTLDJCQUNQLE9BQ0EsWUFDQTtBQUNBLFFBQU0sZUFBZSxPQUFPO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQSxlQVFmLEtBQUssVUFBVSxJQUFJLENBQUM7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBVWpDLFFBQU0sYUFBUztBQUFBLElBQ2IsUUFBUTtBQUFBLElBQ1IsQ0FBQyxNQUFNLGNBQWMsTUFBTSxNQUFNLFdBQVcsWUFBWTtBQUFBLElBQ3hEO0FBQUEsTUFDRSxVQUFVO0FBQUEsTUFDVixPQUFPO0FBQUEsSUFDVDtBQUFBLEVBQ0Y7QUFDQSxTQUFPLE1BQU07QUFDZjtBQUVBLFNBQVMsaUJBQWlCLE9BR3ZCO0FBQ0QsUUFBTSxpQkFBaUIsb0JBQW9CLE1BQU0sS0FBSztBQUN0RCxRQUFNLFdBQVcsc0JBQXNCLE1BQU0sS0FBSztBQUVsRCxRQUFNLGNBQWMsV0FDaEI7QUFBQSxJQUNFO0FBQUEsSUFDQSxHQUFHLGVBQWU7QUFBQSxNQUNoQixDQUFDLE9BQU8sR0FBRyxpQkFBaUIsU0FBUztBQUFBLElBQ3ZDO0FBQUEsRUFDRixJQUNBO0FBRUosU0FDRTtBQUFBLElBQUM7QUFBQTtBQUFBLE1BQ0MsU0FBUztBQUFBLE1BQ1QsT0FBTyxnQkFBSyxNQUFNO0FBQUEsTUFDbEIsS0FBSyxnQkFBSyxJQUFJO0FBQUEsTUFDZCxhQUFZO0FBQUEsTUFDWixpQkFBaUIsc0JBQXNCLE1BQU0sTUFBTSxXQUFXO0FBQUEsTUFDOUQsc0JBQXFCO0FBQUEsTUFFcEIsc0JBQVksSUFBSSxDQUFDLGVBQWU7QUFDL0IsY0FBTSxhQUFhLFdBQVcsaUJBQWlCLFVBQVU7QUFDekQsZUFDRTtBQUFBLFVBQUMsZ0JBQUs7QUFBQSxVQUFMO0FBQUEsWUFFQyxPQUFPLFdBQVc7QUFBQSxZQUNsQixXQUNFLGFBQ0ksRUFBRSxNQUFNLGdCQUFLLGFBQWEsU0FBUyxVQUFVLElBQzdDO0FBQUEsWUFFTixTQUFTLEVBQUUsUUFBUSxXQUFXLEtBQUs7QUFBQSxZQUNuQyxTQUNFLDRDQUFDLDBCQUNDO0FBQUEsY0FBQztBQUFBO0FBQUEsZ0JBQ0MsT0FBTTtBQUFBLGdCQUNOLE1BQU0sZ0JBQUs7QUFBQSxnQkFDWCxVQUFVLFlBQVk7QUFDcEIsd0JBQU0sUUFBUSxtQkFBbUI7QUFDakMsd0JBQU0sTUFBTSxNQUFNLElBQUksSUFBSSxXQUFXO0FBQ3JDLHVDQUFxQixLQUFLO0FBQzFCLHdCQUFNLFNBQVMsTUFBTSxNQUFNLE1BQU0sV0FBVyxZQUFZO0FBQ3hELDZDQUEyQixNQUFNLE9BQU8sVUFBVTtBQUNsRCw0QkFBTSw0QkFBZ0I7QUFBQSxnQkFDeEI7QUFBQTtBQUFBLFlBQ0YsR0FDRjtBQUFBO0FBQUEsVUF0QkcsV0FBVztBQUFBLFFBd0JsQjtBQUFBLE1BRUosQ0FBQztBQUFBO0FBQUEsRUFDSDtBQUVKO0FBRWUsU0FBUixVQUEyQjtBQUNoQyxRQUFNLFNBQVMsVUFBVTtBQUN6QixRQUFNLENBQUMsU0FBUyxVQUFVLFFBQUksdUJBQVMsTUFBTSxnQkFBZ0IsQ0FBQztBQUM5RCxRQUFNLENBQUMsY0FBYyxlQUFlLFFBQUksdUJBQXdCLElBQUk7QUFDcEUsUUFBTSxDQUFDLG1CQUFtQixvQkFBb0IsUUFBSSx1QkFBUyxDQUFDO0FBRTVELDhCQUFVLE1BQU07QUFDZCxVQUFNLG1CQUFtQixNQUFNO0FBQzdCLFlBQU0sT0FBTyxnQkFBZ0I7QUFDN0IsaUJBQVcsQ0FBQyxTQUFVLFNBQVMsT0FBTyxPQUFPLElBQUs7QUFBQSxJQUNwRDtBQUVBLHFCQUFpQjtBQUNqQixVQUFNLFdBQVcsWUFBWSxrQkFBa0IsR0FBRztBQUNsRCxXQUFPLE1BQU0sY0FBYyxRQUFRO0FBQUEsRUFDckMsR0FBRyxDQUFDLENBQUM7QUFFTCxXQUFTLGdCQUFnQixPQUEyQjtBQUNsRCxVQUFNLHFCQUFxQixzQkFBc0IsS0FBSztBQUN0RCxVQUFNLG1CQUFtQixxQkFDckIsS0FBSyxNQUFNLFdBQVcsS0FBSyxVQUFVLFVBQVUsbUJBQW1CLFdBQVcsRUFBRSxDQUFDLDJDQUNoRjtBQUNKLFVBQU0sUUFBUTtBQUFBLE1BQ1osS0FBSyxNQUFNLFdBQVc7QUFBQSxNQUN0QjtBQUFBLE1BQ0E7QUFBQSxNQUNBO0FBQUEsTUFDQSxxQkFDSSxnQkFBZ0IsbUJBQW1CLFdBQVcsT0FDOUM7QUFBQSxNQUNKLFlBQVksTUFBTSxXQUFXO0FBQUEsTUFDN0Isa0JBQWtCLE1BQU0sZUFBZTtBQUFBLE1BQ3ZDLGVBQWUsTUFBTSxjQUFjO0FBQUEsTUFDbkMsVUFBVSxNQUFNLFNBQVM7QUFBQSxJQUMzQjtBQUVBLFdBQU8sTUFBTSxLQUFLLElBQUk7QUFBQSxFQUN4QjtBQUVBLFdBQVMsbUJBQW1CLE9BQW1CO0FBQzdDLFdBQU87QUFBQSxNQUNMLFdBQVcsTUFBTSxRQUFRLENBQUMsS0FBSyxNQUFNO0FBQUEsTUFDckMsU0FBUyxNQUFNLFFBQVEsQ0FBQyxLQUFLLE1BQU07QUFBQSxNQUNuQyxZQUFZLE1BQU0sUUFBUSxDQUFDLEtBQUssTUFBTTtBQUFBLE1BQ3RDLFNBQVMsTUFBTSxRQUFRLENBQUMsS0FBSyxNQUFNO0FBQUEsTUFDbkMsUUFBUSxNQUFNLFFBQVEsQ0FBQyxLQUFLLE1BQU07QUFBQSxNQUNsQyxRQUFRLE1BQU07QUFBQSxJQUNoQjtBQUFBLEVBQ0Y7QUFFQSxRQUFNLGNBQ0osT0FBTyxLQUFLLENBQUMsVUFBVSxNQUFNLFNBQVMsT0FBTyxLQUFLLE9BQU8sQ0FBQztBQUM1RCxRQUFNLG1CQUFtQixjQUNyQixzQkFBc0IsV0FBVyxJQUNqQztBQUVKLFdBQVMsNkJBQTZCLE9BQTJCO0FBQy9ELFVBQU0sV0FBVyxzQkFBc0IsS0FBSztBQUM1QyxRQUFJLENBQUMsU0FBVSxRQUFPO0FBQ3RCLFdBQU8sS0FBSyxTQUFTLFdBQVcsS0FBSyxVQUFVLFVBQVUsU0FBUyxXQUFXLEVBQUUsQ0FBQztBQUFBLEVBQ2xGO0FBRUEsU0FDRSw2Q0FBQyxtQkFBSyxpQkFBZSxNQUFDLHNCQUFxQixvQkFDeEM7QUFBQSxrQkFDQztBQUFBLE1BQUMsZ0JBQUs7QUFBQSxNQUFMO0FBQUEsUUFFQyxPQUFNO0FBQUEsUUFDTixVQUFVLFlBQVk7QUFBQSxRQUN0QixNQUFNLGdCQUFLO0FBQUEsUUFDWCxhQUFhLENBQUMsRUFBRSxNQUFNLEdBQUcsWUFBWSxPQUFPLFdBQVcsQ0FBQztBQUFBLFFBQ3hELFFBQ0U7QUFBQSxVQUFDLGdCQUFLLEtBQUs7QUFBQSxVQUFWO0FBQUEsWUFDQyxVQUFVLDZCQUE2QixXQUFXO0FBQUE7QUFBQSxRQUNwRDtBQUFBLFFBRUYsU0FDRSw0Q0FBQywwQkFDQztBQUFBLFVBQUMsa0JBQU87QUFBQSxVQUFQO0FBQUEsWUFDQyxPQUFNO0FBQUEsWUFDTixNQUFNLGdCQUFLO0FBQUEsWUFDWCxRQUNFO0FBQUEsY0FBQztBQUFBO0FBQUEsZ0JBQ0MsT0FBTztBQUFBLGdCQUNQLFVBQVUsTUFDUixxQkFBcUIsQ0FBQyxZQUFZLFVBQVUsQ0FBQztBQUFBO0FBQUEsWUFFakQ7QUFBQTtBQUFBLFFBRUosR0FDRjtBQUFBO0FBQUEsTUF4QkcscUJBQXFCLGlCQUFpQjtBQUFBLElBMEI3QyxJQUNFO0FBQUEsSUFDSCxPQUFPLElBQUksQ0FBQyxVQUFVO0FBQ3JCLFlBQU0sWUFBWSxNQUFNLFVBQVUsZ0JBQWdCO0FBQ2xELFlBQU0sY0FBcUMsQ0FBQztBQUM1QyxVQUFJO0FBQ0Ysb0JBQVksS0FBSyxFQUFFLEtBQUssRUFBRSxPQUFPLFVBQVUsT0FBTyxpQkFBTSxNQUFNLEVBQUUsQ0FBQztBQUNuRSxrQkFBWSxLQUFLLEVBQUUsTUFBTSxNQUFNLFNBQVMsZ0JBQUssT0FBTyxnQkFBSyxJQUFJLENBQUM7QUFDOUQsVUFBSSxNQUFNLFVBQVU7QUFDbEIsb0JBQVksS0FBSyxFQUFFLE1BQU0sR0FBRyxNQUFNLE9BQU8sTUFBTSxDQUFDO0FBRWxELGFBQ0U7QUFBQSxRQUFDLGdCQUFLO0FBQUEsUUFBTDtBQUFBLFVBRUMsT0FBTyxNQUFNO0FBQUEsVUFDYixNQUFNO0FBQUEsWUFDSixRQUFRLGdCQUFLO0FBQUEsWUFDYixXQUFXLE1BQU07QUFBQSxVQUNuQjtBQUFBLFVBQ0E7QUFBQSxVQUNBLFFBQ0U7QUFBQSxZQUFDLGdCQUFLLEtBQUs7QUFBQSxZQUFWO0FBQUEsY0FDQyxVQUFVLGdCQUFnQixLQUFLO0FBQUEsY0FDL0IsVUFDRSw2Q0FBQyxnQkFBSyxLQUFLLE9BQU8sVUFBakIsRUFDQztBQUFBO0FBQUEsa0JBQUMsZ0JBQUssS0FBSyxPQUFPLFNBQVM7QUFBQSxrQkFBMUI7QUFBQSxvQkFDQyxPQUFNO0FBQUEsb0JBQ04sTUFBTSxNQUFNLFNBQVMsU0FBUztBQUFBO0FBQUEsZ0JBQ2hDO0FBQUEsZ0JBQ0EsNENBQUMsZ0JBQUssS0FBSyxPQUFPLFNBQVMsV0FBMUIsRUFBb0M7QUFBQSxnQkFDckM7QUFBQSxrQkFBQyxnQkFBSyxLQUFLLE9BQU8sU0FBUztBQUFBLGtCQUExQjtBQUFBLG9CQUNDLE9BQU07QUFBQSxvQkFDTixNQUFNO0FBQUEsc0JBQ0osTUFBTSxZQUFZLE1BQU0sZUFBZSxJQUFJO0FBQUEsb0JBQzdDO0FBQUE7QUFBQSxnQkFDRjtBQUFBLGdCQUNBLDRDQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFdBQTFCLEVBQW9DO0FBQUEsZ0JBQ3JDO0FBQUEsa0JBQUMsZ0JBQUssS0FBSyxPQUFPLFNBQVM7QUFBQSxrQkFBMUI7QUFBQSxvQkFDQyxPQUFNO0FBQUEsb0JBQ04sTUFBTSxNQUFNO0FBQUE7QUFBQSxnQkFDZDtBQUFBLGdCQUNBO0FBQUEsa0JBQUMsZ0JBQUssS0FBSyxPQUFPLFNBQVM7QUFBQSxrQkFBMUI7QUFBQSxvQkFDQyxPQUFNO0FBQUEsb0JBQ04sTUFBTSxNQUFNO0FBQUE7QUFBQSxnQkFDZDtBQUFBLGdCQUNBLDRDQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFdBQTFCLEVBQW9DO0FBQUEsZ0JBQ3JDLDZDQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFNBQTFCLEVBQWtDLE9BQU0sVUFDdkM7QUFBQTtBQUFBLG9CQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFFBQVE7QUFBQSxvQkFBbEM7QUFBQSxzQkFDQyxNQUFLO0FBQUEsc0JBQ0wsT0FBTyxtQkFBbUIsS0FBSyxFQUFFO0FBQUE7QUFBQSxrQkFDbkM7QUFBQSxrQkFDQTtBQUFBLG9CQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFFBQVE7QUFBQSxvQkFBbEM7QUFBQSxzQkFDQyxNQUFLO0FBQUEsc0JBQ0wsT0FBTyxtQkFBbUIsS0FBSyxFQUFFO0FBQUE7QUFBQSxrQkFDbkM7QUFBQSxrQkFDQTtBQUFBLG9CQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFFBQVE7QUFBQSxvQkFBbEM7QUFBQSxzQkFDQyxNQUFLO0FBQUEsc0JBQ0wsT0FBTyxtQkFBbUIsS0FBSyxFQUFFO0FBQUE7QUFBQSxrQkFDbkM7QUFBQSxrQkFDQTtBQUFBLG9CQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFFBQVE7QUFBQSxvQkFBbEM7QUFBQSxzQkFDQyxNQUFLO0FBQUEsc0JBQ0wsT0FBTyxtQkFBbUIsS0FBSyxFQUFFO0FBQUE7QUFBQSxrQkFDbkM7QUFBQSxrQkFDQTtBQUFBLG9CQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFFBQVE7QUFBQSxvQkFBbEM7QUFBQSxzQkFDQyxNQUFLO0FBQUEsc0JBQ0wsT0FBTyxtQkFBbUIsS0FBSyxFQUFFO0FBQUE7QUFBQSxrQkFDbkM7QUFBQSxrQkFDQTtBQUFBLG9CQUFDLGdCQUFLLEtBQUssT0FBTyxTQUFTLFFBQVE7QUFBQSxvQkFBbEM7QUFBQSxzQkFDQyxNQUFLO0FBQUEsc0JBQ0wsT0FBTyxtQkFBbUIsS0FBSyxFQUFFO0FBQUE7QUFBQSxrQkFDbkM7QUFBQSxtQkFDRjtBQUFBLGlCQUNGO0FBQUE7QUFBQSxVQUVKO0FBQUEsVUFFRixTQUNFLDZDQUFDLDBCQUNDO0FBQUE7QUFBQSxjQUFDO0FBQUE7QUFBQSxnQkFDQyxPQUFNO0FBQUEsZ0JBQ04sTUFBTSxnQkFBSztBQUFBLGdCQUNYLFVBQVUsRUFBRSxXQUFXLENBQUMsR0FBRyxLQUFLLFNBQVM7QUFBQSxnQkFDekMsVUFBVSxZQUFZO0FBQ3BCLHdDQUFzQixLQUFLO0FBQzNCLDRCQUFNLDRCQUFnQjtBQUFBLGdCQUN4QjtBQUFBO0FBQUEsWUFDRjtBQUFBLFlBQ0MsTUFBTSxVQUFVLElBQ2Y7QUFBQSxjQUFDLGtCQUFPO0FBQUEsY0FBUDtBQUFBLGdCQUNDLE9BQU07QUFBQSxnQkFDTixNQUFNLGdCQUFLO0FBQUEsZ0JBQ1gsVUFBVSxFQUFFLFdBQVcsQ0FBQyxLQUFLLEdBQUcsS0FBSyxJQUFJO0FBQUEsZ0JBQ3pDLFFBQ0U7QUFBQSxrQkFBQztBQUFBO0FBQUEsb0JBQ0M7QUFBQSxvQkFDQSxVQUFVLE1BQ1IscUJBQXFCLENBQUMsWUFBWSxVQUFVLENBQUM7QUFBQTtBQUFBLGdCQUVqRDtBQUFBO0FBQUEsWUFFSixJQUNFO0FBQUEsYUFDTjtBQUFBO0FBQUEsUUF6RkcsTUFBTTtBQUFBLE1BMkZiO0FBQUEsSUFFSixDQUFDO0FBQUEsS0FDSDtBQUVKOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/raycast-theme/test/backgrounds.test.ts b/raycast-theme/test/backgrounds.test.ts new file mode 100644 index 0000000..4820140 --- /dev/null +++ b/raycast-theme/test/backgrounds.test.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { after, test } from "node:test"; +import { backgroundMatchesPath, loadBackgrounds } from "../src/backgrounds.ts"; + +const directories: string[] = []; + +after(async () => { + await Promise.all(directories.map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +async function themeDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "macarchy-backgrounds-")); + directories.push(directory); + return directory; +} + +test("loads root and nested background images", async () => { + const directory = await themeDirectory(); + await mkdir(join(directory, "backgrounds")); + await writeFile(join(directory, "backgrounds", "01-city-night.jpg"), "nested image"); + await writeFile(join(directory, "sunrise.png"), "root image"); + + const backgrounds = await loadBackgrounds(directory); + + assert.deepEqual( + backgrounds.map(({ name, relativePath }) => ({ name, relativePath })), + [ + { name: "City Night", relativePath: "backgrounds/01-city-night.jpg" }, + { name: "Sunrise", relativePath: "sunrise.png" }, + ], + ); +}); + +test("hides an identical root compatibility copy", async () => { + const directory = await themeDirectory(); + await mkdir(join(directory, "backgrounds")); + await writeFile(join(directory, "backgrounds", "01-original.jpg"), "same image"); + await writeFile(join(directory, "wall.jpg"), "same image"); + + const backgrounds = await loadBackgrounds(directory); + + assert.deepEqual( + backgrounds.map(({ relativePath }) => relativePath), + ["backgrounds/01-original.jpg"], + ); + assert.deepEqual(backgrounds[0].aliasPaths, ["wall.jpg"]); + assert.equal(backgroundMatchesPath(backgrounds[0], "wall.jpg"), true); + assert.equal(backgroundMatchesPath(backgrounds[0], "backgrounds/01-original.jpg"), true); +}); + +test("keeps distinct images with the same file size", async () => { + const directory = await themeDirectory(); + await mkdir(join(directory, "backgrounds")); + await writeFile(join(directory, "backgrounds", "nested.jpg"), "first"); + await writeFile(join(directory, "root.jpg"), "other"); + + const backgrounds = await loadBackgrounds(directory); + + assert.deepEqual( + backgrounds.map(({ relativePath }) => relativePath), + ["backgrounds/nested.jpg", "root.jpg"], + ); +}); diff --git a/raycast-theme/test/preferences.test.ts b/raycast-theme/test/preferences.test.ts new file mode 100644 index 0000000..ba682b4 --- /dev/null +++ b/raycast-theme/test/preferences.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import { DEFAULT_PATHS, resolvePreferencePath } from "../src/paths.ts"; +import { normaliseLegacyThemeName } from "../src/theme-utils.ts"; + +const HOME = "/home/example"; + +test("keeps manifest defaults aligned with runtime defaults", async () => { + const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as { + preferences: Array<{ default?: string; name: keyof typeof DEFAULT_PATHS; required?: boolean }>; + }; + + for (const preference of manifest.preferences) { + assert.equal(preference.default, DEFAULT_PATHS[preference.name]); + assert.notEqual(preference.required, true); + } +}); + +test("uses portable defaults when preferences are unset or empty", () => { + assert.equal(resolvePreferencePath(undefined, DEFAULT_PATHS.themesDirectory, HOME), "/home/example/.config/themes"); + assert.equal( + resolvePreferencePath(" ", DEFAULT_PATHS.themeStateDirectory, HOME), + "/home/example/.local/state/macarchy/themes", + ); + assert.equal( + resolvePreferencePath(null, DEFAULT_PATHS.themeSwitcherPath, HOME), + "/home/example/.local/bin/theme-switch", + ); +}); + +test("trims configured paths and expands home references", () => { + assert.equal(resolvePreferencePath(" ~/themes ", DEFAULT_PATHS.themesDirectory, HOME), "/home/example/themes"); + assert.equal( + resolvePreferencePath(" $HOME/bin/theme-switch ", DEFAULT_PATHS.themeSwitcherPath, HOME), + "/home/example/bin/theme-switch", + ); + assert.equal(resolvePreferencePath(" /Volumes/Themes ", DEFAULT_PATHS.themesDirectory, HOME), "/Volumes/Themes"); +}); + +test("keeps an original theme active while its pack is installed", () => { + assert.equal(normaliseLegacyThemeName("lumon\n", [{ name: "lumon" }, { name: "cool-blue" }]), "lumon"); + assert.equal(normaliseLegacyThemeName("turbonite", [{ name: "turbonite" }, { name: "amber-metal" }]), "turbonite"); +}); + +test("uses a renamed theme only when the original pack is absent", () => { + assert.equal(normaliseLegacyThemeName("lumon", [{ name: "cool-blue" }]), "cool-blue"); + assert.equal(normaliseLegacyThemeName("turbonite", [{ name: "amber-metal" }]), "amber-metal"); + assert.equal(normaliseLegacyThemeName("custom", [{ name: "cool-blue" }]), "custom"); +}); diff --git a/raycast-theme/test/runtime-cleanliness.test.ts b/raycast-theme/test/runtime-cleanliness.test.ts new file mode 100644 index 0000000..34a49cb --- /dev/null +++ b/raycast-theme/test/runtime-cleanliness.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { after, test } from "node:test"; + +const homes: string[] = []; +const guard = join(import.meta.dirname, "..", "scripts", "assert-runtime-clean.mjs"); + +after(async () => { + await Promise.all(homes.map((home) => rm(home, { force: true, recursive: true }))); +}); + +async function temporaryHome(): Promise { + const home = await mkdtemp(join(tmpdir(), "macarchy-raycast-")); + homes.push(home); + return home; +} + +test("allows development when the Raycast runtime has no dependencies", async () => { + const home = await temporaryHome(); + const result = spawnSync(process.execPath, [guard], { env: { ...process.env, HOME: home }, encoding: "utf8" }); + + assert.equal(result.status, 0, result.stderr); +}); + +test("blocks duplicate React dependencies in the Raycast runtime", async () => { + const home = await temporaryHome(); + const dependencies = join(home, ".config", "raycast", "extensions", "theme-switcher", "node_modules"); + await mkdir(dirname(dependencies), { recursive: true }); + await mkdir(dependencies); + + const result = spawnSync(process.execPath, [guard], { env: { ...process.env, HOME: home }, encoding: "utf8" }); + + assert.equal(result.status, 1); + assert.match(result.stderr, /Raycast runtime dependencies were found/); +}); diff --git a/raycast-theme/tsconfig.json b/raycast-theme/tsconfig.json new file mode 100644 index 0000000..115efb6 --- /dev/null +++ b/raycast-theme/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 22", + "include": ["src/**/*", "raycast-env.d.ts"], + "compilerOptions": { + "lib": ["es2021"], + "module": "commonjs", + "target": "es2021", + "types": ["node"], + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true + } +} diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..5435d9f --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + +section() { + printf '\n==> %s\n' "$1" +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || { + printf 'Error: %s is required to run the repository checks.\n' "$1" >&2 + exit 1 + } +} + +repository_files() { + git -C "$REPO_ROOT" ls-files -co --exclude-standard -z +} + +check_shell_syntax() { + local path relative first_line + + section 'Checking Bash and Zsh syntax' + while IFS= read -r -d '' relative; do + path="$REPO_ROOT/$relative" + [ -f "$path" ] || continue + + case "$relative" in + *.zsh|*.zsh-theme|*/.zprofile|*/.zshenv|*/.zshrc) + zsh -n "$path" + continue + ;; + *.sh) + bash -n "$path" + continue + ;; + esac + + first_line="$(LC_ALL=C head -n 1 "$path" 2>/dev/null || true)" + case "$first_line" in + *bash*) bash -n "$path" ;; + *zsh*) zsh -n "$path" ;; + esac + done < <(repository_files) +} + +check_json_and_plists() { + local path relative + + section 'Validating JSON and property lists' + while IFS= read -r -d '' relative; do + path="$REPO_ROOT/$relative" + [ -f "$path" ] || continue + + case "$relative" in + *.json|*.jsonc) + jq empty "$path" + ;; + *.plist) + python3 -c \ + 'import plistlib, sys; plistlib.load(open(sys.argv[1], "rb"))' \ + "$path" + ;; + esac + done < <(repository_files) +} + +check_raycast() { + section 'Checking the Raycast extension' + node -e ' + const [major, minor, patch] = process.versions.node.split(".").map(Number); + if (major < 22 || (major === 22 && (minor < 22 || (minor === 22 && patch < 2)))) { + console.error("Error: Raycast checks require Node.js 22.22.2 or newer."); + process.exit(1); + } + ' + ( + cd "$REPO_ROOT/raycast-theme" + npm ci + npm run lint + npm run build + [ -f dist/switch-theme.js ] + [ -f dist/package.json ] + [ -f dist/assets/command-icon.png ] + [ ! -e dist/node_modules ] + ) +} + +main() { + require_command git + require_command jq + require_command node + require_command npm + require_command python3 + require_command strings + require_command zsh + + check_shell_syntax + check_json_and_plists + + section 'Validating themes' + bash "$REPO_ROOT/tests/theme_validation_test.sh" + + section 'Checking for personal hard-coded values' + bash "$REPO_ROOT/tests/no_personal_data_test.sh" + + section 'Testing the installer' + bash "$REPO_ROOT/tests/installer_test.sh" + + section 'Testing SketchyBar metrics' + bash "$REPO_ROOT/tests/sketchybar_metrics_test.sh" + + section 'Testing SketchyBar battery state' + bash "$REPO_ROOT/tests/sketchybar_battery_test.sh" + + section 'Testing SketchyBar network counters' + bash "$REPO_ROOT/tests/sketchybar_network_test.sh" + + section 'Testing SketchyBar Spaces' + bash "$REPO_ROOT/tests/sketchybar_spaces_test.sh" + + section 'Testing yabai helpers' + bash "$REPO_ROOT/tests/yabai_helpers_test.sh" + + check_raycast + + printf '\nAll repository checks passed.\n' +} + +main "$@" diff --git a/sketchybar/colors.sh b/sketchybar/colors.sh index f8a1b27..e14cf93 100755 --- a/sketchybar/colors.sh +++ b/sketchybar/colors.sh @@ -10,44 +10,8 @@ fi export WARNING_COLOR=0xffFFD60A export DANGER_COLOR=0xffff453a -# -- Teal Scheme -- -export BAR_COLOR="${BAR_COLOR:-0xff001f30}" -export ITEM_BG_COLOR="${ITEM_BG_COLOR:-0xff003547}" +# These defaults keep the bar neutral until a Macarchy theme is applied. +export BAR_COLOR="${BAR_COLOR:-0x00000000}" export ACCENT_COLOR="${ACCENT_COLOR:-0xff2cf9ed}" export TEXT_COLOR="${TEXT_COLOR:-0xffffffff}" export WHITE="$TEXT_COLOR" - -# -- Gray Scheme -- -# export BAR_COLOR=0xff101314 -# export ITEM_BG_COLOR=0xff353c3f -# export ACCENT_COLOR=0xffffffff - -# -- Purple Scheme -- -# export BAR_COLOR=0xff140c42 -# export ITEM_BG_COLOR=0xff2b1c84 -# export ACCENT_COLOR=0xffeb46f9 - -# -- Red Scheme --- -# export BAR_COLOR=0xff23090e -# export ITEM_BG_COLOR=0xff591221 -# export ACCENT_COLOR=0xffff2453 - -# -- Blue Scheme --- -# export BAR_COLOR=0xff021254 -# export ITEM_BG_COLOR=0xff093aa8 -# export ACCENT_COLOR=0xff15bdf9 - -# -- Green Scheme -- -# export BAR_COLOR=0xff003315 -# export ITEM_BG_COLOR=0xff008c39 -# export ACCENT_COLOR=0xff1dfca1 - -# -- Orange Scheme -- -# export BAR_COLOR=0xff381c02 -# export ITEM_BG_COLOR=0xff99440a -# export ACCENT_COLOR=0xfff97716 - -# -- Yellow Scheme -- -# export BAR_COLOR=0xff2d2b02 -# export ITEM_BG_COLOR=0xff8e7e0a -# export ACCENT_COLOR=0xfff7fc17 diff --git a/sketchybar/icons/cpu.png b/sketchybar/icons/cpu.png deleted file mode 100644 index 050d340..0000000 Binary files a/sketchybar/icons/cpu.png and /dev/null differ diff --git a/sketchybar/icons/gpu-rotated-270.png b/sketchybar/icons/gpu-rotated-270.png deleted file mode 100644 index e61a5d2..0000000 Binary files a/sketchybar/icons/gpu-rotated-270.png and /dev/null differ diff --git a/sketchybar/icons/network-arrows.png b/sketchybar/icons/network-arrows.png deleted file mode 100644 index 62ef964..0000000 Binary files a/sketchybar/icons/network-arrows.png and /dev/null differ diff --git a/sketchybar/icons/ram-rotated-270.png b/sketchybar/icons/ram-rotated-270.png deleted file mode 100644 index 2d0d61d..0000000 Binary files a/sketchybar/icons/ram-rotated-270.png and /dev/null differ diff --git a/sketchybar/items/battery.sh b/sketchybar/items/battery.sh index 0483d3f..0ff1c6b 100755 --- a/sketchybar/items/battery.sh +++ b/sketchybar/items/battery.sh @@ -1,9 +1,10 @@ -#!/bin/bash +#!/usr/bin/env bash -sketchybar --add item battery right \ - --set battery background.border_width=0 \ - padding_left=0 \ - padding_right=0 \ - update_freq=5 \ - script="$PLUGIN_DIR/battery.sh" \ - --subscribe battery system_woke power_source_change +sketchybar --add item battery right +sketchybar --set battery \ + background.border_width=0 \ + padding_left=0 \ + padding_right=0 \ + update_freq=5 \ + script="$PLUGIN_DIR/battery.sh" +sketchybar --subscribe battery system_woke power_source_change diff --git a/sketchybar/items/calendar.sh b/sketchybar/items/calendar.sh index 9a27048..2e8083c 100755 --- a/sketchybar/items/calendar.sh +++ b/sketchybar/items/calendar.sh @@ -1,8 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash -sketchybar --add item calendar right \ - --set calendar icon=􀧞 \ - padding_left=0 \ - background.border_width=0 \ - update_freq=30 \ - script="$PLUGIN_DIR/calendar.sh" +sketchybar --add item calendar right +sketchybar --set calendar \ + icon=􀧞 \ + padding_left=0 \ + background.border_width=0 \ + update_freq=30 \ + script="$PLUGIN_DIR/calendar.sh" diff --git a/sketchybar/items/system_metrics.sh b/sketchybar/items/system_metrics.sh index cdc3aa9..3639b79 100644 --- a/sketchybar/items/system_metrics.sh +++ b/sketchybar/items/system_metrics.sh @@ -4,9 +4,10 @@ METRIC_FONT="Menlo:Bold:10.0" BIG_ICON_FONT="SF Pro:Semibold:15.0" COLOR="${TEXT_COLOR:-0xffffffff}" BOX_BORDER="0x55ffffff" +MACMON_BIN="$(command -v macmon 2>/dev/null || true)" ICONS_DIR="${CONFIG_DIR:-$HOME/.config/sketchybar}/icons" -# Fixed-width boxes sized around the longest capped metric strings. +# Fixed-width boxes sized around common metric strings. CHAR_W=5 VAL_CHARS=5 # "99.9%", "15.8G", "45.0°" NET_CHARS=8 # "99.9MB/s" @@ -27,8 +28,8 @@ NETWORK_ICON_SCALE=0.13 BOX_GAP=11 set_text_item() { - local name="$1" text_w="$2" pad_left="$3" freq="$4" script="$5" - local icon_y="${6:-6}" label_y="${7:--6}" + local name="$1" text_w="$2" pad_left="$3" + local icon_y="${4:-6}" label_y="${5:--6}" sketchybar --set "$name.text" \ background.border_width=0 \ @@ -49,9 +50,7 @@ set_text_item() { label.align=left \ label.y_offset="$label_y" \ label.padding_left="-$((pad_left + 5))" \ - label.padding_right=0 \ - update_freq="$freq" \ - script="$script" + label.padding_right=0 } set_bracket_box() { @@ -79,54 +78,108 @@ add_spacer() { padding_right=0 } -stacked_metric_image() { - local name="$1" image="$2" item_w="$3" text_w="$4" freq="$5" script="$6" - local icon_w="${7:-$METRIC_ICON_W}" +stacked_metric_icon() { + local name="$1" glyph="$2" image="$3" item_w="$4" text_w="$5" + local icon_w="${6:-$METRIC_ICON_W}" sketchybar --add item "$name.text" right - set_text_item "$name" "$item_w" "$text_w" "$freq" "$script" + set_text_item "$name" "$item_w" "$text_w" sketchybar --add item "$name.icon" right \ --set "$name.icon" \ background.border_width=0 \ - background.drawing=on \ - background.image="$image" \ - background.image.scale="$METRIC_ICON_SCALE" \ + background.drawing=off \ background.color=0x00000000 \ padding_left=0 \ padding_right=0 \ width="$icon_w" \ - icon.drawing=off \ label.drawing=off \ --add bracket "$name.bracket" "$name.icon" "$name.text" + + if [ -r "$image" ]; then + sketchybar --set "$name.icon" \ + background.drawing=on \ + background.image="$image" \ + background.image.scale="$METRIC_ICON_SCALE" \ + icon.drawing=off + else + sketchybar --set "$name.icon" \ + background.drawing=off \ + icon.drawing=on \ + icon="$glyph" \ + icon.font="$METRIC_FONT" \ + icon.color="$COLOR" \ + icon.padding_left=4 \ + icon.padding_right=0 + fi + set_bracket_box "$name.bracket" } stacked_network() { sketchybar --add item network.text right - set_text_item network "$NET_W" "$NET_TEXT_W" 5 "$PLUGIN_DIR/network_speed.sh" 5 -4 - sketchybar --set network.text icon.padding_left=0 + set_text_item network "$NET_W" "$NET_TEXT_W" 5 -4 + sketchybar --set network.text \ + icon.padding_left=0 \ + update_freq=5 \ + script="$PLUGIN_DIR/network_speed.sh" sketchybar --add item network.icon right \ --set network.icon \ background.border_width=0 \ background.color=0x00000000 \ - background.drawing=on \ - background.image="$ICONS_DIR/network-arrows.png" \ - background.image.scale="$NETWORK_ICON_SCALE" \ + background.drawing=off \ padding_left=0 \ padding_right=0 \ width="$NETWORK_ICON_W" \ - icon.drawing=off \ label.drawing=off \ --add bracket network.bracket network.icon network.text + + if [ -r "$ICONS_DIR/network-arrows.png" ]; then + sketchybar --set network.icon \ + background.drawing=on \ + background.image="$ICONS_DIR/network-arrows.png" \ + background.image.scale="$NETWORK_ICON_SCALE" \ + icon.drawing=off + else + sketchybar --set network.icon \ + background.drawing=off \ + icon.drawing=on \ + icon="NET" \ + icon.font="$METRIC_FONT" \ + icon.color="$COLOR" \ + icon.padding_left=2 \ + icon.padding_right=0 + fi + set_bracket_box network.bracket } -# Right-positioned items render in reverse add order. -# Target LTR within this group: network | ram | gpu | cpu -stacked_metric_image cpu "$ICONS_DIR/cpu.png" "$VAL_W" "$VAL_TEXT_W" 5 "$PLUGIN_DIR/system_metrics.sh" -add_spacer gap.cpu -stacked_metric_image gpu "$ICONS_DIR/gpu-rotated-270.png" "$VAL_W" "$VAL_TEXT_W" 5 "$PLUGIN_DIR/system_metrics.sh" -add_spacer gap.gpu -stacked_metric_image ram "$ICONS_DIR/ram-rotated-270.png" "$VAL_W" "$VAL_TEXT_W" 10 "$PLUGIN_DIR/system_metrics.sh" "$RAM_ICON_W" -add_spacer gap.ram +add_system_metrics_updater() { + # macmon's pipe mode keeps metrics local instead of opening an HTTP port. + "$PLUGIN_DIR/macmon_stream.sh" --start + + # One hidden item fetches macmon once and updates all three visible metrics. + sketchybar --add item system_metrics_updater right \ + --set system_metrics_updater \ + drawing=off \ + width=0 \ + updates=on \ + update_freq=5 \ + script="$PLUGIN_DIR/system_metrics.sh" \ + --subscribe system_metrics_updater system_woke +} + +# Right-positioned items render in reverse add order. The hardware tiles are +# optional because macmon supports Apple Silicon only; network speed works on +# every supported Mac. +if [ -n "$MACMON_BIN" ]; then + stacked_metric_icon cpu CPU "$ICONS_DIR/cpu.png" "$VAL_W" "$VAL_TEXT_W" + add_spacer gap.cpu + stacked_metric_icon gpu GPU "$ICONS_DIR/gpu-rotated-270.png" "$VAL_W" "$VAL_TEXT_W" + add_spacer gap.gpu + stacked_metric_icon ram RAM "$ICONS_DIR/ram-rotated-270.png" "$VAL_W" "$VAL_TEXT_W" "$RAM_ICON_W" + add_spacer gap.ram +fi stacked_network +if [ -n "$MACMON_BIN" ]; then + add_system_metrics_updater +fi diff --git a/sketchybar/plugins/battery.sh b/sketchybar/plugins/battery.sh index d28adc6..584247c 100755 --- a/sketchybar/plugins/battery.sh +++ b/sketchybar/plugins/battery.sh @@ -1,10 +1,10 @@ -#!/bin/sh +#!/usr/bin/env bash # shellcheck source=/dev/null source "${CONFIG_DIR:-$HOME/.config/sketchybar}/colors.sh" BATTERY_INFO="$(pmset -g batt)" -PERCENTAGE="$(printf '%s\n' "$BATTERY_INFO" | grep -Eo "\d+%" | cut -d% -f1)" +PERCENTAGE="$(printf '%s\n' "$BATTERY_INFO" | grep -Eo "[0-9]+%" | cut -d% -f1)" POWER_SOURCE="$(printf '%s\n' "$BATTERY_INFO" | awk -F"'" '/Now drawing from/ {print $2; exit}')" BATTERY_STATUS="$(printf '%s\n' "$BATTERY_INFO" | awk -F';' '/InternalBattery/ { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit }')" @@ -22,7 +22,7 @@ LOW_POWER_MODE="$( pmset -g custom 2>/dev/null | awk -v target="$EFFECTIVE_POWER_SOURCE" ' $0 == target ":" { in_section=1; next } in_section && /^[^[:space:]]/ { in_section=0 } - in_section && $1 == "lowpowermode" { print $2; exit } + in_section && ($1 == "lowpowermode" || $1 == "powermode") { print $2; exit } ' )" @@ -31,12 +31,27 @@ if [ "$LOW_POWER_MODE" = "" ]; then for plist in /Library/Preferences/com.apple.PowerManagement.*.plist; do [ -f "$plist" ] || continue defaults read "${plist%.plist}" "$EFFECTIVE_POWER_SOURCE" 2>/dev/null | - awk '$1 == "LowPowerMode" { gsub(";", "", $3); print $3; exit }' && + awk ' + $1 == "LowPowerMode" { + gsub(";", "", $3) + print $3 + found = 1 + exit + } + END { if (!found) exit 1 } + ' && break done )" fi +# Older macOS releases use 1 for enabled. Newer releases may store a different +# positive mode value, so normalise the setting instead of matching one number. +case "$LOW_POWER_MODE" in + ''|0|*[!0-9]*) LOW_POWER_MODE=0 ;; + *) LOW_POWER_MODE=1 ;; +esac + if [ "$PERCENTAGE" = "" ]; then exit 0 fi @@ -59,10 +74,10 @@ fi ICON_COLOR="${WHITE:-0xffffffff}" -if [ "${LOW_POWER_MODE:-0}" = "1" ]; then - ICON_COLOR="${WARNING_COLOR:-0xffFFD60A}" -elif [ "$PERCENTAGE" -lt 7 ]; then +if [ "$PERCENTAGE" -lt 7 ]; then ICON_COLOR="${DANGER_COLOR:-0xffff453a}" +elif [ "$LOW_POWER_MODE" = "1" ]; then + ICON_COLOR="${WARNING_COLOR:-0xffFFD60A}" fi sketchybar --set "$NAME" icon="$ICON" icon.color="$ICON_COLOR" label="${PERCENTAGE}%" diff --git a/sketchybar/plugins/calendar.sh b/sketchybar/plugins/calendar.sh index 479c8a7..1d6b48d 100755 --- a/sketchybar/plugins/calendar.sh +++ b/sketchybar/plugins/calendar.sh @@ -1,3 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash -sketchybar --set "$NAME" label="$(date +'%a %d %b %I:%M %p')" +calendar_label="$(LC_ALL=C date +'%a %d %b %I:%M %p')" +sketchybar --set "${NAME:-calendar}" label="$calendar_label" diff --git a/sketchybar/plugins/icon_map.sh b/sketchybar/plugins/icon_map.sh index f03c97a..b63c152 100755 --- a/sketchybar/plugins/icon_map.sh +++ b/sketchybar/plugins/icon_map.sh @@ -483,7 +483,7 @@ function __icon_map() { "Finder" | "访达" | "Bloom") icon_result=":finder:" ;; - "Firefox") + "Firefox" | "firefox") icon_result=":firefox:" ;; "Firefox Developer Edition" | "Firefox Nightly") @@ -546,7 +546,7 @@ function __icon_map() { "Gemini" | "Google Gemini") icon_result=":gemini:" ;; - "Ghostty") + "Ghostty" | "ghostty") icon_result=":ghostty:" ;; "GIMP") diff --git a/sketchybar/plugins/macmon_stream.sh b/sketchybar/plugins/macmon_stream.sh new file mode 100755 index 0000000..0539cc7 --- /dev/null +++ b/sketchybar/plugins/macmon_stream.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -u + +CONFIG_ROOT="${CONFIG_DIR:-$HOME/.config/sketchybar}" +STATE_DIR="${SKETCHYBAR_STATE_DIR:-$CONFIG_ROOT/.state}" +DATA_FILE="${MACMON_DATA_FILE:-$STATE_DIR/macmon.json}" +PID_FILE="$STATE_DIR/macmon-stream.pid" +LOCK_DIR="$STATE_DIR/macmon-stream.lock" +MACMON_BIN="$(command -v "${MACMON_BIN:-macmon}" 2>/dev/null || true)" +INTERVAL="${MACMON_INTERVAL:-1000}" + +stream_is_running() { + local pid="" + + [ -r "$PID_FILE" ] && read -r pid < "$PID_FILE" + case "$pid" in + ''|*[!0-9]*) return 1 ;; + esac + kill -0 "$pid" 2>/dev/null || return 1 + ps -p "$pid" -o command= 2>/dev/null | grep -F 'macmon_stream.sh --run' >/dev/null +} + +start_stream() { + local attempt + + [ -n "$MACMON_BIN" ] || exit 0 + mkdir -p "$STATE_DIR" + + stream_is_running && exit 0 + rm -f "$PID_FILE" + if ! mkdir "$LOCK_DIR" 2>/dev/null; then + # Another reload may have acquired the lock just before its runner wrote + # the PID file. Give it a moment before treating the lock as stale. + for attempt in 1 2 3 4 5; do + sleep 0.05 + stream_is_running && exit 0 + done + rmdir "$LOCK_DIR" 2>/dev/null || exit 0 + mkdir "$LOCK_DIR" 2>/dev/null || exit 0 + fi + + nohup /bin/bash "$0" --run >/dev/null 2>&1 & +} + +run_stream() { + local fifo="$STATE_DIR/macmon-stream.$$.fifo" + local temporary="$DATA_FILE.$$" + local macmon_pid="" + local line="" + + [ -n "$MACMON_BIN" ] || exit 0 + mkdir -p "$STATE_DIR" + printf '%s\n' "$$" > "$PID_FILE.$$.tmp" + mv "$PID_FILE.$$.tmp" "$PID_FILE" + rm -f "$fifo" + mkfifo "$fifo" || exit 1 + + cleanup() { + [ -n "$macmon_pid" ] && kill "$macmon_pid" 2>/dev/null || true + rm -f "$fifo" "$temporary" + if [ "$(cat "$PID_FILE" 2>/dev/null)" = "$$" ]; then + rm -f "$PID_FILE" + rmdir "$LOCK_DIR" 2>/dev/null || true + fi + } + trap cleanup EXIT INT TERM + + "$MACMON_BIN" pipe --samples 0 --interval "$INTERVAL" > "$fifo" 2>/dev/null & + macmon_pid=$! + + while IFS= read -r line; do + case "$line" in + \{*\}) + printf '%s\n' "$line" > "$temporary" + mv "$temporary" "$DATA_FILE" + ;; + esac + + # The sampler belongs to SketchyBar and should not outlive it. + pgrep -x sketchybar >/dev/null 2>&1 || break + done < "$fifo" +} + +case "${1:-}" in + --start) start_stream ;; + --run) run_stream ;; + *) printf 'usage: %s --start\n' "$0" >&2; exit 1 ;; +esac diff --git a/sketchybar/plugins/network_speed.sh b/sketchybar/plugins/network_speed.sh index b9de423..dd8acd9 100755 --- a/sketchybar/plugins/network_speed.sh +++ b/sketchybar/plugins/network_speed.sh @@ -3,6 +3,8 @@ CONFIG_ROOT="${CONFIG_DIR:-$HOME/.config/sketchybar}" STATE_DIR="$CONFIG_ROOT/.state" STATE_FILE="$STATE_DIR/network_speed" +SKETCHYBAR_BIN="$(command -v "${SKETCHYBAR_BIN:-sketchybar}" 2>/dev/null || true)" +[ -n "$SKETCHYBAR_BIN" ] || exit 0 mkdir -p "$STATE_DIR" @@ -17,16 +19,26 @@ active_interface() { iface="$(ifconfig 2>/dev/null | awk ' /^[a-zA-Z0-9]+:/ { + if (current != "" && current != "lo0" && active == 1 && has_inet == 1) { + print current + found = 1 + exit + } sub(":", "", $1) current = $1 active = 0 + has_inet = 0 } /status: active/ { active = 1 } - /inet / && current != "lo0" && active == 1 { - print current - exit + /inet / { + has_inet = 1 + } + END { + if (!found && current != "" && current != "lo0" && active == 1 && has_inet == 1) { + print current + } } ')" @@ -38,26 +50,27 @@ read_counters() { netstat -ibn -I "$iface" 2>/dev/null | awk -v iface="$iface" ' $1 == iface && $7 ~ /^[0-9]+$/ && $10 ~ /^[0-9]+$/ { - rx += $7 - tx += $10 + # macOS repeats the interface totals for each address family. Taking the + # largest row avoids counting the same bytes two or three times. + if (!found || $7 > rx) rx = $7 + if (!found || $10 > tx) tx = $10 + found = 1 } END { - if (rx == "") rx = 0 - if (tx == "") tx = 0 + if (!found) rx = tx = 0 print rx, tx } ' } format_speed() { - /usr/bin/python3 -c ' -import sys - -speed = float(sys.argv[1]) / 1048576 -if speed >= 100: - speed = 99.9 -print(f"{speed:.1f}MB/s") -' "${1:-0}" + awk -v bytes="${1:-0}" 'BEGIN { + if (bytes !~ /^[0-9]+([.][0-9]+)?$/) bytes = 0 + speed = bytes / 1048576 + if (speed >= 1024) printf "%.1fGB/s", speed / 1024 + else if (speed >= 100) printf "%.0fMB/s", speed + else printf "%.1fMB/s", speed + }' } now="$(date +%s)" @@ -76,7 +89,11 @@ fi down_bps=0 up_bps=0 -if [ "$iface" = "$prev_iface" ] && [ -n "$prev_time" ] && [ "$now" -gt "$prev_time" ]; then +if [ "$iface" = "$prev_iface" ] \ + && [[ "$prev_time" =~ ^[0-9]+$ ]] \ + && [[ "$prev_rx" =~ ^[0-9]+$ ]] \ + && [[ "$prev_tx" =~ ^[0-9]+$ ]] \ + && [ "$now" -gt "$prev_time" ]; then elapsed=$((now - prev_time)) if [ "$rx" -ge "${prev_rx:-0}" ] && [ "$tx" -ge "${prev_tx:-0}" ]; then down_bps=$(((rx - prev_rx) / elapsed)) @@ -84,8 +101,12 @@ if [ "$iface" = "$prev_iface" ] && [ -n "$prev_time" ] && [ "$now" -gt "$prev_ti fi fi -printf '%s %s %s %s\n' "$now" "$iface" "$rx" "$tx" > "$STATE_FILE" +tmp_file="$STATE_FILE.$$" +trap 'rm -f "$tmp_file"' EXIT +printf '%s %s %s %s\n' "$now" "$iface" "$rx" "$tx" > "$tmp_file" +mv "$tmp_file" "$STATE_FILE" +trap - EXIT -sketchybar --set network.text \ +"$SKETCHYBAR_BIN" --set network.text \ icon="$(format_speed "$up_bps")" \ label="$(format_speed "$down_bps")" diff --git a/sketchybar/plugins/space.sh b/sketchybar/plugins/space.sh index 92864a0..640b38d 100755 --- a/sketchybar/plugins/space.sh +++ b/sketchybar/plugins/space.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash source "$CONFIG_DIR/colors.sh" diff --git a/sketchybar/plugins/space_windows.sh b/sketchybar/plugins/space_windows.sh index 309a1dd..97c9051 100755 --- a/sketchybar/plugins/space_windows.sh +++ b/sketchybar/plugins/space_windows.sh @@ -1,11 +1,11 @@ #!/bin/bash CONFIG_ROOT="${CONFIG_DIR:-$HOME/.config/sketchybar}" -YABAI_BIN="${YABAI_BIN:-$(command -v yabai 2>/dev/null || true)}" -[ -x "$YABAI_BIN" ] || YABAI_BIN="/opt/homebrew/bin/yabai" -STATE_DIR="$CONFIG_ROOT/.state" +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +STATE_DIR="${SKETCHYBAR_STATE_DIR:-$CONFIG_ROOT/.state}" SPACE_ITEM_STATE="$STATE_DIR/space_items" -MAX_FALLBACK_SPACES="${MAX_FALLBACK_SPACES:-10}" +CURRENT_SPACES="" # shellcheck source=/dev/null source "$CONFIG_ROOT/colors.sh" @@ -14,10 +14,11 @@ source "$CONFIG_ROOT/colors.sh" source "$CONFIG_ROOT/plugins/icon_map.sh" space_ids() { - local ids="" + local ids="" spaces_json="" display=1 display_ids="" - if [ -x "$YABAI_BIN" ]; then - ids="$("$YABAI_BIN" -m query --spaces 2>/dev/null | jq -r ' + if [ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ]; then + spaces_json="$("$YABAI_BIN" -m query --spaces 2>/dev/null || true)" + ids="$(printf '%s' "$spaces_json" | "$JQ_BIN" -r ' sort_by(.index) | .[] | select(.index != null) @@ -30,11 +31,28 @@ space_ids() { return 0 fi - local space=1 - while [ "$space" -le "$MAX_FALLBACK_SPACES" ]; do - printf '%s\n' "$space" - space=$((space + 1)) + [ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || return 1 + + # yabai can occasionally truncate the unscoped array response while its + # display state settles. Per-display queries use a separate code path, so + # collect those as a safe fallback. Space indexes are global across displays. + while [ "$display" -le "${MAX_YABAI_DISPLAYS:-16}" ]; do + spaces_json="$("$YABAI_BIN" -m query --spaces --display "$display" 2>/dev/null)" || break + display_ids="$(printf '%s' "$spaces_json" | "$JQ_BIN" -r ' + .[] + | select(.index != null) + | .index + ' 2>/dev/null)" || break + ids="${ids}${ids:+$'\n'}${display_ids}" + display=$((display + 1)) done + + if [ -n "$ids" ]; then + printf '%s\n' "$ids" | awk 'NF' | LC_ALL=C sort -nu + return 0 + fi + + return 1 } list_contains() { @@ -50,6 +68,11 @@ list_contains() { add_space_item() { local sid="$1" + local click_action="" + + if [ -n "$YABAI_BIN" ]; then + printf -v click_action '%q -m space --focus %q' "$YABAI_BIN" "$sid" + fi sketchybar --add space "space.$sid" left >/dev/null 2>&1 || true @@ -69,27 +92,30 @@ add_space_item() { background.border_color=0x00000000 \ background.color=0x00000000 \ script="$CONFIG_ROOT/plugins/space.sh" \ - click_script="$YABAI_BIN -m space --focus $sid" + click_script="$click_action" } reconcile_spaces() { local force="${1:-}" - local current_spaces desired_spaces space drawing + local current_spaces desired_spaces known_spaces space drawing state_tmp mkdir -p "$STATE_DIR" - current_spaces="$(space_ids)" - desired_spaces="$(space_ids | awk -v max="$MAX_FALLBACK_SPACES" ' - { seen[$0] = 1 } - END { - for (i = 1; i <= max; i++) seen[i] = 1 - for (space in seen) print space - } - ' | LC_ALL=C sort -n)" + known_spaces="$(cat "$SPACE_ITEM_STATE" 2>/dev/null || true)" + + # Keep the existing bar unchanged during a yabai restart. When yabai is not + # installed, the rest of SketchyBar still works without ten dead Space items. + if ! current_spaces="$(space_ids)"; then + CURRENT_SPACES="" + return 0 + fi + + CURRENT_SPACES="$current_spaces" + desired_spaces="$current_spaces" while IFS= read -r space; do [ -n "$space" ] || continue - if [ "$force" = "--force" ]; then + if [ "$force" = "--force" ] || ! printf '%s\n' "$known_spaces" | list_contains "$space"; then add_space_item "$space" fi @@ -101,12 +127,17 @@ reconcile_spaces() { sketchybar --set "space.$space" drawing="$drawing" >/dev/null 2>&1 || true done <<< "$desired_spaces" + # Hide items left behind when a high-numbered Space is removed. while IFS= read -r space; do [ -n "$space" ] || continue - refresh_space "$space" - done <<< "$current_spaces" + if ! printf '%s\n' "$desired_spaces" | list_contains "$space"; then + sketchybar --set "space.$space" drawing=off >/dev/null 2>&1 || true + fi + done <<< "$known_spaces" - printf '%s\n' "$current_spaces" > "$SPACE_ITEM_STATE" + state_tmp="$SPACE_ITEM_STATE.$$" + printf '%s\n' "$desired_spaces" > "$state_tmp" + mv "$state_tmp" "$SPACE_ITEM_STATE" } map_app_icon() { @@ -127,7 +158,7 @@ render_space() { icon_strip+=" $(map_app_icon "$app")" done <<< "$apps" else - icon_strip=" —" + icon_strip=" -" fi sketchybar --set "space.$space" label="$icon_strip" @@ -137,19 +168,21 @@ apps_for_space() { local space="$1" local space_json display_id - [ -x "$YABAI_BIN" ] || return 1 + [ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || return 1 space_json="$("$YABAI_BIN" -m query --spaces --space "$space" 2>/dev/null)" || return 1 - display_id="$(printf '%s' "$space_json" | jq -r '.display')" + display_id="$(printf '%s' "$space_json" | "$JQ_BIN" -r '.display')" - "$YABAI_BIN" -m query --windows --space "$space" 2>/dev/null | jq -r --argjson display "$display_id" --argjson space "$space" ' + "$YABAI_BIN" -m query --windows --space "$space" 2>/dev/null | "$JQ_BIN" -r --argjson display "$display_id" --argjson space "$space" ' map( select( .app and .space == $space and .display == $display - and .role == "AXWindow" - and .["has-ax-reference"] == true + and ( + (.role == "AXWindow" and .["has-ax-reference"] == true) + or (.["root-window"] == true and ((.role // "") == "")) + ) and .["is-minimized"] == false and .["is-hidden"] == false and (."is-sticky" // false) == false @@ -175,30 +208,40 @@ refresh_space() { render_space "$space" "$apps" } +refresh_current_spaces() { + local space + + while IFS= read -r space; do + [ -n "$space" ] || continue + refresh_space "$space" + done <<< "$CURRENT_SPACES" +} + case "${SENDER:-}" in space_windows_change) reconcile_spaces - if [ -n "${INFO:-}" ]; then - changed_space="$(printf '%s' "$INFO" | jq -r '.space // empty')" + if [ -n "${INFO:-}" ] && [ -n "$JQ_BIN" ]; then + changed_space="$(printf '%s' "$INFO" | "$JQ_BIN" -r '.space // empty' 2>/dev/null)" - if printf '%s\n' "$(space_ids)" | list_contains "$changed_space"; then + if printf '%s\n' "$CURRENT_SPACES" | list_contains "$changed_space"; then refresh_space "$changed_space" else - while IFS= read -r space; do - [ -n "$space" ] || continue - refresh_space "$space" - done <<< "$(space_ids)" + refresh_current_spaces fi - exit 0 + else + refresh_current_spaces fi + + exit 0 ;; esac case "${1:-}" in --reconcile) reconcile_spaces --force + refresh_current_spaces exit 0 ;; --space-ids) @@ -208,9 +251,4 @@ case "${1:-}" in esac reconcile_spaces - -while IFS= read -r space -do - [ -n "$space" ] || continue - refresh_space "$space" -done <<< "$(space_ids)" +refresh_current_spaces diff --git a/sketchybar/plugins/system_metrics.sh b/sketchybar/plugins/system_metrics.sh index 1112886..3634ceb 100755 --- a/sketchybar/plugins/system_metrics.sh +++ b/sketchybar/plugins/system_metrics.sh @@ -1,101 +1,108 @@ -#!/bin/bash +#!/usr/bin/env bash CONFIG_ROOT="${CONFIG_DIR:-$HOME/.config/sketchybar}" -STATE_DIR="$CONFIG_ROOT/.state" -MACMON_URL="${MACMON_URL:-http://localhost:9090/json}" - -mkdir -p "$STATE_DIR" +MACMON_DATA_FILE="${MACMON_DATA_FILE:-$CONFIG_ROOT/.state/macmon.json}" +MACMON_URL="${MACMON_URL:-}" +SKETCHYBAR_BIN="$(command -v "${SKETCHYBAR_BIN:-sketchybar}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +CURL_BIN="$(command -v "${CURL_BIN:-curl}" 2>/dev/null || true)" +MACMON_STREAM="$CONFIG_ROOT/plugins/macmon_stream.sh" +[ -n "$SKETCHYBAR_BIN" ] || exit 0 read_macmon() { - curl --max-time 1 -fsS "$MACMON_URL" 2>/dev/null + local modified now + + if [ -n "$MACMON_URL" ]; then + [ -n "$CURL_BIN" ] || return 1 + "$CURL_BIN" --max-time 1 -fsS "$MACMON_URL" 2>/dev/null + elif [ -r "$MACMON_DATA_FILE" ]; then + # BSD and GNU stat use different flags. Keep the probes separate because + # GNU stat can print filesystem details before rejecting BSD's -f format. + if ! modified="$(stat -f %m "$MACMON_DATA_FILE" 2>/dev/null)"; then + modified="$(stat -c %Y "$MACMON_DATA_FILE" 2>/dev/null || true)" + fi + now="$(date +%s)" + case "$modified" in + ''|*[!0-9]*) return 1 ;; + esac + [ "$((now - modified))" -le 15 ] || return 1 + cat "$MACMON_DATA_FILE" + else + return 1 + fi } -format_macmon_rows() { - /usr/bin/python3 -c ' -import json -import os -import re -import sys - -try: - d = json.load(sys.stdin) -except Exception: - d = {} - -def num(path): - cur = d - try: - for part in path: - cur = cur[part] - return float(cur) - except Exception: - return None - -def _fmt(n, suffix, fixed=False): - if n >= 100: - n = 99.9 - return f"{n:4.1f}{suffix}" if fixed else f"{n:.1f}{suffix}" - -def pct(value): - return "--.-%" if value is None else _fmt(value * 100, "%") - -def temp(value): - return "--.-°" if value is None else _fmt(value, "°", True) - -def gib(value): - return "--.-G" if value is None else _fmt(value / 1073741824, "G", True) - -def pressure_memory(): - output = os.environ.get("MEMORY_PRESSURE_OUTPUT", "") - total_match = re.search(r"The system has (\d+)", output) - free_match = re.search(r"System-wide memory free percentage:\s*(\d+)%", output) - if not total_match or not free_match: - return None, None - - total = float(total_match.group(1)) - pressure_pct = max(0.0, min(100.0, 100.0 - float(free_match.group(1)))) - return pressure_pct / 100.0, total * pressure_pct / 100.0 - -ram_total = num(("memory", "ram_total")) -ram_pct, ram_used = pressure_memory() -if ram_pct is None: - ram_used = num(("memory", "ram_usage")) - ram_pct = None if ram_total is None or ram_used is None or ram_total == 0 else ram_used / ram_total - -rows = { - "cpu_top": pct(num(("cpu_usage_pct",))), - "cpu_bottom": temp(num(("temp", "cpu_temp_avg"))), - "gpu_top": pct(num(("gpu_usage", 1))), - "gpu_bottom": temp(num(("temp", "gpu_temp_avg"))), - "ram_top": pct(ram_pct), - "ram_bottom": gib(ram_used), +format_scaled() { + local value="$1" scale="$2" suffix="$3" placeholder="$4" fixed_width="${5:-false}" + + awk -v value="$value" -v scale="$scale" -v suffix="$suffix" \ + -v placeholder="$placeholder" -v fixed_width="$fixed_width" 'BEGIN { + if (value !~ /^-?[0-9]+([.][0-9]+)?$/) { + print placeholder + exit + } + + number = value * scale + if (number >= 99.95) printf "%.0f%s", number, suffix + else if (fixed_width == "true") printf "%4.1f%s", number, suffix + else printf "%.1f%s", number, suffix + }' } -for key, value in rows.items(): - print(f"{key}={value}") -' -} +format_ratio_percent() { + local used="$1" total="$2" -DATA="$(read_macmon)" -MEMORY_PRESSURE_OUTPUT="$(memory_pressure 2>/dev/null)" -METRICS="$(printf '%s' "$DATA" | MEMORY_PRESSURE_OUTPUT="$MEMORY_PRESSURE_OUTPUT" format_macmon_rows)" + awk -v used="$used" -v total="$total" 'BEGIN { + if (used !~ /^[0-9]+([.][0-9]+)?$/ || total !~ /^[0-9]+([.][0-9]+)?$/ || total <= 0) { + print "--.-%" + exit + } -get_row() { - local key="$1" - printf '%s\n' "$METRICS" | awk -F= -v key="$key" '$1 == key { print substr($0, length(key) + 2); exit }' + percentage = used * 100 / total + if (percentage >= 99.95) printf "%.0f%%", percentage + else printf "%.1f%%", percentage + }' } -ARGS=() - -if [ -n "$DATA" ]; then - ARGS+=( - --set cpu.text icon="$(get_row cpu_top)" label="$(get_row cpu_bottom)" - --set gpu.text icon="$(get_row gpu_top)" label="$(get_row gpu_bottom)" - ) +cpu_usage="NA" +cpu_temp="NA" +gpu_usage="NA" +gpu_temp="NA" +ram_used="NA" +ram_total="NA" + +data="$(read_macmon || true)" +if [ -z "$data" ] && [ -z "$MACMON_URL" ] && [ -x "$MACMON_STREAM" ]; then + # The sampler can exit during sleep or after a macmon failure. Starting it is + # lock-protected and cheap when an existing worker is still healthy. + "$MACMON_STREAM" --start >/dev/null 2>&1 || true fi - -ARGS+=(--set ram.text icon="$(get_row ram_top)" label="$(get_row ram_bottom)") - -if [ "${#ARGS[@]}" -gt 0 ]; then - sketchybar "${ARGS[@]}" +if [ -n "$data" ] && [ -n "$JQ_BIN" ]; then + values="$(printf '%s' "$data" | "$JQ_BIN" -er ' + [ + ((try .cpu_usage_pct catch null) // "NA"), + ((try .temp.cpu_temp_avg catch null) // "NA"), + ((try .gpu_usage[1] catch null) // "NA"), + ((try .temp.gpu_temp_avg catch null) // "NA"), + ((try .memory.ram_usage catch null) // "NA"), + ((try .memory.ram_total catch null) // "NA") + ] | @tsv + ' 2>/dev/null || true)" + + if [ -n "$values" ]; then + IFS=$'\t' read -r cpu_usage cpu_temp gpu_usage gpu_temp ram_used ram_total <<< "$values" + fi fi + +# ram_usage is macmon's actual used-byte counter. Memory pressure measures how +# comfortably macOS can satisfy allocations and is not a used-RAM percentage. +"$SKETCHYBAR_BIN" \ + --set cpu.text \ + icon="$(format_scaled "$cpu_usage" 100 % --.-%)" \ + label="$(format_scaled "$cpu_temp" 1 ° --.-° true)" \ + --set gpu.text \ + icon="$(format_scaled "$gpu_usage" 100 % --.-%)" \ + label="$(format_scaled "$gpu_temp" 1 ° --.-° true)" \ + --set ram.text \ + icon="$(format_ratio_percent "$ram_used" "$ram_total")" \ + label="$(format_scaled "$ram_used" 0.0000000009313225746154785 G --.-G true)" diff --git a/sketchybar/sketchybarrc b/sketchybar/sketchybarrc index 431a865..933e9a6 100755 --- a/sketchybar/sketchybarrc +++ b/sketchybar/sketchybarrc @@ -1,46 +1,59 @@ -#!/bin/bash +#!/usr/bin/env bash +# Macarchy's bar entry point is intentionally small. Each visible section owns +# its item definitions and each changing value has a separate plugin script. source "$CONFIG_DIR/colors.sh" PLUGIN_DIR="$CONFIG_DIR/plugins" ITEM_DIR="$CONFIG_DIR/items" -sketchybar --add event yabai_space_labels_refresh +configure_bar() { + sketchybar --bar \ + position=top \ + display=all \ + height=37 \ + sticky=on \ + blur_radius=0 \ + padding_left=10 \ + padding_right=10 \ + color="$BAR_COLOR" +} + +configure_item_defaults() { + sketchybar --default \ + icon.font="SF Pro:Semibold:15.0" \ + icon.color="$TEXT_COLOR" \ + icon.padding_left=10 \ + icon.padding_right=4 \ + label.font="SF Pro:Semibold:12.0" \ + label.color="$TEXT_COLOR" \ + label.y_offset=1 \ + label.padding_left=4 \ + label.padding_right=10 \ + background.drawing=on \ + background.color=0x00000000 \ + background.border_color=0x33ffffff \ + background.border_width=1 \ + background.corner_radius=5 \ + background.height=24 \ + padding_left=5 \ + padding_right=5 +} + +load_items() { + local item + + for item in spaces calendar battery system_metrics; do + # shellcheck source=/dev/null + source "$ITEM_DIR/$item.sh" + done +} +sketchybar --add event yabai_space_labels_refresh sketchybar --remove '/.*/' >/dev/null 2>&1 || true - -sketchybar --bar height=37 \ - blur_radius=0 \ - display=all \ - position=top \ - sticky=on \ - padding_left=10 \ - padding_right=10 \ - color=0x00000000 - sketchybar --load-font "$HOME/Library/Fonts/sketchybar-app-font.ttf" -sketchybar --default icon.font="SF Pro:Semibold:15.0" \ - icon.color=$TEXT_COLOR \ - label.font="SF Pro:Semibold:12.0" \ - label.color=$TEXT_COLOR \ - background.color=0x00000000 \ - background.border_color=0x33ffffff \ - background.border_width=1 \ - background.drawing=on \ - background.corner_radius=5 \ - background.height=24 \ - padding_left=5 \ - padding_right=5 \ - label.y_offset=1 \ - label.padding_left=4 \ - label.padding_right=10 \ - icon.padding_left=10 \ - icon.padding_right=4 - -source "$ITEM_DIR/spaces.sh" -source "$ITEM_DIR/calendar.sh" -source "$ITEM_DIR/battery.sh" -source "$ITEM_DIR/system_metrics.sh" - +configure_bar +configure_item_defaults +load_items sketchybar --update diff --git a/skhd/examples/advanced-shortcuts.skhdrc b/skhd/examples/advanced-shortcuts.skhdrc new file mode 100644 index 0000000..97704d7 --- /dev/null +++ b/skhd/examples/advanced-shortcuts.skhdrc @@ -0,0 +1,16 @@ +# Optional Space swapping. These actions require yabai's scripting addition. +# Copy only the bindings you want into the installed skhdrc. + +cmd + ctrl - 1 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 1 +cmd + ctrl - 2 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 2 +cmd + ctrl - 3 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 3 +cmd + ctrl - 4 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 4 +cmd + ctrl - 5 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 5 +cmd + ctrl - 6 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 6 +cmd + ctrl - 7 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 7 +cmd + ctrl - 8 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 8 +cmd + ctrl - 9 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 9 +cmd + ctrl - 0 : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/swap_space_and_refresh.sh" 10 + +# Interactive screenshot to the clipboard. +ctrl + shift - s : screencapture -i -c -x diff --git a/skhd/examples/app-shortcuts.skhdrc b/skhd/examples/app-shortcuts.skhdrc new file mode 100644 index 0000000..2b9412f --- /dev/null +++ b/skhd/examples/app-shortcuts.skhdrc @@ -0,0 +1,15 @@ +# Optional application shortcuts. Copy the bindings you want into skhdrc. + +# Open Ghostty in a new window. +ctrl - return : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/launch_ghostty.sh" + +# Open the home folder in Finder. +cmd + shift - f : open "$HOME" + +# Open Firefox's profile manager. +cmd + shift - b : open -na 'Firefox' --args -P + +# Send media keys to Spotify only when it is already running. +play : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to playpause' +next : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to next track' +previous : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to previous track' diff --git a/skhd/examples/theme-shortcuts.skhdrc b/skhd/examples/theme-shortcuts.skhdrc new file mode 100644 index 0000000..83a4602 --- /dev/null +++ b/skhd/examples/theme-shortcuts.skhdrc @@ -0,0 +1,5 @@ +# Optional theme shortcuts. Install the themes component before copying these. + +cmd + ctrl - 0x18 : "$HOME/.local/bin/theme-switch" next +cmd + ctrl + shift - 0x18 : "$HOME/.local/bin/theme-switch" next +cmd + ctrl - 0x1B : "$HOME/.local/bin/theme-switch" prev diff --git a/skhd/skhdrc b/skhd/skhdrc index 67878cb..6753a53 100644 --- a/skhd/skhdrc +++ b/skhd/skhdrc @@ -1,133 +1,49 @@ -# Launch terminal -ctrl - return : /bin/bash "$HOME/.config/yabai/launch_ghostty.sh" - -# Quit application -# In Chrome, Cmd+Q should close the current profile window instead of quitting every profile. -cmd - q [ - "Google Chrome" : osascript -e 'tell application "System Events" to keystroke "w" using {command down, shift down}' - "Firefox" ~ - "Firefox Developer Edition" ~ - "Firefox Nightly" ~ - * : osascript -e 'tell application (path to frontmost application as text) to quit' -] - -# In Messages, TablePlus, and Obsidian, Cmd+W should quit the app instead of just closing the window. -cmd - w [ - "Messages" : osascript -e 'tell application "Messages" to quit' - "TablePlus" : osascript -e 'tell application "TablePlus" to quit' - "Obsidian" : osascript -e 'tell application "Obsidian" to quit' -] - -# Media keys only control Spotify when Spotify is already running. -play : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to playpause' -next : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to next track' -fast : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to next track' -previous : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to previous track' -rewind : /usr/bin/pgrep -x Spotify >/dev/null && /usr/bin/osascript -e 'tell application "Spotify" to previous track' - -# Toggle fullscreen -ctrl - f : yabai -m window --toggle zoom-fullscreen - -# Open Firefox profile manager -cmd + shift - b : open -na 'Firefox' --args -P -ctrl + shift - b : open -na 'Firefox' --args -P - -# Open home folder in Finder -cmd + shift - f : open "$HOME" - -# Focus windows -cmd - left : yabai -m window --focus west -cmd - down : yabai -m window --focus south -cmd - up : yabai -m window --focus north -cmd - right : yabai -m window --focus east -ctrl - left : yabai -m window --focus west -ctrl - down : yabai -m window --focus south -ctrl - up : yabai -m window --focus north -ctrl - right : yabai -m window --focus east - -# Reorder windows -cmd + shift - left : yabai -m window --swap west; yabai -m window --focus west -cmd + shift - down : yabai -m window --swap south; yabai -m window --focus south -cmd + shift - up : yabai -m window --swap north; yabai -m window --focus north -cmd + shift - right : yabai -m window --swap east; yabai -m window --focus east -ctrl + shift - left : yabai -m window --swap west; yabai -m window --focus west -ctrl + shift - down : yabai -m window --swap south; yabai -m window --focus south -ctrl + shift - up : yabai -m window --swap north; yabai -m window --focus north -ctrl + shift - right : yabai -m window --swap east; yabai -m window --focus east - -# Switch workspaces -cmd - 1 : yabai -m space --focus 1 -cmd - 2 : yabai -m space --focus 2 -cmd - 3 : yabai -m space --focus 3 -cmd - 4 : yabai -m space --focus 4 -cmd - 5 : yabai -m space --focus 5 -cmd - 6 : yabai -m space --focus 6 -cmd - 7 : yabai -m space --focus 7 -cmd - 8 : yabai -m space --focus 8 -cmd - 9 : yabai -m space --focus 9 -cmd - 0 : yabai -m space --focus 10 -ctrl - 1 : yabai -m space --focus 1 -ctrl - 2 : yabai -m space --focus 2 -ctrl - 3 : yabai -m space --focus 3 -ctrl - 4 : yabai -m space --focus 4 -ctrl - 5 : yabai -m space --focus 5 -ctrl - 6 : yabai -m space --focus 6 -ctrl - 7 : yabai -m space --focus 7 -ctrl - 8 : yabai -m space --focus 8 -ctrl - 9 : yabai -m space --focus 9 -ctrl - 0 : yabai -m space --focus 10 - -# Swap current workspace with target workspace -cmd + ctrl - 1 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 1 -cmd + ctrl - 2 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 2 -cmd + ctrl - 3 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 3 -cmd + ctrl - 4 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 4 -cmd + ctrl - 5 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 5 -cmd + ctrl - 6 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 6 -cmd + ctrl - 7 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 7 -cmd + ctrl - 8 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 8 -cmd + ctrl - 9 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 9 -cmd + ctrl - 0 : /bin/bash "$HOME/.config/yabai/swap_space_and_refresh.sh" 10 - -# Move window to workspace and follow focus -cmd + shift - 1 : yabai -m window --space 1; yabai -m space --focus 1 -cmd + shift - 2 : yabai -m window --space 2; yabai -m space --focus 2 -cmd + shift - 3 : yabai -m window --space 3; yabai -m space --focus 3 -cmd + shift - 4 : yabai -m window --space 4; yabai -m space --focus 4 -cmd + shift - 5 : yabai -m window --space 5; yabai -m space --focus 5 -cmd + shift - 6 : yabai -m window --space 6; yabai -m space --focus 6 -cmd + shift - 7 : yabai -m window --space 7; yabai -m space --focus 7 -cmd + shift - 8 : yabai -m window --space 8; yabai -m space --focus 8 -cmd + shift - 9 : yabai -m window --space 9; yabai -m space --focus 9 -cmd + shift - 0 : yabai -m window --space 10; yabai -m space --focus 10 -ctrl + shift - 1 : yabai -m window --space 1; yabai -m space --focus 1 -ctrl + shift - 2 : yabai -m window --space 2; yabai -m space --focus 2 -ctrl + shift - 3 : yabai -m window --space 3; yabai -m space --focus 3 -ctrl + shift - 4 : yabai -m window --space 4; yabai -m space --focus 4 -ctrl + shift - 5 : yabai -m window --space 5; yabai -m space --focus 5 -ctrl + shift - 6 : yabai -m window --space 6; yabai -m space --focus 6 -ctrl + shift - 7 : yabai -m window --space 7; yabai -m space --focus 7 -ctrl + shift - 8 : yabai -m window --space 8; yabai -m space --focus 8 -ctrl + shift - 9 : yabai -m window --space 9; yabai -m space --focus 9 -ctrl + shift - 0 : yabai -m window --space 10; yabai -m space --focus 10 - -# Toggle focused window floating / tiled and persist per-app preference -ctrl - t : /bin/bash "$HOME/.config/yabai/toggle_float_persist.sh" - -# Toggle split direction -ctrl - j : yabai -m window --toggle split - -# Rotate and rebalance current BSP layout -ctrl - l : yabai -m space --rotate 90; yabai -m space --balance - -# Resize focused window -ctrl - 0x18 : yabai -m window --ratio rel:0.05 -ctrl - 0x1B : yabai -m window --ratio rel:-0.05 - -# Screenshot (interactive, to clipboard) -ctrl + shift - s : screencapture -i -c -x - -# Cycle Macarchy themes directly -cmd + ctrl - 0x18 : "$HOME/.local/bin/theme-switch" next -cmd + ctrl + shift - 0x18 : "$HOME/.local/bin/theme-switch" next -cmd + ctrl - 0x1B : "$HOME/.local/bin/theme-switch" prev +# Macarchy uses one dedicated modifier family so ordinary Command and Control +# shortcuts keep working in applications. Optional bindings live in examples/. + +# Focus a window. +ctrl + alt - left : yabai -m window --focus west +ctrl + alt - down : yabai -m window --focus south +ctrl + alt - up : yabai -m window --focus north +ctrl + alt - right : yabai -m window --focus east + +# Reorder windows. +ctrl + alt + shift - left : yabai -m window --swap west; yabai -m window --focus west +ctrl + alt + shift - down : yabai -m window --swap south; yabai -m window --focus south +ctrl + alt + shift - up : yabai -m window --swap north; yabai -m window --focus north +ctrl + alt + shift - right : yabai -m window --swap east; yabai -m window --focus east + +# Focus Spaces 1 to 10. +ctrl + alt - 1 : yabai -m space --focus 1 +ctrl + alt - 2 : yabai -m space --focus 2 +ctrl + alt - 3 : yabai -m space --focus 3 +ctrl + alt - 4 : yabai -m space --focus 4 +ctrl + alt - 5 : yabai -m space --focus 5 +ctrl + alt - 6 : yabai -m space --focus 6 +ctrl + alt - 7 : yabai -m space --focus 7 +ctrl + alt - 8 : yabai -m space --focus 8 +ctrl + alt - 9 : yabai -m space --focus 9 +ctrl + alt - 0 : yabai -m space --focus 10 + +# Move the focused window to a Space and follow it. +ctrl + alt + shift - 1 : yabai -m window --space 1; yabai -m space --focus 1 +ctrl + alt + shift - 2 : yabai -m window --space 2; yabai -m space --focus 2 +ctrl + alt + shift - 3 : yabai -m window --space 3; yabai -m space --focus 3 +ctrl + alt + shift - 4 : yabai -m window --space 4; yabai -m space --focus 4 +ctrl + alt + shift - 5 : yabai -m window --space 5; yabai -m space --focus 5 +ctrl + alt + shift - 6 : yabai -m window --space 6; yabai -m space --focus 6 +ctrl + alt + shift - 7 : yabai -m window --space 7; yabai -m space --focus 7 +ctrl + alt + shift - 8 : yabai -m window --space 8; yabai -m space --focus 8 +ctrl + alt + shift - 9 : yabai -m window --space 9; yabai -m space --focus 9 +ctrl + alt + shift - 0 : yabai -m window --space 10; yabai -m space --focus 10 + +# Window and layout controls. +ctrl + alt - f : yabai -m window --toggle zoom-fullscreen +ctrl + alt - t : /bin/bash "${XDG_CONFIG_HOME:-$HOME/.config}/yabai/toggle_float_persist.sh" +ctrl + alt - j : yabai -m window --toggle split +ctrl + alt - l : yabai -m space --rotate 90; yabai -m space --balance +ctrl + alt - 0x18 : yabai -m window --ratio rel:0.05 +ctrl + alt - 0x1B : yabai -m window --ratio rel:-0.05 + +# The installer creates this untracked file for machine-specific shortcuts. +.load "local.skhdrc" diff --git a/tests/installer_test.sh b/tests/installer_test.sh new file mode 100755 index 0000000..faf1681 --- /dev/null +++ b/tests/installer_test.sh @@ -0,0 +1,399 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +INSTALLER="$REPO_ROOT/install" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-installer-test.XXXXXX")" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +pass() { + printf 'ok - %s\n' "$1" +} + +assert_file() { + [ -f "$1" ] || fail "expected file: $1" +} + +assert_no_path() { + [ ! -e "$1" ] && [ ! -L "$1" ] || fail "expected no path: $1" +} + +assert_contains() { + local file="$1" expected="$2" + grep -F -- "$expected" "$file" >/dev/null || fail "expected '$expected' in $file" +} + +HOME="$TEST_ROOT/Home With Spaces" +XDG_CONFIG_HOME="$HOME/Custom Config" +XDG_DATA_HOME="$HOME/Custom Data" +XDG_STATE_HOME="$HOME/Custom State" +MACARCHY_BIN_HOME="$HOME/Custom Bin" +MACARCHY_APPLICATIONS_DIR="$TEST_ROOT/Applications" +FAKE_BIN="$TEST_ROOT/fake-bin" +FAKE_BREW_LOG="$TEST_ROOT/brew.log" +FAKE_BREW_STATE="$TEST_ROOT/brew-state" + +mkdir -p "$HOME" "$FAKE_BIN" "$MACARCHY_APPLICATIONS_DIR" +: > "$FAKE_BREW_LOG" +: > "$FAKE_BREW_STATE" + +cat > "$FAKE_BIN/brew" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$*" >> "$FAKE_BREW_LOG" + +case "${1:-}" in + list) + kind="${2:-}" + item="${4:-${3:-}}" + grep -Fqx "$kind:$item" "$FAKE_BREW_STATE" + ;; + install) + if [ "${2:-}" = '--cask' ]; then + printf '%s\n' "--cask:${3}" >> "$FAKE_BREW_STATE" + else + item="${2##*/}" + printf '%s\n' "--formula:$item" >> "$FAKE_BREW_STATE" + fi + ;; + --prefix) + printf '%s\n' "$TEST_ROOT/fake-homebrew" + ;; + *) + printf 'unexpected fake brew arguments: %s\n' "$*" >&2 + exit 2 + ;; +esac +EOF +chmod +x "$FAKE_BIN/brew" + +export HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_STATE_HOME MACARCHY_BIN_HOME MACARCHY_APPLICATIONS_DIR +export FAKE_BREW_LOG FAKE_BREW_STATE TEST_ROOT +export PATH="$FAKE_BIN:/usr/bin:/bin:/usr/sbin:/sbin" + +run_capture() { + local output="$1" + shift + "$INSTALLER" "$@" > "$output" 2>&1 +} + +# Listing is read-only and documents every selectable component. +LIST_OUTPUT="$TEST_ROOT/list.out" +run_capture "$LIST_OUTPUT" --list +assert_contains "$LIST_OUTPUT" 'window-manager' +assert_contains "$LIST_OUTPUT" 'raycast' +pass 'lists modular components' + +# A dry run must not call Homebrew or write to the temporary home. +DRY_OUTPUT="$TEST_ROOT/dry-run.out" +run_capture "$DRY_OUTPUT" --dry-run install window-manager +assert_no_path "$XDG_CONFIG_HOME/yabai" +[ ! -s "$FAKE_BREW_LOG" ] || fail 'dry run called Homebrew' +assert_contains "$DRY_OUTPUT" 'Would ensure Homebrew formula: asmvik/formulae/yabai' +pass 'dry run is side-effect free' + +# Direct shorthand installs one component and leaves unrelated components alone. +mkdir -p "$XDG_CONFIG_HOME/ghostty" +printf '%s\n' 'original ghostty config' > "$XDG_CONFIG_HOME/ghostty/config.ghostty" +printf '%s\n' 'original legacy ghostty config' > "$XDG_CONFIG_HOME/ghostty/config" +TERMINAL_OUTPUT="$TEST_ROOT/terminal-install.out" +run_capture "$TERMINAL_OUTPUT" terminal +assert_file "$XDG_CONFIG_HOME/ghostty/config.ghostty" +cmp -s "$REPO_ROOT/ghostty/config.ghostty" "$XDG_CONFIG_HOME/ghostty/config.ghostty" || fail 'terminal config was not installed' +assert_contains "$XDG_CONFIG_HOME/ghostty/config" 'Legacy Ghostty compatibility file' +assert_no_path "$XDG_CONFIG_HOME/nvim" +assert_contains "$FAKE_BREW_LOG" 'install --cask ghostty' +assert_file "$XDG_STATE_HOME/macarchy/manifests/terminal.tsv" +BACKUP_COUNT_BEFORE="$(find "$XDG_STATE_HOME/macarchy/backups" -type f | wc -l | tr -d ' ')" +[ "$BACKUP_COUNT_BEFORE" -eq 2 ] || fail 'current and legacy terminal configs were not backed up once' +pass 'installs only the selected component and backs up existing files' + +# Repeating an install should not make another backup or duplicate state. +run_capture "$TEST_ROOT/terminal-reinstall.out" install terminal +BACKUP_COUNT_AFTER="$(find "$XDG_STATE_HOME/macarchy/backups" -type f | wc -l | tr -d ' ')" +[ "$BACKUP_COUNT_AFTER" -eq "$BACKUP_COUNT_BEFORE" ] || fail 'idempotent install created another backup' +[ "$(wc -l < "$XDG_STATE_HOME/macarchy/manifests/terminal.tsv" | tr -d ' ')" -eq 2 ] || fail 'manifest contains duplicate entries' +pass 'repeated install is idempotent' + +# A rerun must stop rather than overwrite a locally edited managed file. +printf '%s\n' 'local terminal edit' > "$XDG_CONFIG_HOME/ghostty/config.ghostty" +if run_capture "$TEST_ROOT/terminal-conflict.out" install terminal; then + fail 'update overwrote a locally edited managed file without --force' +fi +assert_contains "$XDG_CONFIG_HOME/ghostty/config.ghostty" 'local terminal edit' +assert_contains "$TEST_ROOT/terminal-conflict.out" 'managed file has local edits' + +run_capture "$TEST_ROOT/terminal-force.out" --force install terminal +cmp -s "$REPO_ROOT/ghostty/config.ghostty" "$XDG_CONFIG_HOME/ghostty/config.ghostty" \ + || fail 'forced update did not replace the managed file' +BACKUP_COUNT_FORCED="$(find "$XDG_STATE_HOME/macarchy/backups" -type f | wc -l | tr -d ' ')" +[ "$BACKUP_COUNT_FORCED" -eq $((BACKUP_COUNT_BEFORE + 1)) ] \ + || fail 'forced update did not preserve the locally edited version' +pass 'updates stop on local edits unless force is explicit' + +# Files removed from a later component version are pruned when they are still +# unchanged. This keeps updates from leaving obsolete generated bundles behind. +OLD_MANAGED="$XDG_CONFIG_HOME/ghostty/old-managed.conf" +printf '%s\n' 'obsolete managed file' > "$OLD_MANAGED" +OLD_HASH="$(shasum -a 256 "$OLD_MANAGED" | awk '{print $1}')" +printf '%s\t-\t%s\n' "$OLD_MANAGED" "$OLD_HASH" >> "$XDG_STATE_HOME/macarchy/manifests/terminal.tsv" +run_capture "$TEST_ROOT/terminal-prune.out" install terminal +assert_no_path "$OLD_MANAGED" +if grep -F -- "$OLD_MANAGED" "$XDG_STATE_HOME/macarchy/manifests/terminal.tsv" >/dev/null; then + fail 'obsolete managed file remained in the manifest' +fi +pass 'updates prune unchanged files no longer shipped by a component' + +STATUS_OUTPUT="$TEST_ROOT/status.out" +run_capture "$STATUS_OUTPUT" status terminal +assert_contains "$STATUS_OUTPUT" 'installed (2 managed files)' +pass 'reports installed state' + +# Uninstall restores the exact file that existed before Macarchy managed it. +run_capture "$TEST_ROOT/terminal-uninstall.out" uninstall terminal +assert_contains "$XDG_CONFIG_HOME/ghostty/config.ghostty" 'original ghostty config' +assert_contains "$XDG_CONFIG_HOME/ghostty/config" 'original legacy ghostty config' +assert_no_path "$XDG_STATE_HOME/macarchy/manifests/terminal.tsv" +pass 'uninstall restores the original file' + +# A file created by Macarchy is removed, but a later user edit is never deleted. +run_capture "$TEST_ROOT/nvim-install.out" nvim +assert_file "$XDG_CONFIG_HOME/nvim/init.lua" +printf '%s\n' '-- user edit' > "$XDG_CONFIG_HOME/nvim/init.lua" +if run_capture "$TEST_ROOT/nvim-uninstall.out" uninstall nvim; then + fail 'partial uninstall should report modified files' +fi +assert_contains "$XDG_CONFIG_HOME/nvim/init.lua" '-- user edit' +assert_contains "$TEST_ROOT/nvim-uninstall.out" 'kept modified file' +assert_file "$XDG_STATE_HOME/macarchy/manifests/nvim.tsv" +pass 'uninstall preserves modified files and keeps their manifest entries' + +# Window manager dependencies use the maintained taps and no service is started. +: > "$FAKE_BREW_LOG" +mkdir -p "$XDG_CONFIG_HOME/skhd" +printf '%s\n' 'cmd + shift - 1 : legacy-space-command' > "$XDG_CONFIG_HOME/skhd/skhdrc" +run_capture "$TEST_ROOT/window-manager.out" window-manager +assert_contains "$FAKE_BREW_LOG" 'install asmvik/formulae/yabai' +assert_contains "$FAKE_BREW_LOG" 'install asmvik/formulae/skhd' +assert_contains "$FAKE_BREW_LOG" 'install FelixKratz/formulae/borders' +if grep -E 'services|launchctl|sudo' "$FAKE_BREW_LOG" >/dev/null; then + fail 'installer attempted to start a service or elevate privileges' +fi +assert_file "$XDG_CONFIG_HOME/yabai/yabairc" +assert_file "$XDG_CONFIG_HOME/skhd/skhdrc" +assert_file "$XDG_CONFIG_HOME/skhd/local.skhdrc" +cmp -s "$REPO_ROOT/skhd/skhdrc" "$XDG_CONFIG_HOME/skhd/skhdrc" \ + || fail 'public skhd configuration was not installed after preserving shortcuts' +assert_contains "$XDG_CONFIG_HOME/skhd/local.skhdrc" 'cmd + shift - 1 : legacy-space-command' +assert_contains "$TEST_ROOT/window-manager.out" 'Preserved existing shortcuts in:' +if grep -F "$XDG_CONFIG_HOME/skhd/local.skhdrc" "$XDG_STATE_HOME/macarchy/manifests/window-manager.tsv" >/dev/null; then + fail 'machine-specific skhd shortcuts were added to the managed manifest' +fi +assert_contains "$TEST_ROOT/window-manager.out" 'launchctl setenv XDG_CONFIG_HOME' +pass 'preserves unmanaged shortcuts before installing the window manager' + +# An existing local override is user-owned and must survive later installs. +printf '%s\n' 'cmd + shift - 2 : local-space-command' > "$XDG_CONFIG_HOME/skhd/local.skhdrc" +run_capture "$TEST_ROOT/window-manager-reinstall.out" window-manager +assert_contains "$XDG_CONFIG_HOME/skhd/local.skhdrc" 'cmd + shift - 2 : local-space-command' +pass 'does not overwrite an existing local shortcut file' + +# If the local override is missing on a later install, create a fresh template +# instead of copying the already managed public configuration into it. +rm "$XDG_CONFIG_HOME/skhd/local.skhdrc" +run_capture "$TEST_ROOT/window-manager-managed-reinstall.out" window-manager +assert_file "$XDG_CONFIG_HOME/skhd/local.skhdrc" +assert_contains "$XDG_CONFIG_HOME/skhd/local.skhdrc" 'This file is not managed by Macarchy.' +if cmp -s "$REPO_ROOT/skhd/skhdrc" "$XDG_CONFIG_HOME/skhd/local.skhdrc"; then + fail 'managed public shortcuts were copied into the local shortcut file' +fi +pass 'does not copy managed public shortcuts into the local override' + +# SketchyBar's app labels rely on its companion ligature font. +: > "$FAKE_BREW_LOG" +run_capture "$TEST_ROOT/sketchybar.out" sketchybar +assert_contains "$FAKE_BREW_LOG" 'install FelixKratz/formulae/sketchybar' +if [ "$(uname -s)" = 'Darwin' ] && [ "$(uname -m)" = 'arm64' ]; then + assert_contains "$FAKE_BREW_LOG" 'install macmon' +fi +assert_contains "$FAKE_BREW_LOG" 'install --cask font-sketchybar-app-font' +assert_file "$XDG_CONFIG_HOME/sketchybar/sketchybarrc" +pass 'installs the SketchyBar app font dependency' + +# Themes include their shared library, preserve existing assets and start +# without mutable runtime state. +mkdir -p "$XDG_CONFIG_HOME/themes/lumon/backgrounds" "$XDG_CONFIG_HOME/themes/blackgold/backgrounds" +printf '%s\n' 'DARK_MODE=true' > "$XDG_CONFIG_HOME/themes/lumon/theme.env" +printf '%s\n' 'old wallpaper' > "$XDG_CONFIG_HOME/themes/lumon/backgrounds/old.jpg" +printf '%s\n' 'old wallpaper' > "$XDG_CONFIG_HOME/themes/blackgold/backgrounds/old.jpg" +run_capture "$TEST_ROOT/themes.out" themes +assert_file "$XDG_CONFIG_HOME/themes/blackgold/theme.env" +assert_file "$XDG_DATA_HOME/macarchy/lib/theme.sh" +assert_file "$MACARCHY_BIN_HOME/theme-switch" +assert_no_path "$XDG_CONFIG_HOME/themes/.current" +assert_contains "$XDG_CONFIG_HOME/themes/lumon/backgrounds/old.jpg" 'old wallpaper' +assert_contains "$XDG_CONFIG_HOME/themes/blackgold/backgrounds/old.jpg" 'old wallpaper' +if find "$XDG_STATE_HOME/macarchy/backups" -path '*themes/*legacy-*' -print | grep . >/dev/null; then + fail 'existing theme content was moved into a legacy backup' +fi +assert_contains "$TEST_ROOT/themes.out" "$MACARCHY_BIN_HOME/theme-switch" +if grep -F 'desktoppr' "$TEST_ROOT/themes.out" "$FAKE_BREW_LOG" >/dev/null; then + fail 'themes installed optional desktoppr despite having a system fallback' +fi +pass 'installs theme code without moving existing backgrounds' + +# Uninstall removes only unchanged runtime files recorded by theme-switch. +THEME_RUNTIME="$XDG_STATE_HOME/macarchy/themes/current" +THEME_SHELL_STATE="$XDG_CONFIG_HOME/macarchy/generated/shell-env" +THEME_GENERATED_MANIFEST="$XDG_STATE_HOME/macarchy/generated/themes.tsv" +mkdir -p "$(dirname "$THEME_RUNTIME")" "$(dirname "$THEME_SHELL_STATE")" \ + "$(dirname "$THEME_GENERATED_MANIFEST")" +printf '%s\n' 'carbonfox' > "$THEME_RUNTIME" +printf '%s\n' 'generated shell state' > "$THEME_SHELL_STATE" +RUNTIME_HASH="$(shasum -a 256 "$THEME_RUNTIME" | awk '{print $1}')" +SHELL_STATE_HASH="$(shasum -a 256 "$THEME_SHELL_STATE" | awk '{print $1}')" +printf '%s\t%s\n%s\t%s\n' \ + "$THEME_RUNTIME" "$RUNTIME_HASH" \ + "$THEME_SHELL_STATE" "$SHELL_STATE_HASH" > "$THEME_GENERATED_MANIFEST" +printf '%s\n' 'locally edited shell state' > "$THEME_SHELL_STATE" + +if run_capture "$TEST_ROOT/themes-uninstall.out" uninstall themes; then + fail 'themes uninstall did not report a modified generated file' +fi +assert_no_path "$THEME_RUNTIME" +assert_contains "$THEME_SHELL_STATE" 'locally edited shell state' +assert_contains "$THEME_GENERATED_MANIFEST" "$THEME_SHELL_STATE" +assert_no_path "$XDG_STATE_HOME/macarchy/manifests/themes.tsv" +rm -f "$THEME_SHELL_STATE" +run_capture "$TEST_ROOT/themes-uninstall-finish.out" uninstall themes +assert_no_path "$THEME_GENERATED_MANIFEST" +pass 'theme uninstall removes generated state but preserves local edits' + +# Existing applications and a suitable Node runtime satisfy Raycast's +# dependencies even when Homebrew did not install them. Development source is +# kept apart from Raycast's generated runtime so local React cannot shadow the +# copy bundled with Raycast. +mkdir -p "$MACARCHY_APPLICATIONS_DIR/Raycast.app" +cat > "$FAKE_BIN/node" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF +chmod +x "$FAKE_BIN/node" + +RAYCAST_SOURCE="$XDG_DATA_HOME/macarchy/raycast-theme" +LEGACY_RAYCAST_SOURCE="$XDG_CONFIG_HOME/raycast/extensions/theme-switcher" +RAYCAST_RUNTIME="$HOME/.config/raycast/extensions/theme-switcher" +RAYCAST_ORIGINAL_BACKUP="$XDG_STATE_HOME/macarchy/backups/original-raycast/README.md" +RAYCAST_RECOVERY_MANIFEST="$XDG_STATE_HOME/macarchy/recovery/raycast-runtime.tsv" +mkdir -p "$LEGACY_RAYCAST_SOURCE/src" "$RAYCAST_RUNTIME/node_modules/react" +mkdir -p "$(dirname "$RAYCAST_ORIGINAL_BACKUP")" +printf '%s\n' 'legacy managed source' > "$LEGACY_RAYCAST_SOURCE/package.json" +printf '%s\n' 'legacy command source' > "$LEGACY_RAYCAST_SOURCE/src/switch-theme.tsx" +printf '%s\n' 'legacy managed readme' > "$LEGACY_RAYCAST_SOURCE/README.md" +printf '%s\n' 'original Raycast readme' > "$RAYCAST_ORIGINAL_BACKUP" +printf '%s\n' 'duplicate React' > "$RAYCAST_RUNTIME/node_modules/react/index.js" +LEGACY_RAYCAST_HASH="$(shasum -a 256 "$LEGACY_RAYCAST_SOURCE/package.json" | awk '{print $1}')" +LEGACY_RAYCAST_SOURCE_HASH="$(shasum -a 256 "$LEGACY_RAYCAST_SOURCE/src/switch-theme.tsx" | awk '{print $1}')" +LEGACY_RAYCAST_README_HASH="$(shasum -a 256 "$LEGACY_RAYCAST_SOURCE/README.md" | awk '{print $1}')" +printf '%s\t-\t%s\n%s\t-\t%s\n%s\t%s\t%s\n' \ + "$LEGACY_RAYCAST_SOURCE/package.json" "$LEGACY_RAYCAST_HASH" \ + "$LEGACY_RAYCAST_SOURCE/src/switch-theme.tsx" "$LEGACY_RAYCAST_SOURCE_HASH" \ + "$LEGACY_RAYCAST_SOURCE/README.md" "$RAYCAST_ORIGINAL_BACKUP" "$LEGACY_RAYCAST_README_HASH" \ + > "$XDG_STATE_HOME/macarchy/manifests/raycast.tsv" + +: > "$FAKE_BREW_LOG" +run_capture "$TEST_ROOT/raycast-dry-run.out" --dry-run raycast +assert_no_path "$RAYCAST_SOURCE/package.json" +assert_file "$RAYCAST_RUNTIME/node_modules/react/index.js" +assert_file "$LEGACY_RAYCAST_SOURCE/src/switch-theme.tsx" +assert_no_path "$RAYCAST_RECOVERY_MANIFEST" +assert_contains "$TEST_ROOT/raycast-dry-run.out" 'Would remove old Raycast development file:' +assert_contains "$TEST_ROOT/raycast-dry-run.out" 'Would retain Raycast runtime recovery information for:' +if grep -F 'file no longer shipped by raycast' "$TEST_ROOT/raycast-dry-run.out" >/dev/null; then + fail 'Raycast migration dry run reached the generic pruning path' +fi + +run_capture "$TEST_ROOT/raycast.out" raycast +assert_file "$RAYCAST_SOURCE/package.json" +assert_file "$RAYCAST_SOURCE/.nvmrc" +assert_file "$RAYCAST_SOURCE/scripts/assert-runtime-clean.mjs" +assert_contains "$TEST_ROOT/raycast.out" "$RAYCAST_SOURCE" +assert_contains "$LEGACY_RAYCAST_SOURCE/package.json" 'legacy managed source' +assert_no_path "$LEGACY_RAYCAST_SOURCE/src/switch-theme.tsx" +assert_no_path "$LEGACY_RAYCAST_SOURCE/README.md" +assert_contains "$RAYCAST_RECOVERY_MANIFEST" "$LEGACY_RAYCAST_SOURCE/README.md" +assert_contains "$RAYCAST_RECOVERY_MANIFEST" "$RAYCAST_ORIGINAL_BACKUP" +assert_contains "$RAYCAST_ORIGINAL_BACKUP" 'original Raycast readme' +assert_no_path "$RAYCAST_RUNTIME/node_modules" +assert_no_path "$RAYCAST_RUNTIME/src" +assert_no_path "$XDG_CONFIG_HOME/raycast/extensions/macarchy-theme-switcher" +if grep -F '/raycast/extensions/' "$XDG_STATE_HOME/macarchy/manifests/raycast.tsv" >/dev/null; then + fail 'Raycast runtime files remained in the managed manifest' +fi +if ! find "$XDG_STATE_HOME/macarchy/backups" \ + -path '*raycast/*-runtime-node_modules/react/index.js' -type f -print | grep . >/dev/null; then + fail 'unsafe Raycast runtime dependencies were not preserved in a backup' +fi +if grep -F 'install --cask raycast' "$FAKE_BREW_LOG" >/dev/null; then + fail 'installer tried to replace an existing Raycast application' +fi +if grep -F 'install node' "$FAKE_BREW_LOG" >/dev/null; then + fail 'installer ignored a suitable existing Node runtime' +fi +run_capture "$TEST_ROOT/raycast-status.out" status raycast +assert_contains "$TEST_ROOT/raycast-status.out" 'installed (' + +# Raycast owns its generated bundle. Runtime changes must not affect installer +# status or be removed when the source component is uninstalled. +printf '%s\n' 'generated command' > "$RAYCAST_RUNTIME/switch-theme.js" +run_capture "$TEST_ROOT/raycast-runtime-status.out" status raycast +run_capture "$TEST_ROOT/raycast-uninstall.out" uninstall raycast +assert_no_path "$RAYCAST_SOURCE/package.json" +assert_contains "$RAYCAST_RUNTIME/switch-theme.js" 'generated command' +assert_contains "$LEGACY_RAYCAST_SOURCE/package.json" 'legacy managed source' +assert_no_path "$XDG_STATE_HOME/macarchy/manifests/raycast.tsv" +assert_contains "$RAYCAST_RECOVERY_MANIFEST" "$RAYCAST_ORIGINAL_BACKUP" +pass 'keeps Raycast source, runtime and dependencies separate' + +# The shell bootstrap honours a non-default XDG config path, including spaces. +printf '%s\n' 'original zsh bootstrap' > "$HOME/.zshenv" +printf '%s\n' 'existing root zshrc' > "$HOME/.zshrc" +printf '%s\n' 'existing root zprofile' > "$HOME/.zprofile" +mv "$FAKE_BIN/brew" "$FAKE_BIN/brew.disabled" +run_capture "$TEST_ROOT/shell.out" shell +mv "$FAKE_BIN/brew.disabled" "$FAKE_BIN/brew" +assert_contains "$HOME/.zshenv" 'export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-' +assert_contains "$HOME/.zshenv" 'Custom\ Config}' +assert_contains "$HOME/.zshenv" 'export ZDOTDIR="${ZDOTDIR:-$XDG_CONFIG_HOME/zsh}"' +assert_contains "$TEST_ROOT/shell.out" 'sets ZDOTDIR' +[ "$(env -i HOME="$HOME" PATH=/usr/bin:/bin zsh -c 'source "$HOME/.zshenv"; print -r -- "$XDG_CONFIG_HOME"')" = "$XDG_CONFIG_HOME" ] \ + || fail 'shell bootstrap did not restore the custom XDG path' +assert_file "$XDG_CONFIG_HOME/zsh/.zshrc" +run_capture "$TEST_ROOT/shell-uninstall.out" uninstall shell +assert_contains "$HOME/.zshenv" 'original zsh bootstrap' +pass 'shell bootstrap uses portable XDG paths and restores cleanly' + +if run_capture "$TEST_ROOT/force-status.out" --force status terminal; then + fail '--force unexpectedly worked with status' +fi +assert_contains "$TEST_ROOT/force-status.out" '--force can only be used while installing components' +pass 'limits force to explicit installs' + +if run_capture "$TEST_ROOT/unknown.out" install made-up-component; then + fail 'unknown component unexpectedly succeeded' +fi +assert_contains "$TEST_ROOT/unknown.out" "unknown component 'made-up-component'" +pass 'rejects unknown components clearly' + +printf 'All installer tests passed.\n' diff --git a/tests/no_personal_data_test.sh b/tests/no_personal_data_test.sh new file mode 100755 index 0000000..d327ebd --- /dev/null +++ b/tests/no_personal_data_test.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + +# Build the search terms in pieces so this test does not match its own source. +user_home_pattern='/'"Users"'/' +personal_name_pattern='(^|[^[:alnum:]_])[Hh]ar'"ry"'([^[:alnum:]_]|$)' +personal_bundle_pattern='com\.'"har"'ry([.]|$)' +search_pattern="$user_home_pattern|$personal_name_pattern|$personal_bundle_pattern" + +set +e +matches="$(git -C "$REPO_ROOT" grep -nI -E "$search_pattern" -- .)" +status=$? +set -e + +case "$status" in + 0) + printf 'Personal hard-coded values found:\n%s\n' "$matches" >&2 + exit 1 + ;; + 1) + printf 'No personal hard-coded paths or names found.\n' + ;; + *) + printf 'git grep failed while checking for personal values.\n' >&2 + exit "$status" + ;; +esac + +while IFS= read -r -d '' relative; do + path="$REPO_ROOT/$relative" + [ -f "$path" ] || continue + LC_ALL=C grep -Iq . "$path" && continue + + set +e + binary_matches="$(strings "$path" | grep -E -i "$search_pattern|Shutterstock|No use without permission")" + status=$? + set -e + if [ "$status" -eq 0 ]; then + printf 'Personal or restricted metadata found in %s:\n%s\n' "$relative" "$binary_matches" >&2 + exit 1 + fi + [ "$status" -eq 1 ] || exit "$status" +done < <(git -C "$REPO_ROOT" ls-files -z) + +printf 'No personal or restricted binary metadata found.\n' diff --git a/tests/sketchybar_battery_test.sh b/tests/sketchybar_battery_test.sh new file mode 100755 index 0000000..2b94c65 --- /dev/null +++ b/tests/sketchybar_battery_test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-battery-test.XXXXXX")" +FAKE_BIN="$TEST_ROOT/bin" +SKETCHYBAR_LOG="$TEST_ROOT/sketchybar.log" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +mkdir -p "$FAKE_BIN" + +cat > "$FAKE_BIN/pmset" <<'EOF' +#!/usr/bin/env bash + +case "$*" in + '-g batt') + printf "Now drawing from 'AC Power'\n" + printf ' -InternalBattery-0 (id=1)\t%s%%; not charging; present: true\n' "$BATTERY_TEST_PERCENT" + ;; + '-g custom') + printf 'AC Power:\n' + printf ' %s %s\n' "$BATTERY_TEST_POWER_KEY" "$BATTERY_TEST_LOW_POWER" + ;; +esac +EOF + +cat > "$FAKE_BIN/sketchybar" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$SKETCHYBAR_LOG" +EOF + +chmod +x "$FAKE_BIN/pmset" "$FAKE_BIN/sketchybar" +export PATH="$FAKE_BIN:/usr/bin:/bin" +export CONFIG_DIR="$REPO_ROOT/sketchybar" +export NAME=battery +export SKETCHYBAR_LOG + +run_case() { + local percentage="$1" power_key="$2" low_power="$3" expected_colour="$4" + + : > "$SKETCHYBAR_LOG" + BATTERY_TEST_PERCENT="$percentage" \ + BATTERY_TEST_POWER_KEY="$power_key" \ + BATTERY_TEST_LOW_POWER="$low_power" \ + "$REPO_ROOT/sketchybar/plugins/battery.sh" + + grep -F "icon.color=$expected_colour" "$SKETCHYBAR_LOG" >/dev/null \ + || fail "unexpected colour for battery at $percentage per cent with mode $low_power" +} + +run_case 50 powermode 2 0xffFFD60A +run_case 50 lowpowermode 1 0xffFFD60A +run_case 5 powermode 2 0xffff453a +run_case 50 powermode 0 0xffffffff + +printf 'SketchyBar battery power-mode colours passed.\n' diff --git a/tests/sketchybar_metrics_test.sh b/tests/sketchybar_metrics_test.sh new file mode 100755 index 0000000..41404b2 --- /dev/null +++ b/tests/sketchybar_metrics_test.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-metrics-test.XXXXXX")" +FAKE_BIN="$TEST_ROOT/bin" +CONFIG_DIR="$TEST_ROOT/config" + +cleanup() { + if [ -r "$CONFIG_DIR/.state/macmon-stream.pid" ]; then + kill "$(cat "$CONFIG_DIR/.state/macmon-stream.pid")" 2>/dev/null || true + fi + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +mkdir -p "$FAKE_BIN" "$CONFIG_DIR/.state" "$CONFIG_DIR/plugins" +ln -s "$REPO_ROOT/sketchybar/plugins/macmon_stream.sh" "$CONFIG_DIR/plugins/macmon_stream.sh" + +cat > "$FAKE_BIN/macmon" <<'EOF' +#!/usr/bin/env bash +trap 'exit 0' TERM INT +while :; do + printf '%s\n' '{"cpu_usage_pct":0.375,"gpu_usage":[500,0.5],"memory":{"ram_usage":4294967296,"ram_total":17179869184},"temp":{"cpu_temp_avg":42.5,"gpu_temp_avg":39.5}}' + sleep 0.05 +done +EOF + +cat > "$FAKE_BIN/pgrep" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + +cat > "$FAKE_BIN/stat" <<'EOF' +#!/usr/bin/env bash + +# Reproduce GNU stat's partial stdout when passed the BSD-style -f probe. +if [ "${1:-}" = '-f' ]; then + printf 'filesystem details for %s\n' "${3:-unknown}" + exit 1 +fi + +if [ "${1:-}" = '-c' ] && [ "${2:-}" = '%Y' ]; then + if modified="$(/usr/bin/stat -c %Y "$3" 2>/dev/null)"; then + printf '%s\n' "$modified" + else + /usr/bin/stat -f %m "$3" + fi + exit 0 +fi + +exit 2 +EOF + +cat > "$FAKE_BIN/sketchybar" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$SKETCHYBAR_LOG" +EOF + +chmod +x "$FAKE_BIN/macmon" "$FAKE_BIN/pgrep" "$FAKE_BIN/sketchybar" "$FAKE_BIN/stat" + +export CONFIG_DIR +export MACMON_BIN="$FAKE_BIN/macmon" +export PATH="$FAKE_BIN:/usr/bin:/bin" + +"$REPO_ROOT/sketchybar/plugins/macmon_stream.sh" --start + +for _attempt in 1 2 3 4 5 6 7 8 9 10; do + [ -s "$CONFIG_DIR/.state/macmon.json" ] && break + sleep 0.05 +done +[ -s "$CONFIG_DIR/.state/macmon.json" ] || fail 'macmon stream did not write a local sample' + +first_pid="$(cat "$CONFIG_DIR/.state/macmon-stream.pid")" +"$REPO_ROOT/sketchybar/plugins/macmon_stream.sh" --start +second_pid="$(cat "$CONFIG_DIR/.state/macmon-stream.pid")" +[ "$first_pid" = "$second_pid" ] || fail 'a second sampler was started' + +SKETCHYBAR_LOG="$TEST_ROOT/sketchybar.log" +export SKETCHYBAR_LOG +SKETCHYBAR_BIN="$FAKE_BIN/sketchybar" \ + MACMON_DATA_FILE="$CONFIG_DIR/.state/macmon.json" \ + "$REPO_ROOT/sketchybar/plugins/system_metrics.sh" + +grep -F 'icon=37.5%' "$SKETCHYBAR_LOG" >/dev/null || fail 'CPU percentage was not formatted' +grep -F 'icon=50.0%' "$SKETCHYBAR_LOG" >/dev/null || fail 'GPU percentage was not formatted' +grep -F 'icon=25.0%' "$SKETCHYBAR_LOG" >/dev/null || fail 'RAM percentage was not calculated' +grep -F 'label= 4.0G' "$SKETCHYBAR_LOG" >/dev/null || fail 'used RAM was not formatted' + +stale_sample="$TEST_ROOT/stale.json" +cp "$CONFIG_DIR/.state/macmon.json" "$stale_sample" +touch -t 200001010000 "$stale_sample" +: > "$SKETCHYBAR_LOG" +SKETCHYBAR_BIN="$FAKE_BIN/sketchybar" \ + MACMON_DATA_FILE="$stale_sample" \ + "$REPO_ROOT/sketchybar/plugins/system_metrics.sh" +grep -F 'icon=--.-%' "$SKETCHYBAR_LOG" >/dev/null || fail 'stale metrics were still displayed' + +kill "$first_pid" 2>/dev/null || true +for _attempt in 1 2 3 4 5 6 7 8 9 10; do + [ ! -e "$CONFIG_DIR/.state/macmon-stream.pid" ] && break + sleep 0.05 +done +touch -t 200001010000 "$CONFIG_DIR/.state/macmon.json" +SKETCHYBAR_BIN="$FAKE_BIN/sketchybar" \ + MACMON_DATA_FILE="$CONFIG_DIR/.state/macmon.json" \ + "$REPO_ROOT/sketchybar/plugins/system_metrics.sh" +for _attempt in 1 2 3 4 5 6 7 8 9 10; do + [ -s "$CONFIG_DIR/.state/macmon-stream.pid" ] && break + sleep 0.05 +done +[ -s "$CONFIG_DIR/.state/macmon-stream.pid" ] || fail 'stale metrics did not restart the macmon stream' +replacement_pid="$(cat "$CONFIG_DIR/.state/macmon-stream.pid")" +[ "$replacement_pid" != "$first_pid" ] || fail 'macmon stream restart reused the stopped process' + +mkdir -p "$CONFIG_DIR/icons" +touch \ + "$CONFIG_DIR/icons/cpu.png" \ + "$CONFIG_DIR/icons/gpu-rotated-270.png" \ + "$CONFIG_DIR/icons/ram-rotated-270.png" \ + "$CONFIG_DIR/icons/network-arrows.png" +: > "$SKETCHYBAR_LOG" +PLUGIN_DIR="$REPO_ROOT/sketchybar/plugins" \ + /bin/bash "$REPO_ROOT/sketchybar/items/system_metrics.sh" +for image in cpu.png gpu-rotated-270.png ram-rotated-270.png; do + grep -F "background.image=$CONFIG_DIR/icons/$image" "$SKETCHYBAR_LOG" >/dev/null \ + || fail "$image was not used for its hardware metric" +done +grep -F 'background.image.scale=0.16' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'hardware metric images did not use their original scale' +grep -F "background.image=$CONFIG_DIR/icons/network-arrows.png" "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'network-arrows.png was not used for the network metric' +grep -F 'background.image.scale=0.13' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'network image did not use its original scale' + +rm -rf "$CONFIG_DIR/icons" +: > "$SKETCHYBAR_LOG" +PLUGIN_DIR="$REPO_ROOT/sketchybar/plugins" \ + /bin/bash "$REPO_ROOT/sketchybar/items/system_metrics.sh" +for fallback in 'icon=CPU' 'icon=GPU' 'icon=RAM' 'icon=NET'; do + grep -F "$fallback" "$SKETCHYBAR_LOG" >/dev/null \ + || fail "$fallback was not used when its image was unavailable" +done + +mv "$FAKE_BIN/macmon" "$FAKE_BIN/macmon.disabled" +: > "$SKETCHYBAR_LOG" +PLUGIN_DIR="$REPO_ROOT/sketchybar/plugins" \ + /bin/bash "$REPO_ROOT/sketchybar/items/system_metrics.sh" +mv "$FAKE_BIN/macmon.disabled" "$FAKE_BIN/macmon" +grep -F -- '--add item network.text' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'network metric was hidden when macmon was unavailable' +if grep -F -- '--add item cpu.text' "$SKETCHYBAR_LOG" >/dev/null; then + fail 'hardware metric placeholders were added without macmon' +fi + +printf 'Local macmon stream and metric formatting passed.\n' diff --git a/tests/sketchybar_network_test.sh b/tests/sketchybar_network_test.sh new file mode 100755 index 0000000..6c79313 --- /dev/null +++ b/tests/sketchybar_network_test.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-network-test.XXXXXX")" +FAKE_BIN="$TEST_ROOT/bin" +CONFIG_DIR="$TEST_ROOT/config" +SKETCHYBAR_LOG="$TEST_ROOT/sketchybar.log" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +mkdir -p "$FAKE_BIN" + +cat > "$FAKE_BIN/route" <<'EOF' +#!/usr/bin/env bash +exit 1 +EOF + +cat > "$FAKE_BIN/ifconfig" <<'EOF' +#!/usr/bin/env bash +cat <<'OUTPUT' +en0: flags=0 + inet 192.0.2.1 netmask 0xffffff00 + status: inactive +en7: flags=1 + inet 198.51.100.2 netmask 0xffffff00 + status: active +lo0: flags=1 + inet 127.0.0.1 netmask 0xff000000 + status: active +OUTPUT +EOF + +cat > "$FAKE_BIN/date" <<'EOF' +#!/usr/bin/env bash +count="$(cat "$NETWORK_DATE_COUNT" 2>/dev/null || printf '0')" +count=$((count + 1)) +printf '%s\n' "$count" > "$NETWORK_DATE_COUNT" +if [ "$count" -eq 1 ]; then + printf '100\n' +else + printf '105\n' +fi +EOF + +cat > "$FAKE_BIN/netstat" <<'EOF' +#!/usr/bin/env bash +count="$(cat "$NETWORK_COUNTER_COUNT" 2>/dev/null || printf '0')" +count=$((count + 1)) +printf '%s\n' "$count" > "$NETWORK_COUNTER_COUNT" +if [ "$count" -eq 1 ]; then + rx=1000000 + tx=2000000 +else + rx=6242880 + tx=7242880 +fi + +printf 'Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll\n' +printf 'en7 1500 link addr 1 0 %s 1 0 %s 0\n' "$rx" "$tx" +printf 'en7 1500 inet addr 1 - %s 1 - %s -\n' "$rx" "$tx" +printf 'en7 1500 inet6 addr 1 - %s 1 - %s -\n' "$rx" "$tx" +EOF + +cat > "$FAKE_BIN/sketchybar" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$SKETCHYBAR_LOG" +EOF + +chmod +x "$FAKE_BIN/date" "$FAKE_BIN/ifconfig" "$FAKE_BIN/netstat" \ + "$FAKE_BIN/route" "$FAKE_BIN/sketchybar" +export PATH="$FAKE_BIN:/usr/bin:/bin" +export CONFIG_DIR +export NETWORK_DATE_COUNT="$TEST_ROOT/date-count" +export NETWORK_COUNTER_COUNT="$TEST_ROOT/counter-count" +export SKETCHYBAR_BIN="$FAKE_BIN/sketchybar" +export SKETCHYBAR_LOG + +"$REPO_ROOT/sketchybar/plugins/network_speed.sh" +"$REPO_ROOT/sketchybar/plugins/network_speed.sh" + +read -r _ interface _ _ < "$CONFIG_DIR/.state/network_speed" +[ "$interface" = 'en7' ] || fail 'the active interface fallback selected the wrong device' +grep -F 'icon=1.0MB/s label=1.0MB/s' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'duplicate netstat rows were counted more than once' + +printf 'SketchyBar network counter handling passed.\n' diff --git a/tests/sketchybar_spaces_test.sh b/tests/sketchybar_spaces_test.sh new file mode 100755 index 0000000..c0311ef --- /dev/null +++ b/tests/sketchybar_spaces_test.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-spaces-test.XXXXXX")" +FAKE_BIN="$TEST_ROOT/bin" +STATE_DIR="$TEST_ROOT/state" +SKETCHYBAR_LOG="$TEST_ROOT/sketchybar.log" +JQ_BIN="$(command -v jq)" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +mkdir -p "$FAKE_BIN" "$STATE_DIR" +: > "$SKETCHYBAR_LOG" + +cat > "$FAKE_BIN/yabai" <<'EOF' +#!/usr/bin/env bash + +if [ "${SPACE_TEST_QUERY_FAIL:-0}" = '1' ]; then + exit 1 +fi + +case "$*" in + '-m query --spaces') + if [ "${SPACE_TEST_FULL_QUERY_TRUNCATED:-0}" = '1' ]; then + printf '%s\n' '[' + else + printf '%s\n' '[{"index":1},{"index":3}]' + fi + ;; + '-m query --spaces --display 1') + printf '%s\n' '[{"index":1},{"index":3}]' + ;; + '-m query --spaces --display 2') + exit 1 + ;; + '-m query --spaces --space 1') + printf '%s\n' '{"index":1,"display":1}' + ;; + '-m query --spaces --space 3') + printf '%s\n' '{"index":3,"display":1}' + ;; + '-m query --windows --space 1') + printf '%s\n' '[]' + ;; + '-m query --windows --space 3') + printf '%s\n' '[{"app":"ghostty","space":3,"display":1,"role":"","root-window":true,"has-ax-reference":false,"is-minimized":false,"is-hidden":false,"is-sticky":false,"frame":{"x":0,"y":0},"stack-index":0,"id":99}]' + ;; +esac +EOF + +cat > "$FAKE_BIN/sketchybar" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$SKETCHYBAR_LOG" +EOF + +chmod +x "$FAKE_BIN/yabai" "$FAKE_BIN/sketchybar" +export PATH="$FAKE_BIN:/usr/bin:/bin" +export CONFIG_DIR="$REPO_ROOT/sketchybar" +export SKETCHYBAR_STATE_DIR="$STATE_DIR" +export SKETCHYBAR_LOG +export JQ_BIN + +YABAI_BIN=/definitely/missing \ + "$REPO_ROOT/sketchybar/plugins/space_windows.sh" --reconcile +[ ! -s "$SKETCHYBAR_LOG" ] || fail 'space items were added without yabai' + +YABAI_BIN="$FAKE_BIN/yabai" \ + "$REPO_ROOT/sketchybar/plugins/space_windows.sh" --reconcile +grep -F -- '--add space space.1 left' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'Space 1 was not added' +grep -F -- '--add space space.3 left' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'Space 3 was not added' +if grep -F -- '--add space space.2 left' "$SKETCHYBAR_LOG" >/dev/null; then + fail 'a non-existent fallback Space was added' +fi + +: > "$SKETCHYBAR_LOG" +rm -f "$STATE_DIR/space_items" +SPACE_TEST_FULL_QUERY_TRUNCATED=1 \ + YABAI_BIN="$FAKE_BIN/yabai" \ + "$REPO_ROOT/sketchybar/plugins/space_windows.sh" --reconcile +grep -F -- '--add space space.1 left' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'per-display fallback did not recover from a truncated yabai response' + +: > "$SKETCHYBAR_LOG" +SPACE_TEST_QUERY_FAIL=1 \ + YABAI_BIN="$FAKE_BIN/yabai" \ + "$REPO_ROOT/sketchybar/plugins/space_windows.sh" --reconcile +[ ! -s "$SKETCHYBAR_LOG" ] || fail 'a transient yabai failure changed the Space items' + +: > "$SKETCHYBAR_LOG" +SENDER=space_windows_change \ + INFO='{"space":3,"apps":{}}' \ + YABAI_BIN="$FAKE_BIN/yabai" \ + "$REPO_ROOT/sketchybar/plugins/space_windows.sh" +grep -F -- ':ghostty:' "$SKETCHYBAR_LOG" >/dev/null \ + || fail 'the changed Space was not refreshed' +if grep -F -- '--set space.1 label=' "$SKETCHYBAR_LOG" >/dev/null; then + fail 'an unchanged Space was needlessly redrawn' +fi + +printf 'SketchyBar Space reconciliation passed.\n' diff --git a/tests/theme_validation_test.sh b/tests/theme_validation_test.sh new file mode 100755 index 0000000..05b2868 --- /dev/null +++ b/tests/theme_validation_test.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-theme-test.XXXXXX")" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +export HOME="$TEST_ROOT/home" +export XDG_CONFIG_HOME="$HOME/.config" +export XDG_DATA_HOME="$HOME/.local/share" +export XDG_STATE_HOME="$HOME/.local/state" +export XDG_CACHE_HOME="$HOME/.cache" +export MACARCHY_THEMES_DIR="$REPO_ROOT/themes" + +mkdir -p "$HOME" + +# These modules only parse and validate data. None of the theme application +# modules are loaded by this test. +# shellcheck source=../lib/macarchy/common.sh +source "$REPO_ROOT/lib/macarchy/common.sh" +# shellcheck source=../lib/macarchy/theme.sh +source "$REPO_ROOT/lib/macarchy/theme.sh" +# shellcheck source=../lib/macarchy/colours.sh +source "$REPO_ROOT/lib/macarchy/colours.sh" +# shellcheck source=../lib/macarchy/apply/raycast.sh +source "$REPO_ROOT/lib/macarchy/apply/raycast.sh" +# shellcheck source=../lib/macarchy/apply/tools.sh +source "$REPO_ROOT/lib/macarchy/apply/tools.sh" +# shellcheck source=../lib/macarchy/apply/desktop.sh +source "$REPO_ROOT/lib/macarchy/apply/desktop.sh" + +shopt -s nullglob +theme_files=("$MACARCHY_THEMES_DIR"/*/theme.env) +ghostty_files=("$MACARCHY_THEMES_DIR"/*/ghostty.conf) + +[ "${#theme_files[@]}" -gt 0 ] || fail 'no theme.env files were found' +[ "${#ghostty_files[@]}" -eq "${#theme_files[@]}" ] || \ + fail 'each theme must have one Ghostty colour file' + +for theme_file in "${theme_files[@]}"; do + theme_dir="$(dirname "$theme_file")" + theme_name="$(basename "$theme_dir")" + + macarchy_theme_load "$theme_name" || fail "invalid theme data: $theme_file" + [ -z "$THEME_WALLPAPER" ] || [ -f "$theme_dir/$THEME_WALLPAPER" ] || \ + fail "missing wallpaper for $theme_name: $THEME_WALLPAPER" +done + +for ghostty_file in "${ghostty_files[@]}"; do + macarchy_ghostty_theme_valid "$ghostty_file" || \ + fail "invalid Ghostty colour data: $ghostty_file" +done + +# Locally preserved copies of the original eight packs remain valid and keep +# their original names. These small fixtures exercise their data format without +# requiring the old wallpaper assets in the public repository. +LEGACY_THEMES="$TEST_ROOT/legacy-themes" +write_legacy_theme() { + local name="$1" + local foreground="$2" + local accent="${3:-}" + local accent_index="${4:-4}" + shift 4 + + mkdir -p "$LEGACY_THEMES/$name" + printf '%s\n' \ + 'DARK_MODE=true' \ + 'WALLPAPER=""' \ + 'NVIM_COLORSCHEME="legacy"' \ + 'BORDER_ACTIVE="0xff112233"' \ + 'BORDER_INACTIVE="0x00000000"' \ + 'BORDER_WIDTH="6.0"' > "$LEGACY_THEMES/$name/theme.env" + if [ -n "$accent" ]; then + printf 'FIREFOX_ACCENT="%s"\n' "$accent" >> "$LEGACY_THEMES/$name/theme.env" + fi + printf '%s\n' \ + 'background = #101010' \ + "foreground = $foreground" \ + 'palette = 0=#101010' \ + 'palette = 4=#445566' \ + "palette = $accent_index=${accent:-#112233}" > "$LEGACY_THEMES/$name/ghostty.conf" + if [ "$#" -gt 0 ]; then + printf '%s\n' "$@" >> "$LEGACY_THEMES/$name/ghostty.conf" + fi +} + +write_legacy_theme awakening '#dacbe6' '#ffff00' 2 'background-opacity = 0.85' +write_legacy_theme blackgold '#ebdbb2' '#bfa75d' 5 +write_legacy_theme carbonfox '#f2f4f8' '#78a9ff' 4 +write_legacy_theme city-783 '#b9bec6' '#eceff2' 15 'background-opacity = 0.85' +write_legacy_theme lumon '#d6e2ee' '' 12 +write_legacy_theme matte-black '#bebebe' '' 4 +write_legacy_theme midnight '#F0F6FF' '#66CCFF' 6 \ + 'window-padding-y = 8,8' \ + 'window-padding-x = 8,8' \ + 'window-padding-balance = true' +write_legacy_theme turbonite '#eae9e3' '#ed9a1d' 11 + +MACARCHY_THEMES_DIR="$LEGACY_THEMES" +expected_legacy_themes=$'awakening\nblackgold\ncarbonfox\ncity-783\nlumon\nmatte-black\nmidnight\nturbonite' +[ "$(macarchy_theme_list)" = "$expected_legacy_themes" ] || \ + fail 'legacy theme names changed while listing them' +for theme_name in awakening blackgold carbonfox city-783 lumon matte-black midnight turbonite; do + macarchy_theme_load "$theme_name" || fail "legacy theme format was rejected: $theme_name" + macarchy_ghostty_theme_valid "$LEGACY_THEMES/$theme_name/ghostty.conf" || \ + fail "legacy Ghostty settings were rejected: $theme_name" +done +mkdir -p "$(dirname "$MACARCHY_THEME_STATE")" +printf '%s\n' lumon > "$MACARCHY_THEME_STATE" +[ "$(macarchy_theme_cycle next)" = matte-black ] || fail 'theme cycling renamed an installed Lumon pack' +printf '%s\n' turbonite > "$MACARCHY_THEME_STATE" +[ "$(macarchy_theme_cycle next)" = awakening ] || fail 'theme cycling renamed an installed Turbonite pack' +rm -f "$MACARCHY_THEME_STATE" + +INVALID_GHOSTTY="$TEST_ROOT/invalid-ghostty.conf" +for invalid_setting in \ + 'background-opacity = 1.1' \ + 'window-padding-x = -1' \ + 'window-padding-y = 8,unsafe' \ + 'window-padding-balance = yes' \ + 'font-family = unexpected'; do + printf '%s\n' 'background = #101010' "$invalid_setting" > "$INVALID_GHOSTTY" + if macarchy_ghostty_theme_valid "$INVALID_GHOSTTY"; then + fail "unsafe Ghostty value was accepted: $invalid_setting" + fi +done + +# Runtime state belongs outside a theme pack so separately cloned or read-only +# packs are not modified when a theme is selected. +[ "$MACARCHY_THEME_STATE" = "$XDG_STATE_HOME/macarchy/themes/current" ] || \ + fail 'active theme state is stored inside the theme pack' +[ "$MACARCHY_BACKGROUND_STATE" = "$XDG_STATE_HOME/macarchy/themes/backgrounds.json" ] || \ + fail 'background state is stored inside the theme pack' + +# A damaged preferences file must be preserved for inspection rather than +# replaced with empty output from a failed jq command. +RUNTIME_THEMES="$TEST_ROOT/runtime-themes" +mkdir -p "$RUNTIME_THEMES/demo" "$RUNTIME_THEMES/cool-blue" "$(dirname "$MACARCHY_BACKGROUND_STATE")" +: > "$RUNTIME_THEMES/demo/wallpaper.jpg" +MACARCHY_THEMES_DIR="$RUNTIME_THEMES" +MACARCHY_THEME_NAME=demo +MACARCHY_THEME_DIR="$RUNTIME_THEMES/demo" +MACARCHY_LEGACY_THEME_STATE="$RUNTIME_THEMES/.current" +MACARCHY_LEGACY_BACKGROUND_STATE="$RUNTIME_THEMES/.backgrounds.json" +printf '%s\n' demo > "$MACARCHY_LEGACY_THEME_STATE" +[ "$(macarchy_theme_current)" = demo ] || fail 'legacy active theme state was not read' +printf '%s\n' lumon > "$MACARCHY_LEGACY_THEME_STATE" +[ "$(macarchy_theme_current)" = cool-blue ] || fail 'renamed Lumon state was not mapped to Cool Blue' +mkdir -p "$RUNTIME_THEMES/lumon" +[ "$(macarchy_theme_current)" = lumon ] || fail 'an installed Lumon pack was renamed to Cool Blue' + +printf '%s\n' '{"other":"kept.jpg"}' > "$MACARCHY_LEGACY_BACKGROUND_STATE" +macarchy_theme_save_background wallpaper.jpg || fail 'legacy background state was not migrated' +[ "$(jq -r '.other' "$MACARCHY_BACKGROUND_STATE")" = 'kept.jpg' ] || \ + fail 'legacy background choices were lost during migration' + +printf '%s\n' 'not valid json' > "$MACARCHY_BACKGROUND_STATE" +cp "$MACARCHY_BACKGROUND_STATE" "$TEST_ROOT/backgrounds.before" +if macarchy_theme_save_background wallpaper.jpg >/dev/null 2>&1; then + fail 'invalid background state was accepted' +fi +cmp -s "$TEST_ROOT/backgrounds.before" "$MACARCHY_BACKGROUND_STATE" || \ + fail 'invalid background state was overwritten' + +printf '%s\n' '{}' > "$MACARCHY_BACKGROUND_STATE" +macarchy_theme_save_background wallpaper.jpg || fail 'valid background state was not updated' +[ "$(jq -r '.demo' "$MACARCHY_BACKGROUND_STATE")" = 'wallpaper.jpg' ] || \ + fail 'background selection was not saved' +printf '%s\n' demo | macarchy_write_generated "$MACARCHY_THEME_STATE" || \ + fail 'active theme state was not recorded' + +# Generated integration files are checksummed for safe installer cleanup. The +# btop adapter must never rewrite the user's main btop configuration. +MACARCHY_THEMES_DIR="$LEGACY_THEMES" +macarchy_theme_load awakening || fail 'Awakening fixture could not be loaded for adapter tests' + +macarchy_apply_shell >/dev/null || fail 'shell theme state could not be generated' +[ -f "$MACARCHY_GENERATED_DIR/shell-env" ] || fail 'shell state was not written under generated config' +[ ! -e "$MACARCHY_THEMES_DIR/.shell-env" ] || fail 'shell state polluted the theme pack' +grep -F "MACARCHY_ACCENT_IDX='2'" "$MACARCHY_GENERATED_DIR/shell-env" >/dev/null || \ + fail 'legacy FIREFOX_ACCENT did not select its Ghostty palette index' +grep -F "MACARCHY_ACCENT='#ffff00'" "$MACARCHY_GENERATED_DIR/shell-env" >/dev/null || \ + fail 'legacy FIREFOX_ACCENT was not preserved in shell state' + +mkdir -p "$XDG_CONFIG_HOME/sketchybar" +sketchybar() { :; } +macarchy_apply_sketchybar >/dev/null || fail 'SketchyBar theme state could not be generated' +unset -f sketchybar +grep -F 'TEXT_COLOR="0xffdacbe6"' "$XDG_CONFIG_HOME/sketchybar/theme.sh" >/dev/null || \ + fail 'SketchyBar text was not derived from the Ghostty foreground' + +mkdir -p "$XDG_CONFIG_HOME/btop" +printf '%s\n' 'color_theme = "my-own-theme"' 'update_ms = 5000' > "$XDG_CONFIG_HOME/btop/btop.conf" +cp "$XDG_CONFIG_HOME/btop/btop.conf" "$TEST_ROOT/btop.before" +pkill() { return 1; } +macarchy_apply_btop >/dev/null || fail 'btop theme could not be generated' +cmp -s "$TEST_ROOT/btop.before" "$XDG_CONFIG_HOME/btop/btop.conf" || \ + fail 'theme application rewrote btop.conf' +[ -f "$XDG_CONFIG_HOME/btop/themes/macarchy.theme" ] || fail 'generated btop theme is missing' + +while IFS=$'\t' read -r generated checksum; do + [ -f "$generated" ] || fail "manifest points at a missing generated file: $generated" + [ "$(macarchy_file_hash "$generated")" = "$checksum" ] || \ + fail "manifest checksum does not match: $generated" +done < "$MACARCHY_GENERATED_MANIFEST" +grep -F "$MACARCHY_BACKGROUND_STATE" "$MACARCHY_GENERATED_MANIFEST" >/dev/null || \ + fail 'background state is absent from the generated-file manifest' +grep -F "$MACARCHY_GENERATED_DIR/shell-env" "$MACARCHY_GENERATED_MANIFEST" >/dev/null || \ + fail 'shell state is absent from the generated-file manifest' +grep -F "$XDG_CONFIG_HOME/sketchybar/theme.sh" "$MACARCHY_GENERATED_MANIFEST" >/dev/null || \ + fail 'SketchyBar theme state is absent from the generated-file manifest' +grep -F "$XDG_CONFIG_HOME/btop/themes/macarchy.theme" "$MACARCHY_GENERATED_MANIFEST" >/dev/null || \ + fail 'btop theme is absent from the generated-file manifest' + +# Raycast follows the selected appearance while retaining sensible defaults for +# both modes. A stub keeps this test away from the machine's real preferences. +RAYCAST_DEFAULTS_LOG="$TEST_ROOT/raycast-defaults.log" +export RAYCAST_DEFAULTS_LOG +MACARCHY_DEFAULTS_BIN="$TEST_ROOT/defaults" +MACARCHY_RAYCAST_APP="$TEST_ROOT/Raycast.app" +export MACARCHY_DEFAULTS_BIN MACARCHY_RAYCAST_APP +mkdir -p "$MACARCHY_RAYCAST_APP" +printf '%s\n' '#!/bin/sh' 'printf "%s\n" "$*" >> "$RAYCAST_DEFAULTS_LOG"' > "$MACARCHY_DEFAULTS_BIN" +chmod +x "$MACARCHY_DEFAULTS_BIN" +uname() { printf 'Darwin\n'; } +macarchy_apply_raycast >/dev/null || fail 'Raycast appearance defaults could not be generated' +unset -f uname +grep -F 'write com.raycast.macos raycastCurrentThemeId -string bundled-raycast-dark' "$RAYCAST_DEFAULTS_LOG" >/dev/null || \ + fail 'Raycast current theme did not follow dark mode' +grep -F 'write com.raycast.macos raycastCurrentThemeIdDarkAppearance -string bundled-raycast-dark' "$RAYCAST_DEFAULTS_LOG" >/dev/null || \ + fail 'Raycast dark default was not restored' +grep -F 'write com.raycast.macos raycastCurrentThemeIdLightAppearance -string bundled-raycast-light' "$RAYCAST_DEFAULTS_LOG" >/dev/null || \ + fail 'Raycast light default was not restored' +unset MACARCHY_DEFAULTS_BIN MACARCHY_RAYCAST_APP RAYCAST_DEFAULTS_LOG + +if printf '%s\n' 'content' | macarchy_atomic_write /dev/null/macarchy-test >/dev/null 2>&1; then + fail 'an impossible atomic write reported success' +fi + +ORIGINAL_GENERATED_DIR="$MACARCHY_GENERATED_DIR" +MACARCHY_GENERATED_DIR=/dev/null/macarchy-generated +if macarchy_apply_shell >/dev/null 2>&1; then + fail 'theme adapter reported success after its generated write failed' +fi +MACARCHY_GENERATED_DIR="$ORIGINAL_GENERATED_DIR" + +# Prove that theme.env remains data rather than executable shell code. +UNSAFE_THEMES="$TEST_ROOT/unsafe-themes" +mkdir -p "$UNSAFE_THEMES/untrusted" +printf '%s\n' 'DARK_MODE=$(touch "$TEST_ROOT/should-not-exist")' > "$UNSAFE_THEMES/untrusted/theme.env" +MACARCHY_THEMES_DIR="$UNSAFE_THEMES" +if macarchy_theme_load untrusted >/dev/null 2>&1; then + fail 'an executable theme.env value passed validation' +fi +[ ! -e "$TEST_ROOT/should-not-exist" ] || fail 'theme.env content was executed' + +printf '%s\n' 'DARK_MODE=true' 'FIREFOX_ACCENT=blue' > "$UNSAFE_THEMES/untrusted/theme.env" +if macarchy_theme_load untrusted >/dev/null 2>&1; then + fail 'an invalid FIREFOX_ACCENT passed validation' +fi + +printf 'Validated %d themes without applying them.\n' "${#theme_files[@]}" diff --git a/tests/yabai_helpers_test.sh b/tests/yabai_helpers_test.sh new file mode 100755 index 0000000..fa018ea --- /dev/null +++ b/tests/yabai_helpers_test.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/macarchy-yabai-test.XXXXXX")" +FAKE_BIN="$TEST_ROOT/bin" +LOG_FILE="$TEST_ROOT/yabai.log" +JQ_BIN="$(command -v jq)" + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +assert_log() { + grep -F -- "$1" "$LOG_FILE" >/dev/null || fail "missing yabai call: $1" +} + +mkdir -p "$FAKE_BIN" "$TEST_ROOT/config/yabai" + +cat > "$FAKE_BIN/yabai" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$YABAI_TEST_LOG" + +case "$*" in + '-m query --spaces --space') + printf '%s\n' '{"index":3}' + ;; + '-m query --spaces') + printf '%s\n' '[{"id":7,"is-native-fullscreen":true}]' + ;; + '-m query --windows') + if [ "${GHOSTTY_TEST_MODE:-0}" = '1' ]; then + query_count="$(cat "$GHOSTTY_TEST_COUNT" 2>/dev/null || printf '0')" + query_count=$((query_count + 1)) + printf '%s\n' "$query_count" > "$GHOSTTY_TEST_COUNT" + + if [ "$query_count" -lt 3 ]; then + printf '%s\n' '[{"id":10,"app":"Ghostty","is-floating":false}]' + else + printf '%s\n' '[{"id":10,"app":"Ghostty","is-floating":false},{"id":99,"app":"Ghostty","is-floating":true}]' + fi + else + printf '%s\n' '[{"id":42,"pid":77,"app":"Example","is-minimized":true,"is-floating":false}]' + fi + ;; + '-m query --windows --window 42'|'-m query --windows --window') + printf '%s\n' '{"id":42,"pid":77,"app":"Example","is-minimized":false,"is-floating":false}' + ;; + '-m query --windows --window 99') + printf '%s\n' '{"id":99,"pid":88,"app":"Ghostty","is-minimized":false,"is-floating":true}' + ;; + '-m rule --list') + printf '%s\n' '[]' + ;; +esac +EOF + +cat > "$FAKE_BIN/open" <<'EOF' +#!/usr/bin/env bash +printf 'open %s\n' "$*" >> "$YABAI_TEST_LOG" +EOF + +cat > "$FAKE_BIN/sudo" <<'EOF' +#!/usr/bin/env bash +exit "${YABAI_TEST_SUDO_STATUS:-1}" +EOF + +cat > "$FAKE_BIN/sketchybar" <<'EOF' +#!/usr/bin/env bash +printf 'sketchybar %s\n' "$*" >> "$YABAI_TEST_LOG" +if [ "$*" = '--query bar' ]; then + printf '{"hidden":"%s"}\n' "${SKETCHYBAR_TEST_HIDDEN:-off}" +fi +EOF + +chmod +x "$FAKE_BIN/yabai" "$FAKE_BIN/open" "$FAKE_BIN/sudo" "$FAKE_BIN/sketchybar" +export PATH="$FAKE_BIN:/usr/bin:/bin" +export YABAI_BIN="$FAKE_BIN/yabai" +export JQ_BIN +export SKETCHYBAR_BIN="$FAKE_BIN/sketchybar" +export YABAI_CONFIG_DIR="$TEST_ROOT/config/yabai" +export YABAI_TEST_LOG="$LOG_FILE" +export XDG_CACHE_HOME="$TEST_ROOT/cache" + +printf '%s\n' '["Example"]' > "$YABAI_CONFIG_DIR/float_state.json" +YABAI_WINDOW_ID=42 "$REPO_ROOT/yabai/apply_float_state.sh" +assert_log '-m window 42 --toggle float' +if grep -F -- '--float' "$LOG_FILE" >/dev/null; then + fail 'obsolete --float syntax was used' +fi + +: > "$LOG_FILE" +YABAI_PROCESS_ID=77 "$REPO_ROOT/yabai/examples/restore_minimized_on_current_space.sh" +expected_order="$(printf '%s\n' \ + '-m window 42 --space 3' \ + '-m window --deminimize 42' \ + '-m window --focus 42')" +actual_order="$(grep '^-m window' "$LOG_FILE")" +[ "$actual_order" = "$expected_order" ] || fail 'move, deminimise and focus calls were out of order' + +: > "$LOG_FILE" +printf '%s\n' '[]' > "$YABAI_CONFIG_DIR/float_state.json" +"$REPO_ROOT/yabai/toggle_float_persist.sh" +assert_log '-m window --toggle float' +"$JQ_BIN" -e 'index("Example") != null' "$YABAI_CONFIG_DIR/float_state.json" >/dev/null \ + || fail 'floating app preference was not saved' + +: > "$LOG_FILE" +export GHOSTTY_TEST_MODE=1 +export GHOSTTY_TEST_COUNT="$TEST_ROOT/ghostty-query-count" +rm -f "$GHOSTTY_TEST_COUNT" +GHOSTTY_WINDOW_POLL_ATTEMPTS=4 \ + GHOSTTY_WINDOW_POLL_DELAY=0 \ + "$REPO_ROOT/yabai/launch_ghostty.sh" +unset GHOSTTY_TEST_MODE GHOSTTY_TEST_COUNT + +assert_log 'open -na Ghostty' +assert_log '-m window 99 --toggle float' +assert_log '-m window --focus 99' +if grep -F -- '-m window 10' "$LOG_FILE" >/dev/null; then + fail 'an existing Ghostty window was selected' +fi +[ "$(cat "$TEST_ROOT/ghostty-query-count")" -ge 3 ] \ + || fail 'Ghostty window creation was not polled' + +: > "$LOG_FILE" +"$REPO_ROOT/yabai/sync_float_rules.sh" +assert_log 'manage=off' +if grep -F -- 'grid=' "$LOG_FILE" >/dev/null; then + fail 'persistent float rule still sets a fixed grid' +fi + +: > "$LOG_FILE" +"$REPO_ROOT/yabai/swap_space_and_refresh.sh" 5 +assert_log '-m space --swap 5' +if grep -F -- '-m space --focus 5' "$LOG_FILE" >/dev/null; then + fail 'Space swap unexpectedly followed the target Space' +fi + +: > "$LOG_FILE" +SKETCHYBAR_TEST_HIDDEN=on "$REPO_ROOT/yabai/toggle_sketchybar_visibility.sh" +grep -F 'sketchybar --bar hidden=off' "$LOG_FILE" >/dev/null \ + || fail 'hidden SketchyBar was not shown' + +: > "$LOG_FILE" +SKETCHYBAR_TEST_HIDDEN=off "$REPO_ROOT/yabai/toggle_sketchybar_visibility.sh" +grep -F 'sketchybar --bar hidden=on' "$LOG_FILE" >/dev/null \ + || fail 'visible SketchyBar was not hidden' + +: > "$LOG_FILE" +"$REPO_ROOT/yabai/examples/restart_borders_after_fullscreen.sh" +grep -F '7' "$XDG_CACHE_HOME/yabai/borders-native-fullscreen-spaces" >/dev/null \ + || fail 'fullscreen helper did not record its initial state' + +: > "$LOG_FILE" +YABAI_TEST_SUDO_STATUS=1 "$REPO_ROOT/yabai/yabairc" +assert_log '-m signal --remove macarchy_load_sa' +assert_log '-m signal --remove macarchy_space_changed' +assert_log '-m signal --remove macarchy_restore_minimized' +assert_log '-m signal --remove borders_fullscreen_space_created' +assert_log '-m signal --remove borders_fullscreen_space_destroyed' +assert_log '-m signal --remove borders_fullscreen_space_changed' +assert_log '-m signal --remove macarchy_window_destroyed' +assert_log '-m signal --remove macarchy_window_minimized' +if grep -F -- 'event=application_activated' "$LOG_FILE" >/dev/null; then + fail 'minimised-window restoration was enabled by default' +fi +window_created_signal="$(grep -F 'label=macarchy_window_created' "$LOG_FILE")" +case "$window_created_signal" in + *trigger_sketchybar*) fail 'window creation still requested a duplicate full label refresh' ;; +esac +if grep -F -- 'label=macarchy_load_sa' "$LOG_FILE" >/dev/null; then + fail 'scripting-addition signal was added without a working sudoers rule' +fi +printf 'yabai helper command ordering and safety checks passed.\n' diff --git a/themes/.current b/themes/.current deleted file mode 100644 index 5bb09ea..0000000 --- a/themes/.current +++ /dev/null @@ -1 +0,0 @@ -awakening diff --git a/themes/turbonite/ghostty.conf b/themes/amber-metal/ghostty.conf similarity index 70% rename from themes/turbonite/ghostty.conf rename to themes/amber-metal/ghostty.conf index 2884eea..a59843f 100644 --- a/themes/turbonite/ghostty.conf +++ b/themes/amber-metal/ghostty.conf @@ -1,3 +1,7 @@ +# Adapted from HANCORE's MIT-licensed Turbonite theme. +# This renamed palette is not affiliated with Porsche or Porsche Design. +# https://github.com/HANCORE-linux/omarchy-turbonite-theme + background = #0B0C0C foreground = #eae9e3 cursor-color = #8e3a59 diff --git a/themes/turbonite/theme.env b/themes/amber-metal/theme.env similarity index 76% rename from themes/turbonite/theme.env rename to themes/amber-metal/theme.env index c8d201f..bf6f14a 100644 --- a/themes/turbonite/theme.env +++ b/themes/amber-metal/theme.env @@ -1,9 +1,8 @@ DARK_MODE=true -WALLPAPER="backgrounds/BG02.png" +WALLPAPER="" NVIM_COLORSCHEME="gruvbox" BORDER_ACTIVE="0xffed9a1d" BORDER_INACTIVE="0x00000000" BORDER_WIDTH="6.0" SKETCHYBAR_BAR_COLOR="0x00000000" SKETCHYBAR_TEXT_COLOR="0xffeae9e3" -FIREFOX_ACCENT="#ed9a1d" diff --git a/themes/awakening/backgrounds/0-awakening.png b/themes/awakening/backgrounds/0-awakening.png deleted file mode 100644 index 656675f..0000000 Binary files a/themes/awakening/backgrounds/0-awakening.png and /dev/null differ diff --git a/themes/awakening/backgrounds/2-gasmask-runner.jpg b/themes/awakening/backgrounds/2-gasmask-runner.jpg deleted file mode 100644 index 246a1d2..0000000 Binary files a/themes/awakening/backgrounds/2-gasmask-runner.jpg and /dev/null differ diff --git a/themes/awakening/backgrounds/4-street-signal.png b/themes/awakening/backgrounds/4-street-signal.png deleted file mode 100644 index 75b0e7f..0000000 Binary files a/themes/awakening/backgrounds/4-street-signal.png and /dev/null differ diff --git a/themes/awakening/ghostty.conf b/themes/awakening/ghostty.conf deleted file mode 100644 index efcfa36..0000000 --- a/themes/awakening/ghostty.conf +++ /dev/null @@ -1,73 +0,0 @@ -# .S_SSSs .S S. .S_SSSs .S S. sSSs .S_sSSs .S .S_sSSs sSSSSs -# .SS~SSSSS .SS SS. .SS~SSSSS .SS SS. d%%SP .SS~YS%%b .SS .SS~YS%%b d%%%%SP -# S%S SSSS S%S S%S S%S SSSS S%S S&S d%S' S%S `S%b S%S S%S `S%b d%S' -# S%S S%S S%S S%S S%S S%S S%S d*S S%S S%S S%S S%S S%S S%S S%S -# S%S SSSS%S S%S S%S S%S SSSS%S S&S .S*S S&S S%S S&S S&S S%S S&S S&S -# S&S SSS%S S&S S&S S&S SSS%S S&S_sdSSS S&S_Ss S&S S&S S&S S&S S&S S&S -# S&S S&S S&S S&S S&S S&S S&S~YSSY%b S&S~SP S&S S&S S&S S&S S&S S&S -# S&S S&S S&S S&S S&S S&S S&S `S% S&S S&S S&S S&S S&S S&S S&S sSSs -# S*S S&S S*S S*S S*S S&S S*S S% S*b S*S S*S S*S S*S S*S S*b `S%% -# S*S S*S S*S . S*S S*S S*S S*S S& S*S. S*S S*S S*S S*S S*S S*S S% -# S*S S*S S*S_sSs_S*S S*S S*S S*S S& SSSbs S*S S*S S*S S*S S*S SS_sSSS -# SSS S*S SSS~SSS~S*S SSS S*S S*S SS YSSP S*S SSS S*S S*S SSS Y~YSSY -# SP SP SP SP SP SP -# Y Y Y Y Y Y -# - -# ghostty.conf -# By OldJobobo (https://github.com/OldJobobo) -# - - -# Background and foreground colors -background = #141010 -foreground = #dacbe6 -background-opacity = 0.85 - -# Standard colors -palette = 0=#141010 -palette = 1=#f56728 -palette = 2=#ffff00 -palette = 3=#c92b50 -palette = 4=#2976d1 -palette = 5=#976add -palette = 6=#449392 -palette = 7=#dacbe6 - -# Bright colors -palette = 8=#ab9191 -palette = 9=#f88416 -palette = 10=#ead94d -palette = 11=#d42b5b -palette = 12=#979dd5 -palette = 13=#976add -palette = 14=#c0daf6 -palette = 15=#fffbd4 - -# ______ __ ______ -# /_____/\ /_/\ /_____/\ -# \:::_ \ \\:\ \ \:::_ \ \ -# \:\ \ \ \\:\ \ \:\ \ \ \ -# \:\ \ \ \\:\ \____\:\ \ \ \ -# \:\_\ \ \\:\/___/\\:\/.:| | -# ___\_____\/_\_____\/ \____/_/ ______ _______ ______ -# /________/\/_____/\ /_______/\ /_____/\ /_______/\ /_____/\ -# \__.::.__\/\:::_ \ \\::: _ \ \\:::_ \ \\::: _ \ \\:::_ \ \ -# /_\::\ \ \:\ \ \ \\::(_) \/_\:\ \ \ \\::(_) \/_\:\ \ \ \ -# \:.\::\ \ \:\ \ \ \\:: _ \ \\:\ \ \ \\:: _ \ \\:\ \ \ \ -# \: \ \ \ \:\_\ \ \\::(_) \ \\:\_\ \ \\::(_) \ \\:\_\ \ \ -# \_____\/ \_____\/ \_______\/ \_____\/ \_______\/ \_____\/ -# - -# ░░ ░░░ ░░░░░░░░ ░░ -# ▒ ▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒ -# ▓ ▓▓▓▓ ▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓ ▓ -# █ ████ ██ ████████ ████ █ -# ██ ███ ██ ██ -# -# ░ ░░░ ░░░ ░░░░ ░░░ ░░░░ ░░ -# ▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒ ▒ -# ▓▓▓▓▓▓▓ ▓▓ ▓▓▓▓ ▓▓ ▓▓▓ ▓▓▓▓ ▓▓ ▓▓▓ ▓▓▓▓ ▓ -# █ ████ ██ ████ ██ ████ ██ ████ ██ ████ ██ ████ █ -# ██ ████ ███ ████ ███ ████ ██ -# diff --git a/themes/awakening/theme.env b/themes/awakening/theme.env deleted file mode 100644 index a09a51f..0000000 --- a/themes/awakening/theme.env +++ /dev/null @@ -1,9 +0,0 @@ -DARK_MODE=true -WALLPAPER="backgrounds/0-awakening.png" -NVIM_COLORSCHEME="tokyonight-night" -BORDER_ACTIVE="0xffffff00" -BORDER_INACTIVE="0x00000000" -BORDER_WIDTH="6.0" -SKETCHYBAR_BAR_COLOR="0x00000000" -SKETCHYBAR_TEXT_COLOR="0xffdacbe6" -FIREFOX_ACCENT="#ffff00" diff --git a/themes/blackgold/backgrounds/BG2.png b/themes/blackgold/backgrounds/BG2.png deleted file mode 100644 index 3848269..0000000 Binary files a/themes/blackgold/backgrounds/BG2.png and /dev/null differ diff --git a/themes/blackgold/backgrounds/BG3.png b/themes/blackgold/backgrounds/BG3.png deleted file mode 100644 index ccc3d75..0000000 Binary files a/themes/blackgold/backgrounds/BG3.png and /dev/null differ diff --git a/themes/blackgold/ghostty.conf b/themes/blackgold/ghostty.conf index 978d1b2..c3d322f 100644 --- a/themes/blackgold/ghostty.conf +++ b/themes/blackgold/ghostty.conf @@ -1,4 +1,5 @@ -# Black Gold Theme for Ghostty +# Adapted from HANCORE's MIT-licensed Blackgold theme. +# https://github.com/HANCORE-linux/omarchy-blackgold-theme # primary background = #0D0D0D @@ -6,7 +7,7 @@ foreground = #ebdbb2 cursor-color = #a3850e cursor-text = #120f02 -# normal colors +# Normal colours palette = 0=#0D0D0D palette = 1=#D35F5F palette = 2=#a3850e @@ -16,7 +17,7 @@ palette = 5=#bfa75d palette = 6=#7A6A2C palette = 7=#F6F1DD -# bright colors +# Bright colours palette = 8=#303531 palette = 9=#D35F5F palette = 10=#a3850e diff --git a/themes/blackgold/theme.env b/themes/blackgold/theme.env index 569ba55..e1993c0 100644 --- a/themes/blackgold/theme.env +++ b/themes/blackgold/theme.env @@ -1,7 +1,6 @@ DARK_MODE=true -WALLPAPER="backgrounds/BG2.png" -NVIM_COLORSCHEME="blackgold" +WALLPAPER="" +NVIM_COLORSCHEME="gruvbox" BORDER_ACTIVE="0xffbfa75d" BORDER_INACTIVE="0x00000000" BORDER_WIDTH="6.0" -FIREFOX_ACCENT="#bfa75d" diff --git a/themes/carbonfox/backgrounds/( 4 ).jpg b/themes/carbonfox/backgrounds/( 4 ).jpg deleted file mode 100644 index 6c9b87c..0000000 Binary files a/themes/carbonfox/backgrounds/( 4 ).jpg and /dev/null differ diff --git a/themes/carbonfox/backgrounds/( 5 ).jpg b/themes/carbonfox/backgrounds/( 5 ).jpg deleted file mode 100644 index c15ad24..0000000 Binary files a/themes/carbonfox/backgrounds/( 5 ).jpg and /dev/null differ diff --git a/themes/carbonfox/ghostty.conf b/themes/carbonfox/ghostty.conf index 0259eac..cd5f3a4 100644 --- a/themes/carbonfox/ghostty.conf +++ b/themes/carbonfox/ghostty.conf @@ -1,8 +1,12 @@ -# Background and foreground colors +# Adapted from the MIT-licensed Carbonfox and Nightfox palettes. +# https://github.com/gchmel/carbonfox +# https://github.com/EdenEast/nightfox.nvim + +# Background and foreground colours background = #161616 foreground = #f2f4f8 -# Standard colors +# Standard colours palette = 0=#161616 palette = 1=#ee5396 palette = 2=#33b1ff @@ -12,7 +16,7 @@ palette = 5=#be95ff palette = 6=#25be6a palette = 7=#f2f4f8 -# Bright colors +# Bright colours palette = 8=#484848 palette = 9=#f16da6 palette = 10=#52bdff diff --git a/themes/carbonfox/theme.env b/themes/carbonfox/theme.env index afbe224..a00b1e0 100644 --- a/themes/carbonfox/theme.env +++ b/themes/carbonfox/theme.env @@ -1,9 +1,8 @@ DARK_MODE=true -WALLPAPER="backgrounds/( 4 ).jpg" -NVIM_COLORSCHEME="carbonfox" +WALLPAPER="" +NVIM_COLORSCHEME="tokyonight-night" BORDER_ACTIVE="0xff78a9ff" BORDER_INACTIVE="0x00000000" BORDER_WIDTH="6.0" SKETCHYBAR_BAR_COLOR="0x00000000" SKETCHYBAR_TEXT_COLOR="0xfff2f4f8" -FIREFOX_ACCENT="#78a9ff" diff --git a/themes/city-783/backgrounds/0-gasmask-triad.png b/themes/city-783/backgrounds/0-gasmask-triad.png deleted file mode 100644 index 7ab7867..0000000 Binary files a/themes/city-783/backgrounds/0-gasmask-triad.png and /dev/null differ diff --git a/themes/city-783/backgrounds/1-city-783.png b/themes/city-783/backgrounds/1-city-783.png deleted file mode 100644 index 0fb1ff9..0000000 Binary files a/themes/city-783/backgrounds/1-city-783.png and /dev/null differ diff --git a/themes/city-783/backgrounds/2-crimson-diver.png b/themes/city-783/backgrounds/2-crimson-diver.png deleted file mode 100644 index 3806433..0000000 Binary files a/themes/city-783/backgrounds/2-crimson-diver.png and /dev/null differ diff --git a/themes/city-783/backgrounds/3-crimson-corridor.jpg b/themes/city-783/backgrounds/3-crimson-corridor.jpg deleted file mode 100644 index 1b4628c..0000000 Binary files a/themes/city-783/backgrounds/3-crimson-corridor.jpg and /dev/null differ diff --git a/themes/city-783/backgrounds/4-red-monolith.jpeg b/themes/city-783/backgrounds/4-red-monolith.jpeg deleted file mode 100644 index e967265..0000000 Binary files a/themes/city-783/backgrounds/4-red-monolith.jpeg and /dev/null differ diff --git a/themes/city-783/ghostty.conf b/themes/city-783/ghostty.conf deleted file mode 100644 index 596a06e..0000000 --- a/themes/city-783/ghostty.conf +++ /dev/null @@ -1,64 +0,0 @@ -# Background and foreground colors -# base -background = #181a1f - -# text -foreground = #b9bec6 - -# text -selection-foreground = #b9bec6 - -# pine -selection-background = #ad2222 - -background-opacity = 0.85 - -# Standard colors -# base -palette = 0=#181a1f - -# red -palette = 1=#b31414 - -# green -palette = 2=#d12b2b - -# yellow -palette = 3=#9e1a1a - -# blue -palette = 4=#ad2222 - -# magenta -palette = 5=#c3c8d0 - -# cyan -palette = 6=#8f949c - -# text -palette = 7=#b9bec6 - -# Bright colors -# muted -palette = 8=#4b515b - -# red -palette = 9=#b31414 - -# green -palette = 10=#d12b2b - -# yellow -palette = 11=#9e1a1a - -# blue -palette = 12=#ad2222 - -# magenta -palette = 13=#c3c8d0 - -# cyan -palette = 14=#8f949c - -# text -palette = 15=#eceff2 diff --git a/themes/city-783/theme.env b/themes/city-783/theme.env deleted file mode 100644 index a948e9b..0000000 --- a/themes/city-783/theme.env +++ /dev/null @@ -1,9 +0,0 @@ -DARK_MODE=true -WALLPAPER="backgrounds/1-city-783.png" -NVIM_COLORSCHEME="rose-pine-moon" -BORDER_ACTIVE="0xffeceff2" -BORDER_INACTIVE="0x00000000" -BORDER_WIDTH="6.0" -SKETCHYBAR_BAR_COLOR="0x00000000" -SKETCHYBAR_TEXT_COLOR="0xffb9bec6" -FIREFOX_ACCENT="#eceff2" diff --git a/themes/lumon/ghostty.conf b/themes/cool-blue/ghostty.conf similarity index 71% rename from themes/lumon/ghostty.conf rename to themes/cool-blue/ghostty.conf index f1e6b22..0ed7819 100644 --- a/themes/lumon/ghostty.conf +++ b/themes/cool-blue/ghostty.conf @@ -1,3 +1,7 @@ +# Adapted from the MIT-licensed Lumon palette in Basecamp's Omarchy. +# This renamed palette is not affiliated with Apple or the Severance series. +# https://github.com/basecamp/omarchy + background = #16242d foreground = #d6e2ee cursor-color = #f2fcff diff --git a/themes/lumon/theme.env b/themes/cool-blue/theme.env similarity index 65% rename from themes/lumon/theme.env rename to themes/cool-blue/theme.env index 7ddd121..a1ee3b0 100644 --- a/themes/lumon/theme.env +++ b/themes/cool-blue/theme.env @@ -1,6 +1,6 @@ DARK_MODE=true -WALLPAPER="wall.jpg" -NVIM_COLORSCHEME="lumon" +WALLPAPER="" +NVIM_COLORSCHEME="rose-pine-moon" BORDER_ACTIVE="0xfff2fcff" BORDER_INACTIVE="0x00000000" BORDER_WIDTH="6.0" diff --git a/themes/lumon/backgrounds/01-united-in-severance.jpg b/themes/lumon/backgrounds/01-united-in-severance.jpg deleted file mode 100644 index d07b636..0000000 Binary files a/themes/lumon/backgrounds/01-united-in-severance.jpg and /dev/null differ diff --git a/themes/lumon/backgrounds/02-opinions-equally.jpg b/themes/lumon/backgrounds/02-opinions-equally.jpg deleted file mode 100644 index 4f2a0bf..0000000 Binary files a/themes/lumon/backgrounds/02-opinions-equally.jpg and /dev/null differ diff --git a/themes/lumon/wall.jpg b/themes/lumon/wall.jpg deleted file mode 100644 index d07b636..0000000 Binary files a/themes/lumon/wall.jpg and /dev/null differ diff --git a/themes/matte-black/backgrounds/0-ship-at-sea.jpg b/themes/matte-black/backgrounds/0-ship-at-sea.jpg deleted file mode 100644 index 4375ee2..0000000 Binary files a/themes/matte-black/backgrounds/0-ship-at-sea.jpg and /dev/null differ diff --git a/themes/matte-black/backgrounds/1-dark-waters.jpg b/themes/matte-black/backgrounds/1-dark-waters.jpg deleted file mode 100644 index 2afb1bc..0000000 Binary files a/themes/matte-black/backgrounds/1-dark-waters.jpg and /dev/null differ diff --git a/themes/matte-black/backgrounds/2-dot-hands.jpg b/themes/matte-black/backgrounds/2-dot-hands.jpg deleted file mode 100644 index b76792c..0000000 Binary files a/themes/matte-black/backgrounds/2-dot-hands.jpg and /dev/null differ diff --git a/themes/matte-black/ghostty.conf b/themes/matte-black/ghostty.conf index fedd88d..2e5de17 100644 --- a/themes/matte-black/ghostty.conf +++ b/themes/matte-black/ghostty.conf @@ -1,3 +1,6 @@ +# Adapted from the MIT-licensed Matte Black palette in Basecamp's Omarchy. +# https://github.com/basecamp/omarchy + background = #121212 foreground = #bebebe cursor-color = #eaeaea diff --git a/themes/matte-black/theme.env b/themes/matte-black/theme.env index 082928c..785dba2 100644 --- a/themes/matte-black/theme.env +++ b/themes/matte-black/theme.env @@ -1,6 +1,6 @@ DARK_MODE=true -WALLPAPER="wall.jpg" -NVIM_COLORSCHEME="matte-black" +WALLPAPER="" +NVIM_COLORSCHEME="gruvbox" BORDER_ACTIVE="0xffe68e0d" BORDER_INACTIVE="0x00000000" BORDER_WIDTH="6.0" diff --git a/themes/matte-black/wall.jpg b/themes/matte-black/wall.jpg deleted file mode 100644 index 4375ee2..0000000 Binary files a/themes/matte-black/wall.jpg and /dev/null differ diff --git a/themes/midnight/backgrounds/2-hand-of-adam.png b/themes/midnight/backgrounds/2-hand-of-adam.png deleted file mode 100644 index de9e949..0000000 Binary files a/themes/midnight/backgrounds/2-hand-of-adam.png and /dev/null differ diff --git a/themes/midnight/backgrounds/3-omarchy-window.png b/themes/midnight/backgrounds/3-omarchy-window.png deleted file mode 100644 index b197fb9..0000000 Binary files a/themes/midnight/backgrounds/3-omarchy-window.png and /dev/null differ diff --git a/themes/midnight/ghostty.conf b/themes/midnight/ghostty.conf deleted file mode 100644 index 81af23a..0000000 --- a/themes/midnight/ghostty.conf +++ /dev/null @@ -1,32 +0,0 @@ -background = #000000 -foreground = #F0F6FF - -cursor-color = #66CCFF -cursor-text = #000000 - -selection-background = #123A4D -selection-foreground = #F0F6FF - -window-padding-y = 8,8 -window-padding-x = 8,8 -window-padding-balance = true - -# normal colors -palette = 0=#050505 -palette = 1=#FF3366 -palette = 2=#33FFDD -palette = 3=#3399FF -palette = 4=#1E90FF -palette = 5=#00CFFF -palette = 6=#66CCFF -palette = 7=#F0F6FF - -# bright colors -palette = 8=#333333 -palette = 9=#FF6688 -palette = 10=#66FFE6 -palette = 11=#66B3FF -palette = 12=#66B2FF -palette = 13=#33DDFF -palette = 14=#99DDFF -palette = 15=#FFFFFF diff --git a/themes/midnight/theme.env b/themes/midnight/theme.env deleted file mode 100644 index d0a07e2..0000000 --- a/themes/midnight/theme.env +++ /dev/null @@ -1,7 +0,0 @@ -DARK_MODE=true -WALLPAPER="backgrounds/2-hand-of-adam.png" -NVIM_COLORSCHEME="midnight" -BORDER_INACTIVE="0x00000000" -BORDER_WIDTH="6.0" -BORDER_ACTIVE="0xff66CCFF" -FIREFOX_ACCENT="#66CCFF" diff --git a/themes/turbonite/backgrounds/BG02.png b/themes/turbonite/backgrounds/BG02.png deleted file mode 100644 index 43570a1..0000000 Binary files a/themes/turbonite/backgrounds/BG02.png and /dev/null differ diff --git a/themes/turbonite/backgrounds/BG05.png b/themes/turbonite/backgrounds/BG05.png deleted file mode 100644 index 0ad3701..0000000 Binary files a/themes/turbonite/backgrounds/BG05.png and /dev/null differ diff --git a/themes/turbonite/backgrounds/BG09.jpg b/themes/turbonite/backgrounds/BG09.jpg deleted file mode 100644 index e607a5d..0000000 Binary files a/themes/turbonite/backgrounds/BG09.jpg and /dev/null differ diff --git a/yabai/apply_float_state.sh b/yabai/apply_float_state.sh index dd3609b..31e9bbe 100755 --- a/yabai/apply_float_state.sh +++ b/yabai/apply_float_state.sh @@ -2,18 +2,28 @@ # Called by yabai signals (window_created, window_deminimized). # YABAI_WINDOW_ID is injected by yabai into the signal environment. -STATE_FILE="$HOME/.config/yabai/float_state.json" +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 + +window_id="${YABAI_WINDOW_ID:-}" +case "$window_id" in + ''|*[!0-9]*) exit 0 ;; +esac + +CONFIG_ROOT="${YABAI_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yabai}" +STATE_FILE="$CONFIG_ROOT/float_state.json" [[ -f "$STATE_FILE" ]] || exit 0 -window_json="$(yabai -m query --windows --window "$YABAI_WINDOW_ID" 2>/dev/null)" -app="$(jq -r '.app // empty' <<<"$window_json")" +window_json="$("$YABAI_BIN" -m query --windows --window "$window_id" 2>/dev/null)" +app="$("$JQ_BIN" -r '.app // empty' <<<"$window_json")" [[ -n "$app" ]] || exit 0 -in_state="$(jq --arg a "$app" 'map(select(. == $a)) | length' "$STATE_FILE" 2>/dev/null)" +in_state="$("$JQ_BIN" --arg a "$app" 'if type == "array" then map(select(. == $a)) | length else 0 end' "$STATE_FILE" 2>/dev/null)" [[ "$in_state" -gt 0 ]] || exit 0 -is_floating="$(jq -r '."is-floating"' <<<"$window_json")" +is_floating="$("$JQ_BIN" -r '."is-floating" // false' <<<"$window_json")" if [[ "$is_floating" != "true" ]]; then - yabai -m window "$YABAI_WINDOW_ID" --float 2>/dev/null || true - yabai -m window "$YABAI_WINDOW_ID" --grid 6:6:1:1:4:4 2>/dev/null || true + # yabai 7 exposes floating as a toggle, so guard it with the queried state. + "$YABAI_BIN" -m window "$window_id" --toggle float 2>/dev/null || true fi diff --git a/yabai/examples/restart_borders_after_fullscreen.sh b/yabai/examples/restart_borders_after_fullscreen.sh new file mode 100755 index 0000000..3a1ae6c --- /dev/null +++ b/yabai/examples/restart_borders_after_fullscreen.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +BREW_BIN="$(command -v "${BREW_BIN:-brew}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 + +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/yabai" +STATE_FILE="$CACHE_DIR/borders-native-fullscreen-spaces" +STAMP_FILE="$CACHE_DIR/borders-restart-after-fullscreen.stamp" +LOCK_DIR="$CACHE_DIR/borders-restart-after-fullscreen.lock" +mkdir -p "$CACHE_DIR" + +spaces_json="$("$YABAI_BIN" -m query --spaces 2>/dev/null || printf '[]')" +current_ids="$( + "$JQ_BIN" -r '.[] | select(."is-native-fullscreen" == true) | .id' <<<"$spaces_json" \ + | sort -n \ + | tr '\n' ' ' \ + | sed 's/[[:space:]]*$//' +)" +previous_ids="$(cat "$STATE_FILE" 2>/dev/null || true)" +state_tmp="$STATE_FILE.$$" +printf '%s\n' "$current_ids" > "$state_tmp" +mv "$state_tmp" "$STATE_FILE" + +restart_needed=false +current_padded=" $current_ids " +for space_id in $previous_ids; do + case "$current_padded" in + *" $space_id "*) ;; + *) restart_needed=true ;; + esac +done + +[ "$restart_needed" = "true" ] || exit 0 + +( + sleep 0.4 + + mkdir "$LOCK_DIR" 2>/dev/null || exit 0 + trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT + + now="$(date +%s)" + last="$(cat "$STAMP_FILE" 2>/dev/null || printf '0')" + case "$last" in + ''|*[!0-9]*) last=0 ;; + esac + if [ "$((now - last))" -lt 3 ]; then + exit 0 + fi + + printf '%s\n' "$now" > "$STAMP_FILE" + launchctl kickstart -k "gui/$(id -u)/homebrew.mxcl.borders" >/dev/null 2>&1 \ + || { [ -n "$BREW_BIN" ] && "$BREW_BIN" services restart borders >/dev/null 2>&1; } \ + || true +) & diff --git a/yabai/examples/restore_minimized_on_current_space.sh b/yabai/examples/restore_minimized_on_current_space.sh new file mode 100755 index 0000000..dab0c8e --- /dev/null +++ b/yabai/examples/restore_minimized_on_current_space.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# When an app is activated with only minimised windows, restore one on the +# currently focused Space instead of letting macOS revive it elsewhere. + +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 + +CONFIG_ROOT="${YABAI_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yabai}" +REFRESH_SCRIPT="$CONFIG_ROOT/trigger_sketchybar_space_labels_refresh.sh" + +pid="${YABAI_PROCESS_ID:-}" +case "$pid" in + ''|*[!0-9]*) exit 0 ;; +esac + +current_space="$("$YABAI_BIN" -m query --spaces --space 2>/dev/null | "$JQ_BIN" -r '.index // empty')" +case "$current_space" in + ''|*[!0-9]*) exit 0 ;; +esac + +windows_json="$("$YABAI_BIN" -m query --windows 2>/dev/null)" +[ -n "$windows_json" ] || exit 0 + +non_minimized_count="$( + "$JQ_BIN" --argjson pid "$pid" ' + [.[] | select(.pid == $pid and ."is-minimized" == false)] | length + ' <<<"$windows_json" 2>/dev/null +)" + +if [ "${non_minimized_count:-0}" -gt 0 ]; then + exit 0 +fi + +window_id="$( + "$JQ_BIN" -r --argjson pid "$pid" ' + [.[] | select(.pid == $pid and ."is-minimized" == true)] + | last.id // empty + ' <<<"$windows_json" 2>/dev/null +)" + +[ -n "$window_id" ] || exit 0 + +"$YABAI_BIN" -m window "$window_id" --space "$current_space" 2>/dev/null || true +"$YABAI_BIN" -m window --deminimize "$window_id" 2>/dev/null || true +"$YABAI_BIN" -m window --focus "$window_id" 2>/dev/null || true + +# The refresh script coalesces this with the window_deminimized signal. +[ -f "$REFRESH_SCRIPT" ] && /bin/bash "$REFRESH_SCRIPT" || true diff --git a/yabai/launch_ghostty.sh b/yabai/launch_ghostty.sh old mode 100644 new mode 100755 index 0bc5ebf..b2f4244 --- a/yabai/launch_ghostty.sh +++ b/yabai/launch_ghostty.sh @@ -2,15 +2,52 @@ set -euo pipefail +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 + +# Record existing Ghostty windows before launching. Picking the highest window +# ID after a fixed sleep can focus an older window when startup is slow. +before_ids="$("$YABAI_BIN" -m query --windows 2>/dev/null \ + | "$JQ_BIN" -c '[.[] | select((.app // "" | ascii_downcase) == "ghostty") | .id]' 2>/dev/null)" || exit 0 + open -na Ghostty -sleep 0.35 -id="$(yabai -m query --windows 2>/dev/null | jq -r 'map(select(.app == "Ghostty")) | sort_by(.id) | last | .id // empty')" +attempts="${GHOSTTY_WINDOW_POLL_ATTEMPTS:-30}" +delay="${GHOSTTY_WINDOW_POLL_DELAY:-0.1}" +case "$attempts" in + ''|*[!0-9]*|0) attempts=30 ;; +esac +case "$delay" in + ''|.|*[!0-9.]*|*.*.*) delay=0.1 ;; +esac + +id="" +while [ "$attempts" -gt 0 ]; do + windows_json="$("$YABAI_BIN" -m query --windows 2>/dev/null || printf '[]\n')" + id="$( + "$JQ_BIN" -r --argjson before "$before_ids" ' + map( + select( + ((.app // "" | ascii_downcase) == "ghostty") + and (.id as $id | ($before | index($id)) == null) + ) + ) + | sort_by(.id) + | last.id // empty + ' <<<"$windows_json" 2>/dev/null || true + )" + [ -n "$id" ] && break + + attempts=$((attempts - 1)) + [ "$attempts" -gt 0 ] && sleep "$delay" +done + [[ -n "$id" ]] || exit 0 -is_floating="$(yabai -m query --windows --window "$id" 2>/dev/null | jq -r '."is-floating" // false')" +is_floating="$("$YABAI_BIN" -m query --windows --window "$id" 2>/dev/null | "$JQ_BIN" -r '."is-floating" // false')" if [[ "$is_floating" == "true" ]]; then - yabai -m window "$id" --toggle float + "$YABAI_BIN" -m window "$id" --toggle float fi -yabai -m window --focus "$id" 2>/dev/null || true +"$YABAI_BIN" -m window --focus "$id" 2>/dev/null || true diff --git a/yabai/restore_minimized_on_current_space.sh b/yabai/restore_minimized_on_current_space.sh deleted file mode 100755 index ac9b963..0000000 --- a/yabai/restore_minimized_on_current_space.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# When an app is activated with only minimized windows, restore one on the -# currently focused Space instead of letting macOS revive it elsewhere. - -YABAI_BIN="${YABAI_BIN:-$(command -v yabai 2>/dev/null || true)}" -[ -x "$YABAI_BIN" ] || YABAI_BIN="/opt/homebrew/bin/yabai" - -pid="${YABAI_PROCESS_ID:-}" -[ -n "$pid" ] || exit 0 - -current_space="$("$YABAI_BIN" -m query --spaces --space 2>/dev/null | jq -r '.index // empty')" -[ -n "$current_space" ] || exit 0 - -windows_json="$("$YABAI_BIN" -m query --windows 2>/dev/null)" -[ -n "$windows_json" ] || exit 0 - -non_minimized_count="$( - jq --argjson pid "$pid" ' - [.[] | select(.pid == $pid and ."is-minimized" == false)] | length - ' <<<"$windows_json" 2>/dev/null -)" - -if [ "${non_minimized_count:-0}" -gt 0 ]; then - exit 0 -fi - -window_id="$( - jq -r --argjson pid "$pid" ' - [.[] | select(.pid == $pid and ."is-minimized" == true)] - | last.id // empty - ' <<<"$windows_json" 2>/dev/null -)" - -[ -n "$window_id" ] || exit 0 - -"$YABAI_BIN" -m window "$window_id" --space "$current_space" 2>/dev/null || true -"$YABAI_BIN" -m window "$window_id" --deminimize 2>/dev/null || true -"$YABAI_BIN" -m window "$window_id" --focus 2>/dev/null || true diff --git a/yabai/swap_space_and_refresh.sh b/yabai/swap_space_and_refresh.sh index f841022..03b7b3a 100755 --- a/yabai/swap_space_and_refresh.sh +++ b/yabai/swap_space_and_refresh.sh @@ -5,33 +5,23 @@ set -euo pipefail target_space="${1:-}" case "$target_space" in - ''|*[!0-9]*) + ''|*[!0-9]*|0) exit 1 ;; esac -YABAI_BIN="${YABAI_BIN:-$(command -v yabai 2>/dev/null || true)}" -[ -x "$YABAI_BIN" ] || YABAI_BIN="/opt/homebrew/bin/yabai" +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 -SKETCHYBAR_BIN="${SKETCHYBAR_BIN:-$(command -v sketchybar 2>/dev/null || true)}" -[ -x "$SKETCHYBAR_BIN" ] || SKETCHYBAR_BIN="/opt/homebrew/bin/sketchybar" +CONFIG_ROOT="${YABAI_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yabai}" +REFRESH_SCRIPT="$CONFIG_ROOT/trigger_sketchybar_space_labels_refresh.sh" -refresh_bar() { - /bin/bash "$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" "$target_space" - [ -x "$SKETCHYBAR_BIN" ] && "$SKETCHYBAR_BIN" --trigger yabai_space_labels_refresh >/dev/null 2>&1 || true -} - -current_space="$("$YABAI_BIN" -m query --spaces --space | jq -r '.index')" +current_space="$("$YABAI_BIN" -m query --spaces --space | "$JQ_BIN" -r '.index // empty')" +[ -n "$current_space" ] || exit 1 if [ "$current_space" != "$target_space" ]; then "$YABAI_BIN" -m space --swap "$target_space" - refresh_bar - sleep 0.20 - "$YABAI_BIN" -m space --focus "$target_space" fi -for delay in 0.05 0.20 0.50 -do - sleep "$delay" - refresh_bar -done +[ -f "$REFRESH_SCRIPT" ] && /bin/bash "$REFRESH_SCRIPT" || true diff --git a/yabai/sync_float_rules.sh b/yabai/sync_float_rules.sh index 14fb521..6ea3fe7 100755 --- a/yabai/sync_float_rules.sh +++ b/yabai/sync_float_rules.sh @@ -2,31 +2,37 @@ set -euo pipefail -STATE_FILE="${HOME}/.config/yabai/float_state.json" +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 + +CONFIG_ROOT="${YABAI_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yabai}" +STATE_FILE="$CONFIG_ROOT/float_state.json" LABEL_PREFIX="float_persist_" escape_regex() { printf '%s' "$1" | sed -E 's/[][(){}.^$*+?|\\-]/\\&/g' } +mkdir -p "$CONFIG_ROOT" [[ -f "$STATE_FILE" ]] || printf '[]\n' > "$STATE_FILE" -if ! jq empty "$STATE_FILE" >/dev/null 2>&1; then +if ! "$JQ_BIN" -e 'type == "array" and all(.[]; type == "string")' "$STATE_FILE" >/dev/null 2>&1; then printf '[]\n' > "$STATE_FILE" fi -rules_json="$(yabai -m rule --list 2>/dev/null || printf '[]\n')" +rules_json="$("$YABAI_BIN" -m rule --list 2>/dev/null || printf '[]\n')" -jq -r --arg prefix "$LABEL_PREFIX" ' +"$JQ_BIN" -r --arg prefix "$LABEL_PREFIX" ' .[] | select(((.label // "") | startswith($prefix))) | (.index // empty) ' <<<"$rules_json" | sort -rn | while IFS= read -r rule_index; do [[ -n "$rule_index" ]] || continue - yabai -m rule --remove "$rule_index" 2>/dev/null || true + "$YABAI_BIN" -m rule --remove "$rule_index" 2>/dev/null || true done -jq -r '.[]' "$STATE_FILE" | while IFS= read -r app_name; do +"$JQ_BIN" -r '.[]' "$STATE_FILE" | while IFS= read -r app_name; do [[ -n "$app_name" ]] || continue app_regex="$(escape_regex "$app_name")" - yabai -m rule --add app="^${app_regex}$" manage=off grid=6:6:1:1:4:4 label="${LABEL_PREFIX}${app_name}" + "$YABAI_BIN" -m rule --add app="^${app_regex}$" manage=off label="${LABEL_PREFIX}${app_name}" done diff --git a/yabai/toggle_float_persist.sh b/yabai/toggle_float_persist.sh index 2c6e76e..975f1ee 100755 --- a/yabai/toggle_float_persist.sh +++ b/yabai/toggle_float_persist.sh @@ -2,37 +2,43 @@ set -euo pipefail -STATE_FILE="${HOME}/.config/yabai/float_state.json" -SYNC_SCRIPT="${HOME}/.config/yabai/sync_float_rules.sh" +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 + +CONFIG_ROOT="${YABAI_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yabai}" +STATE_FILE="$CONFIG_ROOT/float_state.json" +SYNC_SCRIPT="$CONFIG_ROOT/sync_float_rules.sh" mkdir -p "$(dirname "$STATE_FILE")" [[ -f "$STATE_FILE" ]] || printf '[]\n' > "$STATE_FILE" -if ! jq empty "$STATE_FILE" >/dev/null 2>&1; then +if ! "$JQ_BIN" -e 'type == "array" and all(.[]; type == "string")' "$STATE_FILE" >/dev/null 2>&1; then printf '[]\n' > "$STATE_FILE" fi -window_json="$(yabai -m query --windows --window)" -app_name="$(jq -r '.app // empty' <<<"$window_json")" -is_floating="$(jq -r '."is-floating" // false' <<<"$window_json")" +window_json="$("$YABAI_BIN" -m query --windows --window)" +app_name="$("$JQ_BIN" -r '.app // empty' <<<"$window_json")" +is_floating="$("$JQ_BIN" -r '."is-floating" // false' <<<"$window_json")" if [[ -z "$app_name" ]]; then exit 0 fi -yabai -m window --toggle float +"$YABAI_BIN" -m window --toggle float tmp_file="$(mktemp)" +trap 'rm -f "$tmp_file"' EXIT if [[ "$is_floating" == "true" ]]; then - jq --arg app "$app_name" 'map(select(. != $app))' "$STATE_FILE" > "$tmp_file" + "$JQ_BIN" --arg app "$app_name" 'map(select(. != $app))' "$STATE_FILE" > "$tmp_file" else - yabai -m window --grid 6:6:1:1:4:4 - jq --arg app "$app_name" ' + "$JQ_BIN" --arg app "$app_name" ' (if type == "array" then . else [] end) + [$app] | unique ' "$STATE_FILE" > "$tmp_file" fi mv "$tmp_file" "$STATE_FILE" +trap - EXIT if [[ -x "$SYNC_SCRIPT" ]]; then "$SYNC_SCRIPT" diff --git a/yabai/toggle_sketchybar_visibility.sh b/yabai/toggle_sketchybar_visibility.sh index b3e1b9e..7622b80 100755 --- a/yabai/toggle_sketchybar_visibility.sh +++ b/yabai/toggle_sketchybar_visibility.sh @@ -2,19 +2,14 @@ set -u -STATE_FILE="${HOME}/.config/yabai/.sketchybar_hidden" +SKETCHYBAR_BIN="$(command -v "${SKETCHYBAR_BIN:-sketchybar}" 2>/dev/null || true)" +JQ_BIN="$(command -v "${JQ_BIN:-jq}" 2>/dev/null || true)" +[ -n "$SKETCHYBAR_BIN" ] && [ -n "$JQ_BIN" ] || exit 0 -mkdir -p "$(dirname "$STATE_FILE")" +hidden="$("$SKETCHYBAR_BIN" --query bar 2>/dev/null | "$JQ_BIN" -r '.hidden // "off"' 2>/dev/null || printf 'off')" -is_hidden=0 -if [ -f "$STATE_FILE" ]; then - is_hidden=1 -fi - -if [ "$is_hidden" -eq 1 ]; then - rm -f "$STATE_FILE" - sketchybar --bar hidden=off >/dev/null 2>&1 || true +if [ "$hidden" = "on" ]; then + "$SKETCHYBAR_BIN" --bar hidden=off >/dev/null 2>&1 || true else - : >"$STATE_FILE" - sketchybar --bar hidden=on >/dev/null 2>&1 || true + "$SKETCHYBAR_BIN" --bar hidden=on >/dev/null 2>&1 || true fi diff --git a/yabai/trigger_sketchybar_space_labels_refresh.sh b/yabai/trigger_sketchybar_space_labels_refresh.sh index 4f37e94..5a217d2 100755 --- a/yabai/trigger_sketchybar_space_labels_refresh.sh +++ b/yabai/trigger_sketchybar_space_labels_refresh.sh @@ -1,52 +1,19 @@ #!/usr/bin/env bash -SKETCHYBAR_BIN="${SKETCHYBAR_BIN:-$(command -v sketchybar 2>/dev/null || true)}" -[ -x "$SKETCHYBAR_BIN" ] || SKETCHYBAR_BIN="/opt/homebrew/bin/sketchybar" - -CONFIG_ROOT="${CONFIG_DIR:-$HOME/.config/sketchybar}" -if [ -f "$CONFIG_ROOT/colors.sh" ]; then - # shellcheck source=/dev/null - source "$CONFIG_ROOT/colors.sh" -fi - -YABAI_BIN="${YABAI_BIN:-$(command -v yabai 2>/dev/null || true)}" -[ -x "$YABAI_BIN" ] || YABAI_BIN="/opt/homebrew/bin/yabai" - -ACCENT_COLOR="${ACCENT_COLOR:-0xffe1e1e1}" -TEXT_COLOR="${TEXT_COLOR:-0xffe1e1e1}" -ACCENT_RGB="${ACCENT_COLOR#0x}" -ACCENT_RGB="${ACCENT_RGB#\#}" -if [ "${#ACCENT_RGB}" -eq 8 ]; then - ACCENT_RGB="${ACCENT_RGB#??}" -fi -ACTIVE_BACKGROUND_COLOR="0x1a${ACCENT_RGB}" - -current_space="${1:-}" -if [ -z "$current_space" ]; then - current_space="$("$YABAI_BIN" -m query --spaces --space 2>/dev/null | jq -r '.index // empty')" -fi - -if [ -n "$current_space" ]; then - args=() - - for sid in 1 2 3 4 5 6 7 8 9 10 - do - if [ "$sid" = "$current_space" ]; then - args+=(--set "space.$sid" background.drawing=on - background.color="$ACTIVE_BACKGROUND_COLOR" - background.border_width=1 - background.border_color="$ACCENT_COLOR" - label.color="$ACCENT_COLOR" - icon.color="$ACCENT_COLOR") - else - args+=(--set "space.$sid" background.drawing=off - background.color=0x00000000 - background.border_width=0 - background.border_color=0x00000000 - label.color="$TEXT_COLOR" - icon.color="$TEXT_COLOR") - fi - done - - "$SKETCHYBAR_BIN" "${args[@]}" >/dev/null 2>&1 || true -fi +SKETCHYBAR_BIN="$(command -v "${SKETCHYBAR_BIN:-sketchybar}" 2>/dev/null || true)" +[ -n "$SKETCHYBAR_BIN" ] || exit 0 + +STATE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/macarchy" +REQUEST_FILE="$STATE_DIR/space-label-refresh" +mkdir -p "$STATE_DIR" + +# Several yabai signals can describe one transition. Only the newest request +# should redraw labels after the window and Space queries have settled. +token="$$.${RANDOM:-0}" +tmp_file="$REQUEST_FILE.$token" +printf '%s\n' "$token" > "$tmp_file" +mv "$tmp_file" "$REQUEST_FILE" +sleep "${SPACE_REFRESH_DELAY:-0.08}" +[ "$(cat "$REQUEST_FILE" 2>/dev/null)" = "$token" ] || exit 0 + +"$SKETCHYBAR_BIN" --trigger yabai_space_labels_refresh >/dev/null 2>&1 || true diff --git a/yabai/yabairc b/yabai/yabairc index abb37a9..0eeb982 100755 --- a/yabai/yabairc +++ b/yabai/yabairc @@ -1,50 +1,89 @@ #!/usr/bin/env bash -# Load scripting addition -sudo yabai --load-sa -yabai -m signal --add event=dock_did_restart action="sudo yabai --load-sa" -yabai -m signal --add event=space_changed action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" -yabai -m signal --add event=space_created action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" -yabai -m signal --add event=space_destroyed action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" -yabai -m signal --add event=window_created action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" +YABAI_BIN="$(command -v "${YABAI_BIN:-yabai}" 2>/dev/null || true)" +[ -n "$YABAI_BIN" ] || exit 0 -# Restore apps with only minimized windows onto the current Space when activated. -yabai -m signal --add event=application_activated action="$HOME/.config/yabai/restore_minimized_on_current_space.sh" +CONFIG_ROOT="${YABAI_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yabai}" +REFRESH_SCRIPT="$CONFIG_ROOT/trigger_sketchybar_space_labels_refresh.sh" +FLOAT_SCRIPT="$CONFIG_ROOT/apply_float_state.sh" +SYNC_SCRIPT="$CONFIG_ROOT/sync_float_rules.sh" -# Sync persisted float rules so stale per-app rules cannot override the JSON state -"$HOME/.config/yabai/sync_float_rules.sh" +printf -v refresh_command '/bin/bash %q' "$REFRESH_SCRIPT" +printf -v float_command '/bin/bash %q' "$FLOAT_SCRIPT" +printf -v load_sa_command 'sudo -n %q --load-sa' "$YABAI_BIN" -# Re-apply float preference when windows are created or deminimized -yabai -m signal --add event=window_created action="$HOME/.config/yabai/apply_float_state.sh" -yabai -m signal --add event=window_deminimized action="$HOME/.config/yabai/apply_float_state.sh" +replace_signal() { + local label="$1" event="$2" action="$3" + "$YABAI_BIN" -m signal --remove "$label" 2>/dev/null || true + "$YABAI_BIN" -m signal --add label="$label" event="$event" action="$action" +} -yabai -m signal --add event=window_destroyed action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" -yabai -m signal --add event=window_minimized action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" -yabai -m signal --add event=window_deminimized action="$HOME/.config/yabai/trigger_sketchybar_space_labels_refresh.sh" +replace_rule() { + local label="$1" + shift + "$YABAI_BIN" -m rule --remove "$label" 2>/dev/null || true + "$YABAI_BIN" -m rule --add label="$label" "$@" +} + +# Load the optional scripting addition only when the matching passwordless +# sudoers rule is already configured. This never prompts during service startup. +if sudo -n "$YABAI_BIN" --load-sa >/dev/null 2>&1; then + replace_signal macarchy_load_sa dock_did_restart "$load_sa_command" +else + "$YABAI_BIN" -m signal --remove macarchy_load_sa 2>/dev/null || true +fi + +# SketchyBar's Space component updates selection itself. Refreshing every app +# label on an ordinary Space focus causes 1 + 2N yabai queries for no change. +"$YABAI_BIN" -m signal --remove macarchy_space_changed 2>/dev/null || true +replace_signal macarchy_space_created space_created "$refresh_command" +replace_signal macarchy_space_destroyed space_destroyed "$refresh_command" + +# Older versions restarted JankyBorders around fullscreen transitions. Service +# restarts are now opt-in, so remove any labelled signals left by that config. +"$YABAI_BIN" -m signal --remove borders_fullscreen_space_created 2>/dev/null || true +"$YABAI_BIN" -m signal --remove borders_fullscreen_space_destroyed 2>/dev/null || true +"$YABAI_BIN" -m signal --remove borders_fullscreen_space_changed 2>/dev/null || true + +# Older Macarchy versions moved minimised windows on app activation. That is a +# surprising default, so reloads remove the old signal. An opt-in helper remains +# under examples for people who prefer that behaviour. +"$YABAI_BIN" -m signal --remove macarchy_restore_minimized 2>/dev/null || true + +# Sync persisted float rules so stale per-app rules cannot override the JSON state. +[ -f "$SYNC_SCRIPT" ] && /bin/bash "$SYNC_SCRIPT" + +# SketchyBar's native space_windows_change event owns app-label refreshes. +# yabai signals only apply the persisted floating preference here, avoiding a +# second full round of window queries for the same change. +replace_signal macarchy_window_created window_created "$float_command" +replace_signal macarchy_window_deminimized window_deminimized "$float_command" +"$YABAI_BIN" -m signal --remove macarchy_window_destroyed 2>/dev/null || true +"$YABAI_BIN" -m signal --remove macarchy_window_minimized 2>/dev/null || true # BSP layout -yabai -m config layout bsp -yabai -m config window_insertion_point focused -yabai -m config window_placement second_child -yabai -m config split_type auto -yabai -m config auto_balance off -yabai -m config insert_feedback_color 0x01000000 +"$YABAI_BIN" -m config layout bsp +"$YABAI_BIN" -m config window_insertion_point focused +"$YABAI_BIN" -m config window_placement second_child +"$YABAI_BIN" -m config split_type auto +"$YABAI_BIN" -m config auto_balance off +"$YABAI_BIN" -m config insert_feedback_color 0x01000000 # Padding and gaps -yabai -m config top_padding 8 -yabai -m config bottom_padding 8 -yabai -m config left_padding 8 -yabai -m config right_padding 8 -yabai -m config window_gap 8 - -# Mouse follows focus -yabai -m config mouse_follows_focus on -yabai -m config mouse_modifier ctrl -yabai -m config mouse_action1 move -yabai -m config mouse_action2 resize -yabai -m config mouse_drop_action stack +"$YABAI_BIN" -m config top_padding 8 +"$YABAI_BIN" -m config bottom_padding 8 +"$YABAI_BIN" -m config left_padding 8 +"$YABAI_BIN" -m config right_padding 8 +"$YABAI_BIN" -m config window_gap 8 + +# Mouse controls +"$YABAI_BIN" -m config mouse_follows_focus off +"$YABAI_BIN" -m config mouse_modifier ctrl +"$YABAI_BIN" -m config mouse_action1 move +"$YABAI_BIN" -m config mouse_action2 resize +"$YABAI_BIN" -m config mouse_drop_action stack # Float system apps that don't tile well -yabai -m rule --add app="^System Settings$" manage=off -yabai -m rule --add app="^Calculator$" manage=off -yabai -m rule --add app="^Finder$" manage=off title="^Copy$|^Move$|^Delete$|^Trash$" +replace_rule macarchy_system_settings app="^System Settings$" manage=off +replace_rule macarchy_calculator app="^Calculator$" manage=off +replace_rule macarchy_finder_dialogs app="^Finder$" manage=off title="^Copy$|^Move$|^Delete$|^Trash$" diff --git a/zsh/.zprofile b/zsh/.zprofile index 98a93dc..9ede6d8 100644 --- a/zsh/.zprofile +++ b/zsh/.zprofile @@ -1,10 +1,17 @@ -export HOMEBREW_PREFIX="/opt/homebrew" -export HOMEBREW_CELLAR="/opt/homebrew/Cellar" -export HOMEBREW_REPOSITORY="/opt/homebrew" +# Homebrew is installed in different locations on Apple Silicon, Intel and +# custom setups. Ask Homebrew for its environment instead of fixing one path. +_macarchy_brew="$(command -v brew 2>/dev/null || true)" +if [[ -z "$_macarchy_brew" ]]; then + for _macarchy_candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do + if [[ -x "$_macarchy_candidate" ]]; then + _macarchy_brew="$_macarchy_candidate" + break + fi + done +fi -typeset -gU path fpath -path=("$HOMEBREW_PREFIX/bin" "$HOMEBREW_PREFIX/sbin" $path) -fpath=("$HOMEBREW_PREFIX/share/zsh/site-functions" $fpath) -export PATH FPATH +if [[ -n "$_macarchy_brew" ]]; then + eval "$("$_macarchy_brew" shellenv)" +fi -export INFOPATH="$HOMEBREW_PREFIX/share/info:${INFOPATH:-}" +unset _macarchy_brew _macarchy_candidate diff --git a/zsh/.zshenv b/zsh/.zshenv index 1c764a4..7c93b0c 100644 --- a/zsh/.zshenv +++ b/zsh/.zshenv @@ -1,14 +1,5 @@ -export ZDOTDIR="$HOME/.config/zsh" -export XDG_CONFIG_HOME="$HOME/.config" -export XDG_CACHE_HOME="$HOME/.cache" -export XDG_DATA_HOME="$HOME/.local/share" -export XDG_STATE_HOME="$HOME/.local/state" -export CARGO_HOME="$HOME/.config/cargo/home" -export CLAUDE_CONFIG_DIR="$HOME/.config/claude/home" -export CODEX_HOME="$HOME/.config/codex/home" -export DOCKER_CONFIG="$HOME/.config/docker/home" -export GEMINI_CLI_HOME="$HOME/.config/gemini/home" -export MODAL_CONFIG_PATH="$HOME/.config/modal/modal.toml" -export NPM_CONFIG_CACHE="$HOME/.config/npm/home" -export OLLAMA_MODELS="$HOME/.config/ollama/home/models" -export SUPABASE_HOME="$HOME/.config/supabase/home" +export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" +export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" +export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" +export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" +export ZDOTDIR="${ZDOTDIR:-$XDG_CONFIG_HOME/zsh}" diff --git a/zsh/.zshrc b/zsh/.zshrc index 072b96c..5e46f01 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -1,112 +1,54 @@ -# Fast zsh startup. Keep the first prompt cheap; initialize completion on first Tab. -setopt PROMPT_SUBST - -_MACARCHY_SHELL_ENV="$HOME/.config/themes/.shell-env" -function _macarchy_load_env() { - [[ -r "$_MACARCHY_SHELL_ENV" ]] && source "$_MACARCHY_SHELL_ENV" -} - -_macarchy_load_env -PROMPT='%B%F{${MACARCHY_ACCENT_IDX:-7}}%1~%f%b %B%F{${MACARCHY_FG_IDX:-15}}❯%f%b ' -RPROMPT='' +# Macarchy's shell setup is deliberately small. Put machine-specific settings +# in local.zsh so updates do not overwrite them. +[[ -r "$ZDOTDIR/macarchy-prompt.zsh" ]] && source "$ZDOTDIR/macarchy-prompt.zsh" bindkey -v KEYTIMEOUT=1 autoload -Uz add-zsh-hook compinit -add-zsh-hook precmd _macarchy_load_env -add-zsh-hook preexec _macarchy_load_env - -# Make tab completion case-insensitive, so "d" can match "Desktop". zstyle ':completion:*' matcher-list 'm:{[:lower:][:upper:]}={[:upper:][:lower:]}' -typeset -g _LAZY_COMPINIT_DONE=0 - -function _bind_tab_widget() { - bindkey '^I' tab_accept_or_complete - bindkey -M emacs '^I' tab_accept_or_complete - bindkey -M viins '^I' tab_accept_or_complete -} - -function _lazy_compinit() { - (( _LAZY_COMPINIT_DONE )) && return - - local compdump="${ZDOTDIR:-$HOME}/.zcompdump-${HOST%%.*}-${ZSH_VERSION}" - - compinit -C -d "$compdump" - _LAZY_COMPINIT_DONE=1 - _bind_tab_widget -} +typeset -g _MACARCHY_COMPINIT_DONE=0 -function tab_accept_or_complete() { - if [[ -n "${POSTDISPLAY-}" ]] && zle -l autosuggest-accept >/dev/null 2>&1; then - zle autosuggest-accept - return +function _macarchy_complete_word() { + if (( ! _MACARCHY_COMPINIT_DONE )); then + local compdump="${ZDOTDIR:-$HOME}/.zcompdump-${HOST%%.*}-${ZSH_VERSION}" + compinit -C -d "$compdump" + _MACARCHY_COMPINIT_DONE=1 fi - - _lazy_compinit zle complete-word } -zle -N tab_accept_or_complete -_bind_tab_widget +zle -N _macarchy_complete_word +bindkey '^I' _macarchy_complete_word +bindkey -M viins '^I' _macarchy_complete_word -# Editor -export EDITOR=nvim +if [[ -z "${EDITOR:-}" ]]; then + if command -v nvim >/dev/null 2>&1; then + EDITOR=nvim + else + EDITOR=vi + fi +fi +export EDITOR -# History in ZDOTDIR HISTFILE="$ZDOTDIR/.zsh_history" HISTSIZE=10000 SAVEHIST=10000 setopt AUTO_CD HIST_IGNORE_DUPS HIST_IGNORE_SPACE INTERACTIVE_COMMENTS SHARE_HISTORY -# Aliases -alias ls='eza --icons' -alias treelist='tree -a -I .git' -alias n='nvim' -alias python='python3' -alias pip='pip3' +if command -v eza >/dev/null 2>&1; then + alias ls='eza --icons' +fi +command -v nvim >/dev/null 2>&1 && alias n='nvim' -# Functions function stay() { - nohup "$@" > /dev/null 2>&1 < /dev/null & + nohup "$@" >/dev/null 2>&1 &2 - return 1 - fi -} - -# AI account switcher wrappers (must come before PATH below; sets ~/bin first) -[[ -r "$HOME/.config/zsh/zshrc.d/ai-account-switchers.zsh" ]] && source "$HOME/.config/zsh/zshrc.d/ai-account-switchers.zsh" - -# PATH typeset -gU path fpath -path=( - "$HOME/.config/composer/vendor/bin" - "$HOME/.composer/vendor/bin" - "$HOME/.local/bin" - /opt/homebrew/bin - /opt/homebrew/sbin - /usr/local/bin - /usr/bin - /bin - /usr/sbin - /sbin - $path -) -fpath=(/opt/homebrew/share/zsh/site-functions $fpath) -export PATH -export FPATH - -[[ -r "$HOME/.config/oh-my-zsh/custom/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh" ]] && source "$HOME/.config/oh-my-zsh/custom/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh" +path=("$HOME/.local/bin" $path) +export PATH FPATH -# Keep syntax highlighting last so it can wrap final widgets. -[[ -r "$HOME/.config/oh-my-zsh/custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" ]] && source "$HOME/.config/oh-my-zsh/custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" +[[ -r "$ZDOTDIR/local.zsh" ]] && source "$ZDOTDIR/local.zsh" diff --git a/zsh/macarchy-prompt.zsh b/zsh/macarchy-prompt.zsh new file mode 100644 index 0000000..a5b5a7b --- /dev/null +++ b/zsh/macarchy-prompt.zsh @@ -0,0 +1,16 @@ +setopt PROMPT_SUBST + +_MACARCHY_SHELL_ENV="${MACARCHY_GENERATED_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/macarchy/generated}/shell-env" + +function _macarchy_load_theme() { + [[ -r "$_MACARCHY_SHELL_ENV" ]] && source "$_MACARCHY_SHELL_ENV" +} + +_macarchy_load_theme +autoload -Uz add-zsh-hook +add-zsh-hook precmd _macarchy_load_theme + +# Indexed colours update when Ghostty reloads its palette. True-colour values +# would remain fixed in existing terminal cells. +PROMPT='%B%F{${MACARCHY_ACCENT_IDX:-7}}%1~%f%b %B%F{${MACARCHY_FG_IDX:-15}}❯%f%b ' +RPROMPT='' diff --git a/zsh/themes/macarchy.zsh-theme b/zsh/themes/macarchy.zsh-theme index c459bd8..46bd3d8 100644 --- a/zsh/themes/macarchy.zsh-theme +++ b/zsh/themes/macarchy.zsh-theme @@ -1,30 +1,3 @@ -# macarchy.zsh-theme -# -# Uses ANSI palette indices (%F{N}) so the prompt re-resolves against the -# active ghostty palette on every render — meaning theme-switch's ghostty -# config reload (SIGUSR2) repaints existing scrollback prompts as well as -# new ones. Truecolor (%F{#hex}) would be baked into the cell buffer. -# -# Indices come from ~/.config/themes/.shell-env (written by theme-switch): -# MACARCHY_ACCENT_IDX -> directory text (e.g. palette 2 on awakening = yellow) -# MACARCHY_FG_IDX -> arrow + path text (15 in dark mode, 0 in light) - -setopt PROMPT_SUBST - -_MACARCHY_SHELL_ENV="$HOME/.config/themes/.shell-env" - -_macarchy_load_env() { - [[ -r "$_MACARCHY_SHELL_ENV" ]] && source "$_MACARCHY_SHELL_ENV" -} - -# Load once at theme init so the very first prompt has the right indices. -_macarchy_load_env - -# PROMPT_SUBST expands ${...} at each render, so when .shell-env is -# re-sourced (precmd / preexec) the next prompt picks up the new indices. -PROMPT='%B%F{${MACARCHY_ACCENT_IDX:-7}}%1~%f%b %B%F{${MACARCHY_FG_IDX:-15}}❯%f%b ' -RPROMPT='' - -autoload -Uz add-zsh-hook -add-zsh-hook precmd _macarchy_load_env -add-zsh-hook preexec _macarchy_load_env +# Oh My Zsh compatibility wrapper. The standalone zsh setup loads the same +# prompt file directly. +source "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/macarchy-prompt.zsh" diff --git a/zsh/zshrc.d/ai-account-switchers.zsh b/zsh/zshrc.d/ai-account-switchers.zsh deleted file mode 100644 index cb0cd8a..0000000 --- a/zsh/zshrc.d/ai-account-switchers.zsh +++ /dev/null @@ -1,6 +0,0 @@ -# AI account switcher wrappers. -# Put this near the top of ~/.zshrc so local account wrappers win over Homebrew and /usr/local/bin. -export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" - -# Optional explicit hub path for the zsh cursor wrapper. -export CURSOR_ACCOUNT_HUB_DIR="$HOME/.config/cursor-account-hub"