diff --git a/.agents/skills/omarchy-shell-quickshell/SKILL.md b/.agents/skills/omarchy-shell-quickshell/SKILL.md new file mode 100644 index 00000000..97b6e702 --- /dev/null +++ b/.agents/skills/omarchy-shell-quickshell/SKILL.md @@ -0,0 +1,69 @@ +--- +name: omarchy-shell-quickshell +description: "Customise and reload the Omarchy shell (omarchy-shell) - the Quickshell process behind the bar, notifications, OSD, launcher, and settings - in this dotfiles repo. Use when working with the Omarchy shell or Quickshell: editing omarchy/.config/omarchy/plugins/, the shell.json generator dot/src/lib/omarchyShellConfig.ts, shell.json, BarWidget/WidgetButton or other Quickshell QML, running omarchy plugin or omarchy restart shell, referencing upstream Quickshell, or when a shell or bar change is not showing up." +--- + +# Omarchy Shell (Quickshell) + +## Background / upstream + +- The Omarchy shell is a single long-running **Quickshell** process (`omarchy-shell`) that hosts the top bar, notification daemon, on-screen display, launcher, and settings panel. Restarting "the shell" restarts all of them together. +- **Quickshell** is the upstream framework: a QML/Qt6 toolkit for Wayland desktop shells (layer-shell surfaces via `WlrLayershell`, an IPC bus, and live QML reload). Upstream is self-hosted Forgejo at `git.outfoxxed.me/quickshell/quickshell`; the opencode `quickshell` reference clones the official GitHub mirror `quickshell-mirror/quickshell`. Use it for QML types, layer-shell, `IpcHandler`, reloadable config, and the `qs` CLI. +- Omarchy's shell source lives in `~/.local/share/omarchy/shell/` - **READ-ONLY** (reading is useful: base classes `BarWidget`/`WidgetButton` in `shell/Ui/`, the bar host in `shell/plugins/bar/Bar.qml`). Edits are lost on `omarchy update`. +- This work targets the `quattro` branch of `basecamp/omarchy`, matching the Omarchy 4 package installed on this system. + +## Source of truth (never edit live) + +- Custom shell content ships as **user plugins**. Edit the stow source `omarchy/.config/omarchy/plugins//` (stows to `~/.config/omarchy/plugins//`). +- `~/.config/omarchy/shell.json` is **generated, not hand-edited**. It is rendered by `dot` from `dot/src/lib/omarchyShellConfig.ts` (`mergeOmarchyShellConfig`), starting from Omarchy's default and inserting personal modules. Edit the generator, rebuild `dot`, then `dot stow` regenerates the file. The live file is mode `0600` and tracked by neither dotfiles repo. + +## Plugin layout + +A plugin is a folder with `manifest.json` + entry-point QML: + +- `manifest.json`: `schemaVersion: 1`, required `id`, `name`, `version`, non-empty `kinds`, and `entryPoints` mapping each kind to a safe relative QML path. A bar widget also has `barWidget` metadata such as `displayName`, `category`, and `allowMultiple`. Third-party ids must be namespaced and cannot start with `omarchy.`. +- Entry QML: a bar widget extends `BarWidget` (`import qs.Commons`, `import qs.Ui`), sets `moduleName` to the plugin id, reads per-instance `shell.json` settings via `setting(name, fallback)`, and uses `WidgetButton` for clickable text cells. + +Stow gotcha: `~/.config/omarchy/plugins/` is a **real directory with per-plugin symlinks**. A brand-new plugin needs `dot stow` to create its symlink before the shell sees it. Editing an existing plugin's files is already live (symlink -> dotfiles source). + +## Reload matrix (run after a change) + +| Change | Action | +| --- | --- | +| `shell.json` layout/settings, existing modules only | Hot-reloads on save - nothing to run | +| User plugin QML edited | Hot-reloads on save - nothing to run | +| New plugin added | `omarchy shell shell rescanPlugins` | +| Omarchy's first-party shell QML edited, or rescan cannot recover | `omarchy restart shell` | + +`omarchy shell shell rescanPlugins` unloads user plugin instances, clears the QML component cache, rescans manifests, and reloads enabled plugins. `reloadConfig` only reloads `shell.json`. A full `omarchy restart shell` refuses to run while the session is locked, then stops Quickshell and launches a detached replacement. + +## The XCB trap (critical) + +`omarchy-restart-shell` inherits the caller's environment. If `QT_QPA_PLATFORM=xcb`, Quickshell starts under XWayland, `WlrLayershell` cannot attach, and the shell renders as a **floating window** instead of bar/overlay surfaces (no error). This only matters when doing a full restart; plugin rescans stay in the running Wayland process. + +- Interactive shells in this repo set `QT_QPA_PLATFORM="wayland;xcb"` (`zsh/.zshrc`) so a direct `omarchy restart shell` works. A plain `xcb` reintroduces the floating-window bug. +- From any non-interactive context (SSH, `systemd`, `cron`, an agent shell) force Wayland and make sure the session is reachable: + +```bash +QT_QPA_PLATFORM=wayland omarchy restart shell # needs WAYLAND_DISPLAY + XDG_RUNTIME_DIR +``` + +- `dot update` bakes this in: it reloads the shell **only when the generated `shell.json` changed**, forcing `QT_QPA_PLATFORM=wayland` on the restart (`reloadOmarchyShell` in `dot/src/commands/Update.ts`). Standalone `dot stow` does not reload. + +## Lint (required after every final QML change) + +After every final QML edit, lint the files you touched with the **Qt6** `qmllint`. On Arch the Qt6 binary is `/usr/lib/qt6/bin/qmllint` - the bare `/usr/bin/qmllint` may be an older Qt5 build that rejects these flags. `-I /usr/lib/qt6/qml` lets it resolve the installed `Quickshell.*` modules: + +```bash +/usr/lib/qt6/bin/qmllint -I /usr/lib/qt6/qml --import disable --unqualified disable .qml +``` + +The shell's own `qs.*` modules ship in the (unpackaged) shell source, so the `import`/`unqualified` categories stay disabled as noise. This is a syntax-focused gate: unresolved Omarchy types can still produce warnings, while parse errors fail with a non-zero exit. This mirrors `.github/workflows/quickshell-lint.yml`, which uses Arch's stable `quickshell` package as a syntax proxy because Omarchy's runtime `quickshell-git` package is not available in the plain Arch container. Renovate keeps the proxy version current. Lint must exit successfully before a QML change is considered done. + +## Verify + +- `omarchy plugin list` - is the plugin registered and enabled? +- `omarchy-shell shell debugBarGeometry` - per-module `x`/`width`/`visible`; confirm a widget renders or collapses. +- `omarchy plugin validate ` rejects symlinked plugin folders - **expected** for stowed plugins, not a real error. +- `grim -g "0,0 360x32" out.png` - visual check of the bar's left edge; after a restart confirm `hyprctl layers | grep omarchy-bar`. +- After editing the generator: `mise run dot:check`. diff --git a/.github/workflows/quickshell-lint.yml b/.github/workflows/quickshell-lint.yml new file mode 100644 index 00000000..5750da3b --- /dev/null +++ b/.github/workflows/quickshell-lint.yml @@ -0,0 +1,72 @@ +--- +name: Lint Quickshell + +on: + push: + branches: + - distro/arch-omarchy + paths: + - '**/*.qml' + - '.github/workflows/quickshell-lint.yml' + pull_request: + paths: + - '**/*.qml' + - '.github/workflows/quickshell-lint.yml' + +permissions: + contents: read + +env: + # Version of Arch's stable `quickshell` package used as a syntax proxy. The + # Omarchy runtime uses quickshell-git from its own repository, which is not + # available in this plain Arch container. Both pull qt6-declarative, which + # provides qmllint. + # Renovate keeps it in sync via the repology datasource (see the + # customManagers entry in renovate.json); the version check below fails if + # the container's quickshell has drifted from this pin, which is the signal + # to merge the Renovate bump. + QUICKSHELL_VERSION: "0.3.0" + +jobs: + qmllint: + runs-on: ubuntu-latest + container: archlinux:latest + steps: + - name: Install quickshell and qmllint + run: | + # Refresh the keyring first (the base image's can lag and break + # signature checks), then install quickshell (pulls qt6-declarative = + # qmllint) and git (needed by actions/checkout in this container). + pacman -Sy --noconfirm --needed archlinux-keyring + pacman -Su --noconfirm --needed quickshell qt6-declarative git + + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Trust the checkout (container git ownership) + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Verify quickshell matches the pinned version + run: | + installed="$(pacman -Q quickshell | awk '{print $2}' | cut -d- -f1)" + echo "Installed quickshell: $installed (pinned QUICKSHELL_VERSION: $QUICKSHELL_VERSION)" + if [ "$installed" != "$QUICKSHELL_VERSION" ]; then + echo "::error::Arch quickshell is $installed but QUICKSHELL_VERSION is pinned to $QUICKSHELL_VERSION. Merge the Renovate bump so the pin matches the package being linted against." >&2 + exit 1 + fi + + - name: Lint QML + run: | + export PATH="/usr/lib/qt6/bin:$PATH" + mapfile -t qml_files < <(git ls-files '*.qml') + if [ "${#qml_files[@]}" -eq 0 ]; then + echo 'No QML files found.' + exit 0 + fi + printf 'Linting %s QML file(s) with %s:\n' "${#qml_files[@]}" "$(qmllint --version)" + printf ' %s\n' "${qml_files[@]}" + # quickshell installs its QML modules under /usr/lib/qt6/qml, so `-I` + # lets qmllint resolve `Quickshell.*`. The Omarchy shell's own `qs.*` + # modules ship in the (unpackaged) shell source, so the import + # category stays disabled; qmllint still fails on real syntax errors. + qmllint -I /usr/lib/qt6/qml --import disable --unqualified disable "${qml_files[@]}" diff --git a/.gitignore b/.gitignore index 3e8306ba..a29e30b0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,8 @@ dot/dist/ scripts/.local/bin/dot scripts/.local/bin/dot* zsh/.local/share/zsh/chpwd-recent-dirs +zsh/.local/share/zsh/site-functions/_mise +zsh/.local/share/zsh/site-functions/_op /session-*.md +/uwsm/.config/uwsm/env.d/99-omarchy-upgrade-env +/uwsm/.config/uwsm/*.omarchy-upgrade-to-quattro.*.bak diff --git a/AGENTS.md b/AGENTS.md index ef9b9074..842529b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,16 +59,18 @@ Keep shared cross-project agent behaviour in the global `~/.config/opencode/AGEN ## Repo-Specific Skills -- The repo-relevant skills are `effect` (Effect v4 code in `dot/`) and `opentui` (OpenTUI code in `dot/`); both self-document their triggers. +- The repo-relevant skills are `effect` (Effect v4 code in `dot/`), `opentui` (OpenTUI code in `dot/`), and `omarchy-shell-quickshell` (Omarchy shell / Quickshell work in this repo); all self-document their triggers. ## Omarchy Host Overrides -- Hyprland config is a stowed dotfiles package (`hypr/.config/hypr/`, conf-only), not a tracked Omarchy repo. -- `waybar` and `uwsm` are single-branch Omarchy repos expected on `main`. +- Hyprland config is a stowed dotfiles package (`hypr/.config/hypr/`), not a tracked Omarchy repo. +- UWSM custom environment values are stowed from `uwsm/.config/uwsm/env.d/90-dotfiles`; Quattro owns the defaults under `/usr/share`. - `ghostty` is a stowed package (`ghostty/.config/ghostty/`) with `config.$OMARCHY_HOST` overrides loaded by `ghostty-host-config`. - Hypr host-specific overrides live under `~/.config/hypr/hosts/$OMARCHY_HOST`, selected by the runtime `~/.config/hypr/host` symlink. - `dot stow` lays down the Hypr package with `--no-folding` and creates/repairs `~/.config/hypr/host`; `dot doctor` checks the host link and flags any leftover legacy `omarchy-hypr` clone. +- The Hypr package alone is stowed non-destructively: `dot stow` and `dot install` skip the usual unstow-then-restow for `hypr` so its symlinks (notably `hyprland.lua`) never vanish mid-stow, then reload Hyprland afterwards. This stops Hyprland's live-config autoreload from catching a missing config and dropping into emergency mode. Keep this behaviour if you touch the stow loop in `dot/src/commands/{Stow,Install}.ts`. - A machine still on the retired `~/.config/hypr` `omarchy-hypr` clone halts `dot update` until the clone is backed up and re-stowed. +- `dot stow` and `dot install` remove the retired `timmo001/omarchy-uwsm` checkout before the `uwsm` package takes ownership; Quattro-generated migration files are not copied into this repo. - If this host override layout changes, update the docs site (`docs/src/content/docs/`), `README.md`, `AGENTS.md`, and skill documentation together so repo instructions stay consistent. ## Stow Rules @@ -86,6 +88,7 @@ Keep shared cross-project agent behaviour in the global `~/.config/opencode/AGEN - Keep command and flag metadata in `dot/src/cli/spec.ts`; help and completion generation consume that registry. - When changing `dot` commands, subcommands, aliases, or flags, run `dot completions` for each supported shell after rebuilding so the stowed completion files stay in sync. - The `docs/` command reference (`docs/src/content/docs/dot/commands.md`) is generated from `dot/src/cli/spec.ts`. After changing commands, regenerate it with `mise run docs:gen:cli` (alongside shell completions) and commit the result. +- The Omarchy bar `shell.json` is generated by `dot/src/lib/omarchyShellConfig.ts`; the `dot update` loop reloads the running shell only when the rendered `shell.json` changes and forces `QT_QPA_PLATFORM=wayland` on the restart so the layer-shell bar attaches. Keep this behaviour if you touch `reloadOmarchyShell` in `dot/src/commands/Update.ts` or the shell-config return value. See the `omarchy-shell-quickshell` skill. ## Documentation Site diff --git a/README.md b/README.md index 7e69560a..9221e0ef 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ My public [Omarchy](https://omarchy.org) dotfiles, managed with GNU Stow and the - Stow-based dotfiles rooted at `~/.config/dotfiles`, applied with the `dot` command - A single compiled binary at `scripts/.local/bin/dot` (Bun + Effect v4 + OpenTUI) with a TUI dashboard and a full CLI -- Git/GitHub tooling: diff, log, status, workflow runs, and a notification inbox across managed repos, with Waybar modules -- Managed Omarchy repos (`waybar`, `uwsm`) and stowed Hyprland/Ghostty config +- Git/GitHub tooling: diff, log, status, workflow runs, and a notification inbox across managed repos, surfaced in the Omarchy Quickshell status bar +- Stowed UWSM, Hyprland, and Ghostty customisations for Omarchy Quattro - Optional private overlay from `~/.config/dotfiles-private` - Shared OpenCode agents, commands, and plugins published to [`timmo001/opencode-config`](https://github.com/timmo001/opencode-config), with portable skills in [`timmo001/skills`](https://github.com/timmo001/skills) @@ -68,6 +68,7 @@ Everything is documented at : - `zsh/`, `neovim/`, `starship/`, `editorconfig/` — shell, editor, and prompt config - `agents/` — OpenCode config (`.config/opencode/`) and the pinned [`skills`](https://github.com/timmo001/skills) checkout (`.agents/skills/`), published through [`opencode-config`](https://github.com/timmo001/opencode-config) - `hypr/` — Hyprland config (stowed with `--no-folding`, per-host overrides) +- `uwsm/` — user environment overrides layered over Quattro's package defaults - `ghostty/` — Ghostty config, host overrides, launcher, and desktop entry The documentation is the single source of truth; this README links to it rather than duplicating content. The `dot` command reference and the OpenCode reference on the docs site are generated from `dot/src/cli/spec.ts` and the OpenCode assets respectively. diff --git a/bash/.local/share/bash-completion/completions/dot b/bash/.local/share/bash-completion/completions/dot index 7e82331c..19683aa7 100644 --- a/bash/.local/share/bash-completion/completions/dot +++ b/bash/.local/share/bash-completion/completions/dot @@ -24,7 +24,7 @@ _dot_cmd_init() { esac if [[ $cur == -* ]]; then - COMPREPLY=( $(compgen -W '--confirm --noninteractive --interactive --force --host --log --branch --help -h' -- "$cur") ) + COMPREPLY=( $(compgen -W '--confirm --noninteractive --interactive --force --host --log --help -h' -- "$cur") ) return fi } diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 320656ab..a966069b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -91,7 +91,12 @@ export default defineConfig({ }, { label: 'Omarchy & Hyprland', - items: [{ autogenerate: { directory: 'omarchy' } }], + items: [ + { label: 'Overview', slug: 'omarchy' }, + { label: 'Host Overrides', slug: 'omarchy/host-overrides' }, + { label: 'Shell (Quickshell)', slug: 'omarchy/shell' }, + { label: 'Controls', slug: 'omarchy/controls' }, + ], }, { label: 'Bar Integrations', slug: 'bar-integrations' }, { label: 'Cleanup', slug: 'cleanup' }, diff --git a/docs/src/content/docs/bar-integrations.mdx b/docs/src/content/docs/bar-integrations.mdx index 773cc102..fcff37a2 100644 --- a/docs/src/content/docs/bar-integrations.mdx +++ b/docs/src/content/docs/bar-integrations.mdx @@ -15,7 +15,7 @@ export const commandCards = [ `dot git-diff` status scans disable Git's optional index locks. Background bar and TUI polling can read working-tree state during a rebase, merge, or another index-writing operation without refreshing the index or competing for `.git/index.lock`. -The JSON format is bar-agnostic: it works with [Waybar](https://github.com/Alexays/Waybar), [Quickshell](https://quickshell.outfoxxed.me), or any bar that can run a command and parse JSON. My own setup uses Waybar, so the bundled modules and `dot doctor` checks target Waybar, but the commands themselves do not depend on it. +The JSON format is bar-agnostic: it works with [Quickshell](https://quickshell.outfoxxed.me), [Waybar](https://github.com/Alexays/Waybar), or any bar that can run a command and parse JSON. My own setup uses the Omarchy 4 Quickshell bar, which runs these commands through `timmo.command` widgets, but the commands themselves do not depend on any particular bar. See [Shell (Quickshell)](/omarchy/shell/) for how that bar is configured. ## dot commands with bar output @@ -35,7 +35,7 @@ ha-entity-bar-json-once --icon '' input_text.current_next_event_in_an_hour ### `ha-module-bar` -`ha-module-bar` wraps go-automate with opinionated Waybar modules for common Home Assistant entities. Each mode ships default entity IDs and display rules; override them with `--entity`, `--name`, `--icon`, and mode-specific flags when your setup differs. Run `ha-module-bar --help` for the full flag list. +`ha-module-bar` wraps go-automate with opinionated status-bar modules for common Home Assistant entities. Each mode ships default entity IDs and display rules; override them with `--entity`, `--name`, `--icon`, and mode-specific flags when your setup differs. Run `ha-module-bar --help` for the full flag list. | Mode | Behaviour | | --- | --- | @@ -45,9 +45,9 @@ ha-entity-bar-json-once --icon '' input_text.current_next_event_in_an_hour | `nas-activity` | Show NAS activity while the gate switch is on; highlight when the inactivity script is armed. Uses `--switch-entity` and `--inactive-script-entity`. | | `dining-temperature` | Show dining-room temperature only while the air-conditioner target is below `--gate-below` (default `25`). Uses `--gate-entity` for the AC target. | | `current-next-event` | Pass through the entity's bar JSON when the text is non-empty. | -| `doorbell` | Stream doorbell state through `singleton-stream` so Waybar can keep one long-lived watcher per module instance. | +| `doorbell` | Stream doorbell state through `singleton-stream` so the bar can keep one long-lived watcher per module instance. | -Most modes poll once per refresh. `doorbell` is the exception: it keeps a singleton stream alive for the Waybar ancestor process and emits JSON only when the output changes. Override the stream identity with `--stream-key` when multiple doorbell modules share an entity. +Most modes poll once per refresh. `doorbell` is the exception: it keeps a singleton stream alive for the bar ancestor process and emits JSON only when the output changes. Override the stream identity with `--stream-key` when multiple doorbell modules share an entity. ```bash ha-module-bar temperature @@ -70,20 +70,11 @@ ha-module-bar co2-alert \ `--trigger-on` accepts `transition` (default; fire when state enters `--trigger-state`) or `match` (fire while state equals `--trigger-state`). `--trigger-initial true` allows the first observed state to fire; the default skips the initial read so restarts do not replay alerts. `--trigger-cooldown` enforces a minimum interval between fires. Trigger state persists under `$XDG_RUNTIME_DIR` (override the key with `--trigger-key` when multiple modules share a mode). -`dot dashboard` rejects commands that embed `singleton-stream` or `doorbell`, so use `ha-entity-bar-json-once` for dashboard cards and reserve `ha-module-bar doorbell` for Waybar. See [Private Dashboard Config](/configuration/private-dashboard/). +`dot dashboard` rejects commands that embed `singleton-stream` or `doorbell`, so use `ha-entity-bar-json-once` for dashboard cards and reserve `ha-module-bar doorbell` for a streaming bar widget. See [Private Dashboard Config](/configuration/private-dashboard/). ### `package-updates-bar` -`package-updates-bar` reports how many watched public packages have updates available. It reads `$DOTFILES_PUBLIC_DIR/.dot-public-packages` (override with `WAYBAR_PACKAGE_UPDATES_FILE`), classifies each entry as a repo or AUR package with `pacman -Qnq` / `pacman -Qmq`, checks repo packages with `pacman -Qu` and AUR packages with `yay -Qua`, and caches the rendered JSON under `$XDG_CACHE_HOME/waybar/` (override with `WAYBAR_PACKAGE_UPDATES_CACHE_DIR`). - -```bash -package-updates-bar status # print cached JSON; refresh in the background when stale -package-updates-bar refresh # rebuild the cache synchronously -``` - -The `status` subcommand returns a loading placeholder when no cache exists yet, hides the module when every watched package is current, and shows a warning glyph when the update check itself fails. When repo updates are available but the AUR check fails, the module still shows the repo count and appends `AUR updates unavailable` to the tooltip. A background refresh starts when the cache is older than 15 minutes (override with `WAYBAR_PACKAGE_UPDATES_CACHE_MAX_AGE`). - -Background AUR lookups back off exponentially after HTTP 4xx/5xx responses (30-minute base, capped at six hours) so a broken AUR connection does not hammer the network on every poll. Backoff applies only to background refreshes; an explicit `refresh` retries immediately and clears the backoff when the request succeeds. Right-click refresh in Waybar should call `package-updates-bar refresh` directly so a recovered connection is picked up without waiting for the backoff timer. +`package-updates-bar` reports updates for packages listed in `.dot-public-packages`. It caches status-bar JSON under `$XDG_CACHE_HOME/status-bar`, refreshes stale data in the background, and keeps the previous AUR backoff behaviour. Run `package-updates-bar refresh` for an immediate retry. ## Configuration @@ -91,4 +82,4 @@ Which repos and which activity reach the bar is controlled by the private `dot-g ## Health checks -`dot doctor` verifies the active status-bar module wiring for `git-workflows` and `git-notifications`, alongside `dot-git.yml` and the absence of legacy `git-workflow-watch` leftovers. +`dot doctor` verifies `dot-git.yml`, GitHub notification API access, and the absence of legacy `git-workflow-watch` leftovers. diff --git a/docs/src/content/docs/cleanup.md b/docs/src/content/docs/cleanup.md index 9c846456..86af9a18 100644 --- a/docs/src/content/docs/cleanup.md +++ b/docs/src/content/docs/cleanup.md @@ -148,20 +148,17 @@ After the stowed links and system config are removed, delete cloned repos and ge ```bash rm -rf ~/.config/dotfiles-private -rm -rf ~/.config/bootstrap ~/.config/waybar ~/.config/uwsm rm -rf ~/.local/state/dot ~/.cache/dot ``` Private package repos and other private Git clones are configured by the private overlay. Review `~/.config/dotfiles-private/.dot-private-package-repo` and `~/.config/dotfiles-private/dot-git.yml`, then remove only clones and mirrors you no longer need. -If you removed Omarchy config directories that `dot init` replaces with managed repos, or stowed config directories that Omarchy should own again, refresh the stock Omarchy defaults afterwards. +If you removed stowed config directories that Omarchy should own again, refresh the stock Omarchy defaults afterwards. Quattro's UWSM defaults are package-owned under `/usr/share`, so there is no `uwsm/env` user config to refresh. ```bash -omarchy refresh waybar omarchy refresh shell omarchy refresh hyprland omarchy refresh config ghostty/config -omarchy refresh config uwsm/env ``` Run `omarchy refresh --help` on the target machine for the exact refresh commands supported by that Omarchy version. diff --git a/docs/src/content/docs/configuration/environment.md b/docs/src/content/docs/configuration/environment.md index f3456936..e42db329 100644 --- a/docs/src/content/docs/configuration/environment.md +++ b/docs/src/content/docs/configuration/environment.md @@ -13,6 +13,12 @@ The global mise configuration (`mise/.config/mise/config.toml`) pins the shared Python 3.14 and [uv](https://docs.astral.sh/uv/) are installed through mise for Python package and virtual-environment management. Use `uv` directly for project work; `topgrade` leaves mise pins unchanged unless you run `mise install` or `dot update`. +The stowed `mise` wrapper keeps the global config under dotfiles control. Commands such as Omarchy's `mise use -g ` are redirected to `~/.local/state/mise/omarchy-config.toml`, while reads, installs, upgrades, and project-local writes continue normally. To intentionally change the stowed global config, opt in explicitly: + +```bash +mise --write-global-config use -g --pin node@26 +``` + The configuration also provides the Android SDK command-line tools and sets the SDK environment for Gradle and `sdkmanager`. Individual Android repositories still declare their required platform and build-tools versions; install those SDK packages with `sdkmanager` when a checkout requires them. ## Paths and overlay @@ -32,7 +38,6 @@ The configuration also provides the Android SDK command-line tools and sets the | `DOT_GITHUB_RATE_LIMIT_TTL_SECONDS` | Seconds to cache `gh api rate_limit` results (default `60`). | | `DOT_GITHUB_RATE_LIMIT_MIN_REMAINING` | Minimum REST quota remaining before `gh` calls wait (default `0`). | | `DOT_GITHUB_RATE_LIMIT_MAX_WAIT_SECONDS` | Upper bound on rate-limit backoff waits (default `60`). | -| `DOT_INCLUDE_OMARCHY_DIFF_REPOS` | Include Omarchy repos in `dot git-diff` (`1\|0`, default `1`). | | `DOT_FETCH_TTL_SECONDS` | Seconds to reuse the last upstream fetch (default `300`). | | `DOT_GH_EXTENSIONS_FILE` | Public `gh` extension list installed by `dot init` (default `$DOTFILES_PUBLIC_DIR/.dot-gh-extensions`). | | `DOT_GH_MCP_BEARER` | Bearer token for the read-only GitHub MCP server. The shell wrappers and `opencode-server` set it only for agent harness processes; it is not exported globally. | @@ -57,7 +62,6 @@ The configuration also provides the Android SDK command-line tools and sets the | ----------------------- | ------------------------------------------------------------------------------------------------------ | | `OMARCHY_REPO_BASE_DIR` | Omarchy repo base path (default `~/.config`). | | `OMARCHY_HOST` | Hypr host override name; `dot init` defaults to `desktop` when unset unless `--host ` is passed. | -| `DOT_OMARCHY_BRANCH` | Branch override for Omarchy repos during sync. | ## Init and logging diff --git a/docs/src/content/docs/configuration/private-dashboard.md b/docs/src/content/docs/configuration/private-dashboard.md index 0881728c..09ba6bbb 100644 --- a/docs/src/content/docs/configuration/private-dashboard.md +++ b/docs/src/content/docs/configuration/private-dashboard.md @@ -53,7 +53,7 @@ sources: Each `command` must behave like a status-bar poll, not a long-running watcher: - Print one JSON object on the first line of stdout. -- Use the same `--bar-json` shape as Waybar modules: `text`, `tooltip`, and `class` fields. See [Bar Integrations](/bar-integrations/). +- Use the shared `--bar-json` status-bar shape: `text`, `tooltip`, and `class` fields. See [Bar Integrations](/bar-integrations/). - Finish within eight seconds. `dot dashboard` kills overdue commands with `SIGTERM`. - Avoid unbounded stream helpers. Commands containing `ha-watch-singleton`, `singleton-stream`, or `doorbell` are rejected as unsafe. diff --git a/docs/src/content/docs/configuration/private-git.md b/docs/src/content/docs/configuration/private-git.md index dbbf8ec6..98751475 100644 --- a/docs/src/content/docs/configuration/private-git.md +++ b/docs/src/content/docs/configuration/private-git.md @@ -50,4 +50,4 @@ The `notifications.bar.ignore_bot_activity` key controls status-bar bot noise. A ## Requirements - `dot git-notifications` requires `gh` authenticated with a classic token carrying the `notifications` or `repo` scope. -- `dot doctor` verifies `dot-git.yml`, the active status-bar module wiring, and the absence of legacy `git-workflow-watch` leftovers. +- `dot doctor` verifies `dot-git.yml` and the absence of legacy `git-workflow-watch` leftovers. diff --git a/docs/src/content/docs/dot/commands.md b/docs/src/content/docs/dot/commands.md index 21acd91d..834d702f 100644 --- a/docs/src/content/docs/dot/commands.md +++ b/docs/src/content/docs/dot/commands.md @@ -58,7 +58,6 @@ ongoing maintenance. | `--force` | Re-run init even if the machine looks initialised | | `--host` `` | Hypr host to link before stow (default: OMARCHY_HOST or desktop) | | `--log` `` | Init log path (default: ~/.local/state/dot/init.log) | -| `--branch` `` | Branch override for Omarchy repos | **Examples** @@ -66,7 +65,6 @@ ongoing maintenance. dot init --noninteractive dot init --host laptop --noninteractive dot init --force --noninteractive -dot init --branch main ``` ## `dot install` @@ -189,12 +187,12 @@ OpenCode server Shared Hypr autostart and ~/.config/opencode/.env password Herdr integration Herdr binary and OpenCode integration installed GitHub MCP auth gh token available for DOT_GH_MCP_BEARER Git config Managed include is active -Workflow runs Repo list, status bar config, legacy watcher cleanup -Git notifications API scope and status bar notification module wiring +Workflow runs Repo list and legacy watcher cleanup +Git notifications API scope and notification access Doctor startup Startup notification timer uwsm session PATH ~/.local/bin on the uwsm/systemd user-environment PATH Daily volume reset Laptop-only optional timer -Omarchy repos Diff repos + worktree branch correctness +Omarchy config Managed repos and Hypr host-link correctness Legacy Hypr repo Flags a retired omarchy-hypr clone at ~/.config/hypr Neovim theme link Repairs a mislocated omarchy-nvim theme.lua symlink Private access Private dotfiles overlay enabled or explains why it is disabled diff --git a/docs/src/content/docs/dot/shell.md b/docs/src/content/docs/dot/shell.md index 191b608e..e2c29fa3 100644 --- a/docs/src/content/docs/dot/shell.md +++ b/docs/src/content/docs/dot/shell.md @@ -41,4 +41,6 @@ The shell keeps a few typo and navigation helpers close to the aliases. For exam Run `update` to select maintenance steps with Gum. Dotfiles and Omarchy remain separate choices, while each enabled Topgrade step can be selected independently. Selected work runs in the displayed order: Dotfiles, Omarchy, then Topgrade. Mise, GitHub CLI extensions, and Yazi appear first within Topgrade and start selected; the remaining steps are opt-in. Selected Topgrade steps run together through `topgrade --only`; selecting every Topgrade step uses the normal full `topgrade` command instead. +The Omarchy step redirects Quattro's global mise writes to `~/.local/state/mise/omarchy-config.toml`, then re-stows the public dotfiles after Omarchy regenerates its command wrappers. This keeps `mise/.config/mise/config.toml` under dotfiles control during the complete update, not just normal shell commands. + Use `update -y` or `update --yes`, or run it in a non-interactive shell, to select every step without prompting. Agent-driven runs without a controlling terminal route internal `sudo` calls through a temporary `pkexec` helper so authentication can use the desktop PolicyKit prompt. Interactive terminals keep normal `sudo` credential caching. The sequence stops if a selected step fails. diff --git a/docs/src/content/docs/dot/stow.mdx b/docs/src/content/docs/dot/stow.mdx index 2645896b..d41886fe 100644 --- a/docs/src/content/docs/dot/stow.mdx +++ b/docs/src/content/docs/dot/stow.mdx @@ -7,7 +7,7 @@ sidebar: import { FileTree } from '@astrojs/starlight/components'; -The repository is a [GNU Stow](https://www.gnu.org/software/stow/) package root targeting `~/`. Each top-level directory (`zsh`, `neovim`, `starship`, `hypr`, `ghostty`, `scripts`, ...) is a stow package whose contents are symlinked into your home directory. +The repository is a [GNU Stow](https://www.gnu.org/software/stow/) package root targeting `~/`. Each top-level directory (`zsh`, `neovim`, `starship`, `uwsm`, `hypr`, `ghostty`, `scripts`, ...) is a stow package whose contents are symlinked into your home directory. @@ -22,6 +22,11 @@ The repository is a [GNU Stow](https://www.gnu.org/software/stow/) package root - .config - hypr - ... + - **uwsm/** stow package + - .config + - uwsm + - env.d + - 90-dotfiles - **ghostty/** stow package - .config - ghostty @@ -48,18 +53,19 @@ dot stow --private # private only ## What `dot stow` does - Lays down public packages first, then the private overlay from `~/.config/dotfiles-private`. -- Stows any package that targets runtime-owned directories with `--no-folding`, including `hypr/`, `ghostty/`, `herdr/`, shell completions, `.local/bin`, and systemd user units. This keeps `~/.config/herdr/` local while stowing only `config.toml`, so logs, sockets, and session state never enter the repository. For Hypr it also creates/repairs the `~/.config/hypr/host` symlink for the active host. +- Stows any package that targets runtime-owned directories with `--no-folding`, including `uwsm/`, `hypr/`, `ghostty/`, `herdr/`, shell completions, `.local/bin`, and systemd user units. This keeps generated UWSM migration files and Herdr runtime state out of the source directories. For Hypr it also creates/repairs the `~/.config/hypr/host` symlink for the active host. - Before stow, moves unmanaged real files or directories that block active package targets into the repo's `backup/` directory and logs each source-to-backup path. This includes host-specific packages such as `chromium--laptop`. It does not follow an unmanaged parent symlink into an external tree; those conflicts are left for manual resolution. - Before public stow, backs up the retired `timmo001/omarchy-ghostty` clone at `~/.config/ghostty` when present, so the `ghostty/` package can own that path. +- Before public stow, removes the retired `timmo001/omarchy-uwsm` checkout, including Quattro's generated migration files, then links only the intentional `90-dotfiles` override. - During `dot install` and `dot init`, public packages use `--adopt`, but a committed-wins pre-pass first backs up differing live files so stock config cannot silently overwrite committed public files. Any remaining adopted changes are reported for review. Private packages use normal stow without `--adopt`. ## Hypr package handling The Hypr package is treated differently from every other stow package. -Hyprland enables config autoreload by default. If `~/.config/hypr/hyprland.conf` goes missing even briefly, Hyprland writes a default stub as a real file. That stub then blocks the next stow because stow cannot replace a regular file with a symlink. +Hyprland enables config autoreload by default. If `~/.config/hypr/hyprland.lua` goes missing even briefly, Hyprland can write a default stub as a real file. That stub then blocks the next stow because stow cannot replace a regular file with a symlink. -To avoid that gap, the steady-state `dot stow` flow and the install flow used by `dot install` and `dot init` never unstow the `hypr` package. Before stowing Hypr, they atomically repair `~/.config/hypr/hyprland.conf` when the link is missing or points at the wrong target: the link is created through a temporary path and renamed into place, so Hyprland never sees a missing file. The idempotent stow step then fills in any other missing Hypr files without an unstow/restow cycle. +To avoid that gap, the steady-state `dot stow` flow and the install flow used by `dot install` and `dot init` never unstow the `hypr` package. Before stowing Hypr, they atomically repair `~/.config/hypr/hyprland.lua` when the link is missing or points at the wrong target: the link is created through a temporary path and renamed into place, so Hyprland never sees a missing file. The idempotent stow step then fills in any other missing Hypr files without an unstow/restow cycle. After the Hypr package is laid down, `dot stow` also creates or repairs the `~/.config/hypr/host` symlink for the active `OMARCHY_HOST`. See [Host Overrides](/omarchy/host-overrides/) for how host overrides are selected. diff --git a/docs/src/content/docs/dot/usage.md b/docs/src/content/docs/dot/usage.md index 10ddf94f..45244f02 100644 --- a/docs/src/content/docs/dot/usage.md +++ b/docs/src/content/docs/dot/usage.md @@ -23,7 +23,7 @@ Events are written per machine and per day: $XDG_STATE_HOME/tool-usage/events//YYYY-MM-DD.ndjson ``` -`invoker` is one of `human`, `agent` (an AI coding agent was detected), or `automation` (a status-bar poll, detected from `--bar-json`), so Waybar polling does not drown out genuine feature usage. +`invoker` is one of `human`, `agent` (an AI coding agent was detected), or `automation` (a status-bar poll, detected from `--bar-json`), so shell polling does not drown out genuine feature usage. ## Reporting diff --git a/docs/src/content/docs/getting-started/install.md b/docs/src/content/docs/getting-started/install.md index 563c4437..c92a2777 100644 --- a/docs/src/content/docs/getting-started/install.md +++ b/docs/src/content/docs/getting-started/install.md @@ -34,7 +34,7 @@ mise run dot:build ## First-use setup -`dot init` runs the one-time first-use setup: it bootstraps private dotfiles when allowed, syncs Omarchy repos, selects the Hypr host, installs and adopts config, installs stowed mise tools, verifies and registers the signed `timmo` package repository, sets up packages and machine hooks, and syncs agents. It logs to `~/.local/state/dot/init.log` by default. The private-overlay pull or clone runs first as an unbounded preflight; the setup phases that follow use the same spinner and timeout handling as `dot update`. +`dot init` runs the one-time first-use setup: it bootstraps private dotfiles when allowed, selects the Hypr host, installs and adopts config, installs stowed mise tools, verifies and registers the signed `timmo` package repository, sets up packages and machine hooks, and syncs agents. It logs to `~/.local/state/dot/init.log` by default. The private-overlay pull or clone runs first as an unbounded preflight; the setup phases that follow use the same spinner and timeout handling as `dot update`. ```bash ~/.config/dotfiles/scripts/.local/bin/dot init --noninteractive @@ -51,7 +51,7 @@ Or run `dot init` in an interactive shell to be prompted. `--noninteractive` skips only the Hypr host questionnaire; elevation and package tools may still prompt. `--confirm` remains accepted for compatibility but does not suppress prompts. Private overlay preflight is controlled by `DOT_ALLOW_PRIVATE`: `auto` skips without GitHub authentication and tolerates an existing-overlay pull failure, but a failed attempted clone is fatal; `always` requires the overlay to update or clone successfully; `never` skips it. :::note -If stock Omarchy directories already exist at `~/.config/waybar` or `~/.config/uwsm`, `dot init` backs them up with a `.dot-init-backup-*` suffix before cloning the managed repos. Hyprland and Ghostty config are stowed from the `hypr/` and `ghostty/` packages instead. +UWSM, Hyprland, and Ghostty customisations are stowed from the `uwsm/`, `hypr/`, and `ghostty/` packages. If the retired `timmo001/omarchy-uwsm` checkout is present, init removes it before linking the Quattro-compatible environment override. ::: ## Ongoing workflow diff --git a/docs/src/content/docs/getting-started/new-machine.mdx b/docs/src/content/docs/getting-started/new-machine.mdx index ba68dcc8..cead90d5 100644 --- a/docs/src/content/docs/getting-started/new-machine.mdx +++ b/docs/src/content/docs/getting-started/new-machine.mdx @@ -29,7 +29,7 @@ A clean, end-to-end walkthrough for setting up a new machine. ``` 5. Run first-use setup using the mode that matches this machine. -6. If stock Omarchy config directories already exist at `~/.config/waybar` or `~/.config/uwsm`, `dot init` backs them up with a `.dot-init-backup-*` suffix before cloning the managed repos. Hyprland and Ghostty config are stowed from the `hypr/` and `ghostty/` packages instead. +6. UWSM, Hyprland, and Ghostty customisations are stowed from their public packages. Init removes only the retired `timmo001/omarchy-uwsm` checkout before linking the Quattro-compatible override. 7. `dot init` verifies the pinned public package signing fingerprint, registers the signed `timmo` repository before the standard repositories, and retains AUR as the fallback for packages not published there. 8. `dot init` opens the managed [firewall rules](/dot/utilities/#firewall-rules) (KDE Connect, Home Assistant, the OpenCode server, LocalSend, and the libvirt NAT network) when `ufw` is installed. 9. Restart your shell and confirm `dot help` is on `PATH`. @@ -65,7 +65,7 @@ dot init `--noninteractive` skips the Hypr host questionnaire; elevation and package tools may still prompt. The accepted `--confirm` compatibility flag does not suppress those prompts. :::tip[Hypr host and mise tools] -`dot init` selects the Hypr host early and creates `~/.config/hypr/host`. Reboot after init so Hyprland, Waybar, launchers, services, and new shells inherit `OMARCHY_HOST` from the stowed host config. Init runs `mise install` immediately after stowing dotfiles and before installing managed Arch/AUR package lists, so Bun, Node, pnpm, and similar tools come from the stowed mise config rather than global pacman packages. Later full `dot update` runs repo pulls, rebuilds, mise trust refreshes, MCP sync, and stow without checking or installing packages. +`dot init` selects the Hypr host early and creates `~/.config/hypr/host`. Reboot after init so Hyprland, the Omarchy shell, launchers, services, and new shells inherit `OMARCHY_HOST` from the stowed host config. Init runs `mise install` immediately after stowing dotfiles and before installing managed Arch/AUR package lists, so Bun, Node, pnpm, and similar tools come from the stowed mise config rather than global pacman packages. Later full `dot update` runs repo pulls, rebuilds, mise trust refreshes, MCP sync, and stow without checking or installing packages. ::: :::note[GNOME Boxes shared folders] diff --git a/docs/src/content/docs/git/diff.md b/docs/src/content/docs/git/diff.md index 1097bc10..04cf09da 100644 --- a/docs/src/content/docs/git/diff.md +++ b/docs/src/content/docs/git/diff.md @@ -19,7 +19,7 @@ dot git-diff --no-fetch # skip upstream fetches; use local refs only dot git-diff --tab other # open with the Other pane focused ``` -The TUI polls every ten seconds and loads Waybar cache on startup for a fast first paint. `dot git-log` reuses the same tracked repo list. +The TUI polls every ten seconds and performs an initial poll for a fast first paint. `dot git-log` reuses the same tracked repo list. ## TUI layout @@ -61,8 +61,8 @@ When a repo has an upstream configured, `dot git-diff` fetches the tracking bran ## Status bar module -A status bar module polls `dot git-diff --bar-json` through its own short-lived cache; left click opens the TUI and right click refreshes the cache. See [Bar Integrations](/bar-integrations/) for the shared JSON contract. +The Quickshell module polls `dot git-diff --bar-json` through the stowed `git-diff-bar` cache command; left click opens the TUI and right click refreshes the widget. See [Bar Integrations](/bar-integrations/) for the shared JSON contract. ## Configuration -Which repos appear and whether Omarchy repos are included is controlled by the private `dot-git.yml` config and `DOT_INCLUDE_OMARCHY_DIFF_REPOS`. See [Private Git Config](/configuration/private-git/) and [Environment Variables](/configuration/environment/#git-and-github). +Which repositories appear is controlled by the private `dot-git.yml` config. See [Private Git Config](/configuration/private-git/). diff --git a/docs/src/content/docs/git/notifications.md b/docs/src/content/docs/git/notifications.md index 59681318..38c2fa5b 100644 --- a/docs/src/content/docs/git/notifications.md +++ b/docs/src/content/docs/git/notifications.md @@ -42,4 +42,4 @@ The notification API requires `gh` authenticated with a classic token carrying t ## Status bar module -A status bar module refreshes `dot git-notifications --bar-json` through its own short-lived cache. Notification surfaces hide repos that are not enabled in `dot-git.yml`, while upstream notifications can match a managed fork's `remote.upstream.url`. Left click opens `dot git-notifications --bar-filter`; right click refreshes the cache. `dot doctor` verifies GitHub notification API access plus the active notification module wiring. +The Quickshell module refreshes `dot git-notifications --bar-json` through the stowed `git-notifications-bar` cache command. Notification surfaces hide repos that are not enabled in `dot-git.yml`, while upstream notifications can match a managed fork's `remote.upstream.url`. Left click opens `dot git-notifications --bar-filter`; right click refreshes the widget. `dot doctor` verifies GitHub notification API access. diff --git a/docs/src/content/docs/git/workflows.md b/docs/src/content/docs/git/workflows.md index 6d1ae97b..4f4a8ddd 100644 --- a/docs/src/content/docs/git/workflows.md +++ b/docs/src/content/docs/git/workflows.md @@ -26,7 +26,7 @@ dot git-workflows --bar-json --since "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M: ## Status bar module -A status bar module refreshes `dot git-workflows --bar-json --since ` through its own short-lived cache; left click opens the filtered TUI and right click refreshes the cache. `dot doctor` verifies `dot-git.yml`, the active workflow-runs module wiring, and the absence of legacy `git-workflow-watch` leftovers. +The JSON mode remains available to status bars and other shell integrations. `dot doctor` verifies `dot-git.yml` and the absence of legacy `git-workflow-watch` leftovers. :::note The old `git-workflow-watch` hook and its user systemd timer are obsolete and should not be installed. diff --git a/docs/src/content/docs/knowledge-base/fido2-security-key-auth.mdx b/docs/src/content/docs/knowledge-base/fido2-security-key-auth.mdx index b87189c6..4b8498db 100644 --- a/docs/src/content/docs/knowledge-base/fido2-security-key-auth.mdx +++ b/docs/src/content/docs/knowledge-base/fido2-security-key-auth.mdx @@ -48,45 +48,13 @@ Keep the existing password lines below it. With `sufficient`, a successful secur ## Add Hyprlock -Hyprlock uses its own PAM service. Omarchy's FIDO2 setup does not add this line automatically, so prepend it to `/etc/pam.d/hyprlock`: +The Omarchy shell lock screen uses its own PAM service. Omarchy's FIDO2 setup does not add this line automatically, so prepend it to `/etc/pam.d/omarchy-lock-password`: ```text auth sufficient pam_u2f.so cue [cue_prompt=Touch your security key] authfile=/etc/fido2/fido2 ``` -Leave the default `auth include login` line below it so the normal password unlock still works. - -## Update the lock-screen prompt - -Hyprlock config is stowed from the dotfiles repo. Edit the active host source, not Omarchy's upstream config directory: - -```text -~/.config/dotfiles/hypr/.config/hypr/hosts//hyprlock.conf -``` - -Use generic security-key wording so the config works with non-Yubico keys too: - -```ini -input-field { - placeholder_text = Touch Security Key or Type Password - check_text = Touch your security key -} -``` - -If the fingerprint setup script was run on a machine without a fingerprint reader, disable Hyprlock fingerprint auth in the same file: - -```ini -auth { - fingerprint:enabled = false -} -``` - -Apply the stowed config: - -```bash -dot stow -hyprctl configerrors -``` +Leave the default password stack below it so normal password unlock still works. The packaged lock-screen prompt is owned by Omarchy 4 and is not configured through a stowed `hyprlock.conf`. ## Remove fingerprint leftovers @@ -97,7 +65,7 @@ pkexec sed -i '/pam_fprintd\.so/d' /etc/pam.d/sudo /etc/pam.d/polkit-1 pkexec pacman -Rns fprintd libfprint-git ``` -Keep `fingerprint:enabled = false` in the stowed Hyprlock config. +The Omarchy shell detects fingerprint support through its separate `omarchy-lock-fingerprint` PAM service. ## PIN mode @@ -118,13 +86,13 @@ Test each layer before relying on it: 1. Run `sudo -k`, then `sudo true`. 2. Touch the security key when prompted. 3. Trigger a polkit prompt if convenient. -4. Lock the screen with Hyprlock while another session or terminal remains available. +4. Lock the screen with the Omarchy shell while another session or terminal remains available. 5. Test password fallback by trying without the security key. ## Rollback -Restore the backed-up PAM files, or remove only the added `pam_u2f.so` line from `/etc/pam.d/hyprlock` if the lock screen is the only broken part. +Restore the backed-up PAM files, or remove only the added `pam_u2f.so` line from `/etc/pam.d/omarchy-lock-password` if the lock screen is the only broken part. Do not delete `/etc/fido2/fido2` unless you are fully removing FIDO2 auth. diff --git a/docs/src/content/docs/knowledge-base/hyprsunset-dimming.mdx b/docs/src/content/docs/knowledge-base/hyprsunset-dimming.mdx deleted file mode 100644 index d7b5ee01..00000000 --- a/docs/src/content/docs/knowledge-base/hyprsunset-dimming.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Hyprsunset Dimming -description: Use gamma-only dimming on the laptop without changing colour temperature. -sidebar: - order: 8 ---- - -The laptop host has a gamma-only dimming layer built around `hyprsunset`. It is separate from normal panel brightness and does not enable a warm night-light profile by default. - -Use it when the display is still too bright at low hardware brightness, especially in a dark room. - -## Host scope - -The dimming key bindings are in the laptop host override: - -```text -~/.config/hypr/host/bindings.conf -``` - -The public source is: - -```text -~/.config/dotfiles/hypr/.config/hypr/hosts/laptop/bindings.conf -``` - -The shared and desktop `hyprsunset.conf` keep an identity profile so `hyprsunset` does nothing to the screen unless a helper changes gamma. - -## Controls - -| Binding | Action | -| --- | --- | -| `SUPER+CTRL+D` | Toggle dim mode. | -| `SUPER+CTRL+-` | Dim down. | -| `SUPER+CTRL+=` | Dim up. | -| `CTRL+BrightnessDown` | Dim down. | -| `CTRL+BrightnessUp` | Dim up. | -| `BrightnessDown` | Clear dimming, then lower hardware brightness. | -| `BrightnessUp` | Clear dimming, then raise hardware brightness. | - -Normal brightness keys clear dim mode first so a screen is not accidentally left gamma-dimmed after increasing hardware brightness. - -## State files - -The helper scripts keep state under: - -```text -~/.config/hypr/.state/ -``` - -| File | Meaning | -| --- | --- | -| `hyprsunset-dim-enabled` | Present when dim mode is active. | -| `hyprsunset-dim-level` | Last gamma level written by the helper. | - -The default toggle level is `50`. Dim steps move by `5`, clamped between `5` and `100`. - -## Commands - -Toggle dim mode: - -```bash -~/.config/hypr/bin/hyprsunset-toggle-dim -``` - -Dim down or up: - -```bash -~/.config/hypr/bin/hyprsunset-dim-step down -~/.config/hypr/bin/hyprsunset-dim-step up -``` - -Clear dim mode: - -```bash -~/.config/hypr/bin/hyprsunset-clear-dim -``` - -Each helper starts `hyprsunset` with the active host config if it is not already running. - -## Check current state - -```bash -pgrep -x hyprsunset -hyprctl hyprsunset gamma -ls ~/.config/hypr/.state -``` - -If `hyprctl hyprsunset gamma` reports `100`, dim mode is effectively off. - -## Troubleshooting - -If the keys do nothing, confirm the active host is `laptop` and the host symlink points at the laptop overrides. See [Host Overrides](/omarchy/host-overrides/). - -If dim mode gets stuck, clear it explicitly: - -```bash -~/.config/hypr/bin/hyprsunset-clear-dim -``` - -If `hyprsunset` is not running after login, check the laptop autostart override. It starts: - -```text -uwsm app -- hyprsunset --config ~/.config/hypr/host/hyprsunset.conf -``` - -## Night-light profiles - -The laptop host config includes commented examples for temperature or scheduled gamma profiles, but they are not active. Keep them commented unless you want automatic colour or gamma changes in addition to the manual dim controls. diff --git a/docs/src/content/docs/knowledge-base/index.mdx b/docs/src/content/docs/knowledge-base/index.mdx index bcb332b4..71461355 100644 --- a/docs/src/content/docs/knowledge-base/index.mdx +++ b/docs/src/content/docs/knowledge-base/index.mdx @@ -15,10 +15,9 @@ export const cards = [ { title: 'Workspace Session Recovery', href: '/knowledge-base/workspace-session-recovery/', description: 'Capture and restore Hyprland window sessions after restarts, crashes, or layout experiments.' }, { title: 'Work Browser Launchers', href: '/knowledge-base/work-browser-launchers/', description: 'Launch work Chrome, Slack, and Discord into predictable Hyprland workspaces.' }, { title: 'URL Routing and Webapps', href: '/knowledge-base/url-routing-webapps/', description: 'Route matching URLs to Omarchy webapps while keeping the normal browser fallback.' }, - { title: 'Hyprsunset Dimming', href: '/knowledge-base/hyprsunset-dimming/', description: 'Use gamma-only dimming on the laptop without changing colour temperature.' }, { title: 'Doorbell Popup', href: '/knowledge-base/doorbell-popup/', description: 'Open a Home Assistant camera popup from a doorbell or motion entity.' }, { title: 'Twitch Desktop Tools', href: '/knowledge-base/twitch-desktop-tools/', description: 'Use the Twitch menu, channel picker, and notification recovery scripts.' }, - { title: 'Resume Recovery', href: '/knowledge-base/resume-recovery/', description: 'Refresh Waybar, git caches, and Twitch notifications after suspend.' }, + { title: 'Resume Recovery', href: '/knowledge-base/resume-recovery/', description: 'Refresh the Omarchy shell and Twitch notifications after suspend.' }, { title: 'System Health Triage', href: '/knowledge-base/system-health-triage/', description: 'Collect a short health report for freezes, slowdowns, thermal issues, and noisy logs.' }, { title: 'Desktop Command Wrappers', href: '/knowledge-base/desktop-command-wrappers/', description: 'Small helper scripts for terminal launches, notifications, loading overlays, and workspace placement.' }, { title: 'GitHub Dotenv Secrets', href: '/knowledge-base/github-dotenv-secrets/', description: 'Import simple .env keys into GitHub Actions repository secrets with gh.' }, diff --git a/docs/src/content/docs/knowledge-base/resume-recovery.mdx b/docs/src/content/docs/knowledge-base/resume-recovery.mdx index 0e7ec25d..e2da8775 100644 --- a/docs/src/content/docs/knowledge-base/resume-recovery.mdx +++ b/docs/src/content/docs/knowledge-base/resume-recovery.mdx @@ -1,28 +1,28 @@ --- title: Resume Recovery -description: Refresh Waybar, git caches, and Twitch notifications after suspend. +description: Refresh the Omarchy shell and Twitch notifications after suspend. sidebar: order: 11 --- import { Steps } from '@astrojs/starlight/components'; -`on-resume` is the post-suspend recovery hook used by the Hypr host overrides. It restarts small desktop services that tend to go stale after sleep and refreshes git-related bar caches. +`on-resume-monitor` watches logind for resume events and runs `on-resume`, which refreshes desktop services and shell widgets that can go stale after sleep. ## Where it runs -Both desktop and laptop host `hypridle.conf` files call `on-resume` from `after_sleep_cmd` after waking the display: +The `dot-on-resume-monitor.service` user unit runs `on-resume-monitor` throughout the session. It calls `on-resume` after logind reports that sleep has ended. ```text -after_sleep_cmd = sleep 1 && omarchy-system-wake && on-resume +systemctl --user status dot-on-resume-monitor.service ``` -The laptop host also re-arms keyboard backlight handling before `on-resume`. +On the laptop, `on-resume` also re-arms keyboard backlight handling. You can run the recovery manually without auto-opening Twitch streams with the shared binding: ```text -SUPER+SHIFT+W +SUPER+SHIFT+R ``` Or from a terminal: @@ -35,13 +35,12 @@ on-resume `on-resume` detaches itself, writes a fresh log, then: -- Stops `twitch-notifications`. -- Clears Waybar git module cache files under `${XDG_CACHE_HOME:-~/.cache}/waybar`. -- Clears the dot upstream fetch cache under `${XDG_CACHE_HOME:-~/.cache}/dot/fetch-upstream`. -- Restarts Waybar through `uwsm-app`. -- Gracefully restarts `twitch-notifications` in auto-open mode. +- Re-arms keyboard backlight handling when available. +- Gracefully restarts `twitch-notifications`, auto-opening configured live channels after an automatic resume. +- Refreshes the Omarchy shell indicators. +- Refreshes the Quickshell git diff and notification widgets. -The `SUPER+SHIFT+W` binding passes `--no-auto-open`, so manual recovery restarts Twitch without opening live channels. +The `SUPER+SHIFT+R` binding passes `--no-auto-open`, so manual recovery restarts Twitch without opening live channels. The log is written to: @@ -51,28 +50,27 @@ ${XDG_STATE_HOME:-~/.local/state}/on-resume.log ## Manual recovery flow -Use this when the machine wakes but the bar or desktop helpers look stale: +Use this when the machine wakes but the shell or desktop helpers look stale: -1. Press `SUPER+SHIFT+W`, or run `on-resume`. -2. Wait for Waybar to disappear and return. +1. Press `SUPER+SHIFT+R`, or run `on-resume`. +2. Wait for the shell indicators to refresh. 3. If something still looks wrong, read `~/.local/state/on-resume.log`. ## Troubleshooting -If Waybar does not return, run it directly to surface errors: +If shell widgets stay stale, restart the Omarchy shell under Wayland: ```bash -uwsm-app -- waybar +QT_QPA_PLATFORM=wayland omarchy restart shell ``` -If git status in the bar stays stale, remove the caches and run recovery again: +If git status in the bar stays stale, clear the generic status-bar caches and run recovery again: ```bash -rm -f ~/.cache/waybar/git-*-waybar.json ~/.cache/waybar/git-*-waybar.json.tmp -rm -rf ~/.cache/dot/fetch-upstream +rm -rf ~/.cache/status-bar/git-diff* ~/.cache/status-bar/git-notifications* on-resume ``` diff --git a/docs/src/content/docs/knowledge-base/twitch-desktop-tools.mdx b/docs/src/content/docs/knowledge-base/twitch-desktop-tools.mdx index ef582d8c..cad8d558 100644 --- a/docs/src/content/docs/knowledge-base/twitch-desktop-tools.mdx +++ b/docs/src/content/docs/knowledge-base/twitch-desktop-tools.mdx @@ -5,7 +5,7 @@ sidebar: order: 10 --- -The Twitch scripts wrap `twitch-notifications` with a Walker menu, channel picker, and detached restart/recheck helpers. +The Twitch scripts wrap `twitch-notifications` with an Omarchy menu, channel picker, and detached restart/recheck helpers. ## Autostart diff --git a/docs/src/content/docs/knowledge-base/work-browser-launchers.mdx b/docs/src/content/docs/knowledge-base/work-browser-launchers.mdx index 61ac6a2f..c01b4b72 100644 --- a/docs/src/content/docs/knowledge-base/work-browser-launchers.mdx +++ b/docs/src/content/docs/knowledge-base/work-browser-launchers.mdx @@ -12,7 +12,7 @@ The work launchers isolate a Google Chrome work profile and place work apps on k | Binding | Action | | --- | --- | | `SUPER+ALT+B` | Launch work browser. | -| `SUPER+SHIFT+S` | Launch Slack. | +| `SUPER+CTRL+SHIFT+S` | Launch Slack. | | `SUPER+D` | Launch Discord. | ## Work Chrome profile diff --git a/docs/src/content/docs/knowledge-base/workspace-relayout.mdx b/docs/src/content/docs/knowledge-base/workspace-relayout.mdx index dc49c304..7fe8eb8c 100644 --- a/docs/src/content/docs/knowledge-base/workspace-relayout.mdx +++ b/docs/src/content/docs/knowledge-base/workspace-relayout.mdx @@ -51,7 +51,7 @@ They are grouped by tiled window count under `layouts.`. Preset version 3 The checked-in presets cover workspaces with two to five tiled windows. Their JSON order controls both the family menu and the presets within each family. Common top/bottom layouts come first, ordered from the largest top region down. Other families distinguish columns, left or right stacks, and purpose-specific arrangements. -Preset names do not need to be unique. Walker appends a rounded ratio signature, such as `75% top [75/25, 16/16/16/52]`, to distinguish variants. +Preset names do not need to be unique. The menu appends a rounded ratio signature, such as `75% top [75/25, 16/16/16/52]`, to distinguish variants. The script writes back through the stowed symlink when you edit presets, keeping the symlink in place. @@ -62,7 +62,7 @@ The script writes back through the stowed symlink when you edit presets, keeping 1. Focus the workspace to relayout. 2. Keep at least two tiled, mapped, non-hidden windows on that workspace. 3. Press `SUPER+TAB`. -4. Choose a layout family from Walker, such as **Top / bottom** or **Left / right**. +4. Choose a layout family from the Omarchy menu, such as **Top / bottom** or **Left / right**. 5. Choose an ordered scale preset from that family. 6. Let the script move windows through the temporary workspace and rebuild the split tree. @@ -105,7 +105,8 @@ The temp workspace must be a positive numeric workspace id and must not match th - `hyprctl` - `jq` - `awk` -- `omarchy-launch-walker` +- `omarchy-menu-select` +- `omarchy-menu-input` - The presets file at `~/.local/share/workspace-relayout/presets.json` If a requirement is missing, the script sends a desktop notification and exits. diff --git a/docs/src/content/docs/knowledge-base/workspace-session-recovery.mdx b/docs/src/content/docs/knowledge-base/workspace-session-recovery.mdx index 4365a8d0..a0e56cc2 100644 --- a/docs/src/content/docs/knowledge-base/workspace-session-recovery.mdx +++ b/docs/src/content/docs/knowledge-base/workspace-session-recovery.mdx @@ -107,7 +107,7 @@ When that file is missing, Chromium windows can still be matched or relaunched b ## Workspace menu -The Walker menu wraps the common actions: +The Omarchy menu wraps the common actions: ```bash workspace-menu diff --git a/docs/src/content/docs/omarchy/controls.md b/docs/src/content/docs/omarchy/controls.md index cff9a0eb..f30e1e42 100644 --- a/docs/src/content/docs/omarchy/controls.md +++ b/docs/src/content/docs/omarchy/controls.md @@ -1,6 +1,6 @@ --- title: Controls -description: The dot omarchy desktop controls menu. +description: The dot omarchy desktop controls menu and local keybindings. sidebar: order: 3 --- @@ -41,18 +41,51 @@ dot omarchy theme set # execute theme set directly When restarting Omarchy-managed apps, prefer `omarchy restart ` (via the `restart` submenu) over manual process kills. ::: +## Keybinding overrides + +The shared Hyprland config loads after Omarchy's defaults. It replaces these default bindings: + +| Binding | Omarchy default | Local action | +| --- | --- | --- | +| `SUPER+TAB` | Next workspace | Apply a saved [workspace layout](/knowledge-base/workspace-relayout/) | +| `SUPER+ALT+TAB` | Next grouped window | Edit workspace layout presets | +| `CTRL+ALT+TAB` | Focus next monitor | Unbound in Hyprland; available to applications such as Ghostty | +| `CTRL+ALT+SHIFT+TAB` | Focus previous monitor | Unbound in Hyprland; available to applications such as Ghostty | +| `SUPER+SHIFT+F` | File manager | Add the active application to the floating rules | +| `SUPER+RETURN` | Omarchy terminal launcher | Open host-configured Ghostty in the active terminal directory | +| `SUPER+SHIFT+B` | Browser | Open a private personal Chromium window | +| `SUPER+CTRL+ALT+T` | Local time notification | Show local and US time zones | +| `SUPER+ALT+-` / `SUPER+ALT+=` | Resize width by 25 | Resize width by 2 | +| `SUPER+ALT+SHIFT+-` / `SUPER+ALT+SHIFT+=` | Resize height by 25 | Resize height by 2 | + +The config also redirects `CTRL+SHIFT+T`, `CTRL+SHIFT+W`, `CTRL+TAB`, and `CTRL+SHIFT+TAB` to Herdr tab actions while Ghostty is active. Other applications receive the original key event. + +Custom bindings that conflicted with Quattro defaults use these chords instead: + +| Binding | Local action | Avoided default | +| --- | --- | --- | +| `SUPER+CTRL+ALT+P` | Power profile menu | Power panel on `SUPER+CTRL+P` | +| `SUPER+CTRL+SHIFT+S` | Slack | Google Maps on `SUPER+SHIFT+S` | +| `SUPER+ALT+X` | X notifications | Universal cut on `SUPER+X` and X on `SUPER+SHIFT+X` | +| `SUPER+CTRL+ALT+G` | GitHub notifications | Move window out of group on `SUPER+ALT+G` | +| `SUPER+CTRL+SHIFT+C` | Toggle in-call automation | Calendar on `SUPER+SHIFT+C` | +| `SUPER+CTRL+SHIFT+M` | Toggle microphone mute | Music on `SUPER+SHIFT+M` | +| `SUPER+CTRL+SHIFT+B` | Reconnect laptop Bluetooth headphones | Battery status on `SUPER+CTRL+ALT+B` | + +The config leaves Quattro's native `SUPER+W` close-window, `SUPER+SHIFT+RETURN` browser, `SUPER+ALT+RETURN` Tmux, and `SUPER+SHIFT+/` 1Password bindings in place. Application defaults such as Tmux and 1Password require Omarchy's preinstalled bindings to be enabled. + ## Power profiles Power-profile control has two entrypoints: - `dot omarchy power` opens the Omarchy power-profile submenu from the `dot` TUI. -- `SUPER+CTRL+P` runs `power-profile-menu`, a Walker picker that shows the current profile and selects one of the profiles reported by `omarchy-powerprofiles-list`. +- `SUPER+CTRL+ALT+P` runs `power-profile-menu`, an Omarchy menu that shows the current profile and selects one of the profiles reported by `omarchy-powerprofiles-list`. The menu writes the selected profile with `powerprofilesctl set`, so the available choices and final state come from `power-profiles-daemon` rather than a dot-specific state file. ### Laptop automation -The `laptop` Hypr host starts `power-profile-daemon` from `~/.config/hypr/host/autostart.conf`. It replaces Omarchy's AC udev auto-switching, which otherwise forces `performance` on AC. +The `laptop` Hypr host starts `power-profile-daemon` from `~/.config/hypr/host/autostart.lua`. It replaces Omarchy's AC udev auto-switching, which otherwise forces `performance` on AC. On laptops, the daemon keeps the profile conservative by default: @@ -74,4 +107,4 @@ omarchy-powerprofiles-list pgrep -af power-profile-daemon ``` -If `SUPER+CTRL+P` opens no choices, check that `omarchy-powerprofiles-list` prints profile IDs. If the laptop policy is not running, confirm the active host symlink points at the laptop overrides and that Hyprland sourced `~/.config/hypr/host/autostart.conf`; see [Host Overrides](/omarchy/host-overrides/). +If `SUPER+CTRL+ALT+P` opens no choices, check that `omarchy-powerprofiles-list` prints profile IDs. If the laptop policy is not running, confirm the active host symlink points at the laptop overrides and that Hyprland loaded `~/.config/hypr/host/autostart.lua`; see [Host Overrides](/omarchy/host-overrides/). diff --git a/docs/src/content/docs/omarchy/host-overrides.md b/docs/src/content/docs/omarchy/host-overrides.md index 85cf940f..96ea47b9 100644 --- a/docs/src/content/docs/omarchy/host-overrides.md +++ b/docs/src/content/docs/omarchy/host-overrides.md @@ -1,17 +1,13 @@ --- title: Host Overrides -description: Managed Omarchy repos and stowed host configuration. +description: Stowed Omarchy configuration and host overrides. sidebar: order: 2 --- -## Managed Omarchy repos +## UWSM environment -`dot` tracks a small set of Omarchy components as git repos and keeps them on the expected branch: - -- `waybar` and `uwsm` — single-branch Omarchy repos expected on `main`. - -`dot init` clones these into `~/.config/{waybar,uwsm}`. If a stock Omarchy config directory already exists there and is not a git repo, init moves it aside with a `.dot-init-backup-*` suffix before cloning. `dot update` syncs them, and `dot doctor` verifies their worktree branches. +Quattro provides `/usr/share/uwsm/env.d/10-omarchy` and `/usr/share/omarchy/default/uwsm/default`, including the user-local binary path and mise activation. The stowed `~/.config/uwsm/env.d/90-dotfiles` adds only the custom Hypr helper path and OpenCode feature flags, without copying the generated `99-omarchy-upgrade-env` or timestamped upgrade backups. Omarchy keeps `BROWSER` shell-scoped so browser default selection continues to work. ## Ghostty host overrides @@ -30,14 +26,14 @@ The shared config uses 8px window padding. The laptop override keeps that paddin ## Hyprland host overrides -Hyprland config is a stowed dotfiles package (`hypr/.config/hypr/`, conf-only), not a tracked repo. Host-specific overrides live under `~/.config/hypr/hosts/$OMARCHY_HOST`, selected by the runtime `~/.config/hypr/host` symlink. +Hyprland config is a stowed dotfiles package (`hypr/.config/hypr/`), not a tracked repo. Host-specific Lua overrides live under `~/.config/hypr/hosts/$OMARCHY_HOST`, selected by the runtime `~/.config/hypr/host` symlink. - `dot stow` lays down the Hypr package with `--no-folding` and creates/repairs `~/.config/hypr/host` to point at the active host. - `dot init` selects the Hypr host early (via `--host `, defaulting to `OMARCHY_HOST` or `desktop`), and the stow phase creates the `host` symlink. - `dot doctor` checks the host link and flags any leftover legacy `omarchy-hypr` clone at `~/.config/hypr`. -- Shared Hyprland-loaded config files wrap host override `source = ~/.config/hypr/host/*.conf` lines in Hyprland's `hyprlang noerror` guard, so a missing host override during stow, update, or migration does not leave Hyprland in an error loop. +- Both host monitor overrides detect common virtual machines through DMI data and use unscaled toolkit and fallback monitor settings; bare-metal hosts keep their normal HiDPI scale. -Shared Hypr autostart lives in `~/.config/hypr/autostart.conf` and runs on every host before the selected host override is sourced. Host-only services stay in `~/.config/hypr/host/autostart.conf`. KDE Connect starts on both hosts, while the OpenCode server starts only on the desktop. +Shared Hypr autostart lives in `~/.config/hypr/autostart.lua` and runs on every host before the selected host override is loaded. Host-only services stay in `~/.config/hypr/host/autostart.lua`. KDE Connect starts on both hosts, while the OpenCode server starts only on the desktop. :::caution[Retired omarchy-hypr clone] A machine with the retired `~/.config/hypr` `omarchy-hypr` clone halts `dot update` until the clone is backed up and re-stowed. Hyprland config is a stowed package, not a cloned repo. @@ -47,6 +43,5 @@ A machine with the retired `~/.config/hypr` `omarchy-hypr` clone halts `dot upda - `OMARCHY_HOST` — the Hypr host override name (e.g. `desktop`, `laptop`). - `OMARCHY_REPO_BASE_DIR` — Omarchy repo base path (default `~/.config`). -- `DOT_OMARCHY_BRANCH` — branch override during sync. See [Environment Variables](/configuration/environment/) for the full list. diff --git a/docs/src/content/docs/omarchy/index.mdx b/docs/src/content/docs/omarchy/index.mdx index 32e35252..3c3e76be 100644 --- a/docs/src/content/docs/omarchy/index.mdx +++ b/docs/src/content/docs/omarchy/index.mdx @@ -1,6 +1,6 @@ --- title: Omarchy & Hyprland -description: Managed Omarchy repos and stowed host overrides. +description: Stowed Omarchy Quattro customisations and host overrides. sidebar: label: Overview order: 1 @@ -8,27 +8,25 @@ sidebar: import { CardGrid, LinkCard } from '@astrojs/starlight/components'; -These dotfiles run on [Omarchy](https://omarchy.org). Some Omarchy components are tracked as managed git repos that `dot` keeps in sync, while [Hyprland](https://hyprland.org) and Ghostty config are stowed dotfiles packages with per-host overrides. +These dotfiles run on [Omarchy](https://omarchy.org). UWSM, [Hyprland](https://hyprland.org), and Ghostty customisations are stowed dotfiles packages, with per-host overrides where needed. + -## Managed Omarchy repos +## UWSM -- `waybar` and `uwsm` are single-branch Omarchy repos expected on `main`. -- `dot update` syncs these by default; `dot doctor` verifies their worktree branches. -- Waybar module commands are stowed from `scripts/.local/bin/`; the Waybar repo contains only their config and Waybar-specific test and benchmark harnesses. -- `package-updates-bar` backs off automatic AUR checks after HTTP errors. An explicit refresh retries immediately, so a recovered AUR connection clears stale unavailable state. +Omarchy Quattro owns its UWSM bootstrap and session defaults under `/usr/share`. The `uwsm/.config/uwsm/env.d/90-dotfiles` package adds only the user environment values needed by these dotfiles. `dot stow` removes the retired `timmo001/omarchy-uwsm` checkout before linking that override; upgrade-generated preservation files and backups are not imported. ## Hyprland -Hyprland config is **not** a tracked Omarchy repo. It is a stowed dotfiles package (`hypr/.config/hypr/`, conf-only) laid down with `--no-folding`, with host-specific overrides selected by a runtime symlink. See [Host Overrides](/omarchy/host-overrides/). +Hyprland config is **not** a tracked Omarchy repo. It is a stowed dotfiles package (`hypr/.config/hypr/`) laid down with `--no-folding`, with host-specific Lua overrides selected by a runtime symlink. See [Host Overrides](/omarchy/host-overrides/). -Shared application floating rules generated by [`float-app`](https://github.com/timmo001/float-app) live in the stowed `~/.config/hypr/float-app.conf`. `looknfeel.conf` includes that file, so new rules remain part of the Hypr package rather than becoming unmanaged live config. +Shared application floating rules generated by [`float-app`](https://github.com/timmo001/float-app) live in the stowed `~/.config/hypr/float-app.lua`. `looknfeel.lua` requires that module, so new rules remain part of the Hypr package rather than becoming unmanaged live config. -`SUPER+W` runs the stowed `close-active-window` script. It lets Ghostty handle its own close confirmation and uses Hyprland's normal close action for other applications. +`SUPER+W` uses Hyprland's normal close-window action. ## Ghostty @@ -36,10 +34,16 @@ Ghostty config is also stowed from `ghostty/.config/ghostty/`. The stowed launch Terminal bells mark the Ghostty tab title without requesting focus, so notifications do not switch Hyprland workspaces. +## Shell + +Omarchy 4 runs a single [Quickshell](https://quickshell.outfoxxed.me) process (`omarchy-shell`) for the bar, notifications, OSD, launcher, and settings. These dotfiles extend it through a generated `shell.json` and a few custom bar plugins rather than forking it. See [Shell (Quickshell)](/omarchy/shell/). + +## Herdr + `dot update` restores the Herdr Lazy lockfile, links the stowed local Herdr plugins, and starts the title watcher. Agent Usage and GitHub PR status are tracked through the same Herdr Lazy list and lockfile. The Agent sidebar shows the workspace-aware title and current branch PR status, billing provider and shortest limit window, then context usage. `ALT+X`, then `U` opens the full limits pane; `ALT+X`, then `SHIFT+U` refreshes the meters. Browser-cookie import is disabled in the managed Herdr server environment, so OpenCode Go uses local estimates unless an explicit cookie is supplied outside this public repository. Agent Usage notifications are disabled by default and no credentials are stored in dotfiles. The local Mise Task Runner opens with `ALT+X`, then `SHIFT+M`. It lists local tasks from the focused pane's directory, previews the selected task, and runs it in a focused new tab so its output remains available. The memex session palette opens with `ALT+X`, then `F`. -Ghostty's normal tab shortcuts are redirected to Herdr while its client window is active: `CTRL+SHIFT+T` creates a tab in the current workspace, `CTRL+SHIFT+W` closes the current tab, and `CTRL+TAB` / `CTRL+SHIFT+TAB` select the next or previous tab. Other applications receive the original key event unchanged. New tabs skip the naming prompt; the local Terminal Title plugin mirrors each pane's existing Zsh or application title into its Herdr tab label. `ALT+X`, then `S` opens Sessionizer to focus an existing workspace or pick a Home Assistant repository under `~/repos/home-assistant`; `ALT+X`, then `Up` opens its worktree picker. `ALT+X`, then `W` retains Herdr's built-in workspace picker. `ALT+X`, then `G` toggles a lazygit side pane, while `ALT+X`, then `SHIFT+G` opens `dot git-diff` as an overlay. Both inherit the focused pane's current directory. `ALT+X`, then `Y` opens Yazi beside the focused pane, and `ALT+X`, then `SHIFT+Y` opens it in a new tab. Herdr Lazy keeps the plugin list and exact lockfile in dotfiles for reproducible installs across systems; Renovate proposes lock commit updates and `herdr-lazy restore` applies the merged lockfile on each system. The tracked mirror plugin exposes configured remote Herdr workspaces in the local sidebar; machine-specific hosts remain in the private overlay, and `ALT+X`, then `ALT+N` creates a workspace on the default remote host. The lazygit plugin's AI backend, pane widths, and commit prompt are also stowed, while generated and per-pane state remains local. `ALT+X`, then `SHIFT+L` opens the manager. Reviewr opens an agent-aware branch diff and review pane with `ALT+X`, then `SHIFT+V`; automatic opening is disabled so it does not change new-worktree layouts. Experimental pane history preserves recent terminal output across full Herdr server restarts, and Kitty graphics rendering enables compatible inline images in Ghostty. `SUPER+Q` and `SUPER+SHIFT+Q` attach another client to the shared default session. `ALT+X`, then `Q` detaches the client and leaves every pane process running. `CTRL+D` keeps its normal shell EOF behaviour, including in the final root shell; closing the final tab does not stop the persistent server. Outside Herdr the tab shortcuts keep their normal Ghostty behaviour. The global `SUPER+SHIFT+T` override sends Ghostty a dedicated shortcut that always creates a Ghostty tab, including while Herdr is focused. +Ghostty's normal tab shortcuts are redirected to Herdr while its client window is active: `CTRL+SHIFT+T` creates a tab in the current workspace, `CTRL+SHIFT+W` closes the current tab, and `CTRL+TAB` / `CTRL+SHIFT+TAB` select the next or previous tab. Other applications receive the original key event unchanged. New tabs skip the naming prompt; the local Terminal Title plugin mirrors each pane's existing Zsh or application title into its Herdr tab label. `SUPER+Q` and `SUPER+SHIFT+Q` attach another client to the shared default session. `ALT+X`, then `R` opens the repository picker generated from private `dot-git.yml` repositories and shortcuts. For direct shell access, `SUPER+RETURN` opens Ghostty and `SUPER+SHIFT+T` opens a new Ghostty tab. diff --git a/docs/src/content/docs/omarchy/shell.md b/docs/src/content/docs/omarchy/shell.md new file mode 100644 index 00000000..e628f9ec --- /dev/null +++ b/docs/src/content/docs/omarchy/shell.md @@ -0,0 +1,123 @@ +--- +title: Shell (Quickshell) +description: The Omarchy 4 Quickshell shell, its generated shell.json, and the custom bar plugins. +--- + +Omarchy 4 replaces Waybar with a single long-running [Quickshell](https://quickshell.outfoxxed.me) process, `omarchy-shell`. That one process hosts the top bar, the notification daemon, the on-screen display, the launcher, and the settings panel. Restarting "the shell" restarts all of them together. + +These dotfiles do not fork the shell. They extend it in two supported ways: a generated `shell.json` that lays out the bar, and a small set of user plugins that the bar loads as extra widgets. + +## Source of truth + +Two things drive the bar, and neither is hand-edited live: + +- **`~/.config/omarchy/shell.json`** is generated, not stowed. `dot` renders it from Omarchy's shipped default and inserts the personal modules. The generator is `dot/src/lib/omarchyShellConfig.ts` (`mergeOmarchyShellConfig`). The live file is mode `0600` and tracked by neither dotfiles repo. +- **Bar plugins** live under `omarchy/.config/omarchy/plugins//` in this repo and stow to `~/.config/omarchy/plugins//`. Each plugin is a `manifest.json` plus an entry-point QML file. + +To change the bar, edit the generator (then rebuild `dot`) or edit a plugin's QML, never the live `shell.json`. + +:::caution[Omarchy's shell source is read-only] +The shell itself lives in `~/.local/share/omarchy/shell/`. Reading it is useful (the `BarWidget` / `WidgetButton` base classes live there), but edits are lost on `omarchy update`. Customisation belongs in plugins and the generated config. +::: + +## Generated `shell.json` + +`dot stow` regenerates `shell.json` for the active [host](/omarchy/host-overrides/), starting from Omarchy's default and adding personal modules around the stock ones ("add, not remove"). The generator owns widget sections and ordering so desktop and laptop stay aligned; rearranging widgets through Quattro is reset on the next stow. The merge is idempotent: it only rewrites the file when the rendered content changes. + +Per-host differences: + +- **Bar position**: `bottom` on `laptop`, `top` on every other host. +- **Idle timers**: screensaver at 2.5 minutes and lock at 5 minutes on `laptop`; screensaver at 30 minutes and lock at 60 minutes on every other host. +- **Home Assistant sensors**: temperature, CO2, doorbell, and VOC entities differ per host (desktop vs laptop). + +Layout changes applied on top of the default bar: + +- **Left**: Omarchy's persistent workspaces widget is swapped for `timmo.workspaces`, then a calendar module is appended. +- **Centre**: the clock stays as the centre anchor (the stock config gear only renders next to a centred clock), the weather is pulled out, personal status widgets are inserted before the system-update group, and the doorbell trigger goes last. Centre widgets get `revealOnHover`, so a class-hidden module fades in dimmed when the centre cluster is hovered. +- **Right**: the Home Assistant sensors are inserted before the default tray cluster, and weather moves after the personal widgets immediately before the stock network widget. + +The personal status widgets read from bar-agnostic scripts, `dot` JSON output, and Home Assistant. See [Bar Integrations](/bar-integrations/) for the `--bar-json` commands behind the git and notification cells. + +## Stock Quattro comparison + +The generated config starts from Omarchy Quattro's shipped `shell.json` and modifies that layout rather than replacing it wholesale. + +### Removed or replaced + +No stock widget is removed without a replacement. + +`omarchy.workspaces` is replaced in place by `timmo.workspaces`. The stock widget keeps persistent workspace slots visible; the replacement shows only workspaces that currently exist, displays the focused workspace number at full opacity, and dims the others. + +### Moved + +`omarchy.weather` moves from the centre section to the right section after the personal widgets and immediately before `omarchy.network`. The original stock entry and implementation are preserved. + +No other stock widget changes section. `omarchy.system-update` remains in the centre after the added status widgets, while the complete stock tray cluster remains on the right in its original order. + +### Added widgets + +| Section | Added widgets | +| --- | --- | +| Left | Calendar | +| Centre, before `omarchy.system-update` | Time check, in-call state, NAS activity, GitHub notifications, repository diff status, GitHub workflow status, package updates, Twitch notifications | +| Centre, after `omarchy.system-update` | Doorbell | +| Right, before `omarchy.tray` | Heating, CO₂ alert, rain, temperature | +| Right, laptop only | VOC alert, dining-room temperature | + +The centre additions use `revealOnHover`: status cells hidden in their normal inactive state appear dimmed while the centre cluster is hovered. Attention and active states remain visible according to each widget's class rules. + +### Retained stock layout + +These stock widgets retain their implementations and stay in their original sections: + +- **Left:** `omarchy.menu`. +- **Centre:** `omarchy.indicators`, `omarchy.clock`, `omarchy.keyboard-layout`, and `omarchy.system-update`. +- **Right:** `omarchy.tray`, `omarchy.agents`, `omarchy.bluetooth`, `omarchy.network`, `omarchy.audio`, `omarchy.monitor`, and `omarchy.power`. + +The stock clock formats, opaque bar, config version, plugin list, and `omarchy.clock` centre anchor are also preserved. + +### Host overrides + +| Setting | Stock Quattro | Desktop | Laptop | +| --- | --- | --- | --- | +| Bar position | Top | Top | Bottom | +| Screensaver | 2.5 minutes | 30 minutes | 2.5 minutes | +| Lock | 5 minutes | 60 minutes | 5 minutes | + +Home Assistant entity IDs and the doorbell popup monitor and size also vary by host. The laptop adds the VOC and dining-room temperature widgets listed above; the desktop omits them. + +## Custom plugins + +A plugin is a folder with `manifest.json` (schema version 1, an `id` like `timmo.`, its `kinds`, and entry-point QML) plus the QML itself. A bar widget extends `BarWidget`, reads per-instance settings from `shell.json` via `setting(name, fallback)`, and uses `WidgetButton` for clickable cells. + +| Plugin | Kind | What it does | +| --- | --- | --- | +| `timmo.command` | bar-widget | Runs a shell command on an interval and renders its status-bar JSON (`text` / `tooltip` / `class`). The Waybar `custom/*` equivalent. | +| `timmo.stream-command` | bar-widget | Runs a long-running command that streams status-bar JSON lines and renders the latest line (for watchers like `ha-watch-singleton`). | +| `timmo.workspaces` | bar-widget | Workspace numbers without persistent workspaces: only existing workspaces show, the focused one at full opacity and the rest dimmed. | + +`timmo.command` and `timmo.stream-command` both support `classColors` (class-name to colour), `hideClasses`, `onClick` / `onClickRight`, and `revealOnHover`, so the generator can style and wire every cell without bespoke QML per module. + +:::note[New plugins need a stow] +`~/.config/omarchy/plugins/` is a real directory with per-plugin symlinks. A brand-new plugin needs `dot stow` to create its symlink before the shell sees it; editing an existing plugin's files is already live. +::: + +## Reloading the shell + +| Change | Action | +| --- | --- | +| `shell.json` layout or settings, existing modules only | Hot-reloads on save, nothing to run | +| New plugin added | `omarchy shell shell rescanPlugins`, then the hot-reload picks it up | +| User plugin QML edited | Hot-reloads on save, nothing to run | +| Omarchy's first-party shell QML edited, or hot-reload fails | `omarchy restart shell` (full restart) | + +`dot update` bakes this in: it regenerates `shell.json` and reloads the running shell **only when the rendered config changed**. A standalone `dot stow` regenerates the file but does not reload. + +:::caution[Force Wayland on restart] +`omarchy restart shell` inherits the caller's environment. If `QT_QPA_PLATFORM=xcb`, Quickshell starts under XWayland, the layer-shell surface cannot attach, and the bar renders as a floating window with no error. Interactive shells here set `QT_QPA_PLATFORM="wayland;xcb"`, and `dot update` forces `QT_QPA_PLATFORM=wayland` on its reload. From any non-interactive context (SSH, systemd, an agent shell), force Wayland explicitly: + +```bash +QT_QPA_PLATFORM=wayland omarchy restart shell +``` + +::: diff --git a/dot/AGENTS.md b/dot/AGENTS.md index 8c4a5d17..66b3602c 100644 --- a/dot/AGENTS.md +++ b/dot/AGENTS.md @@ -78,7 +78,7 @@ src/ GitHub.ts — Shared GitHub CLI/API wrapper with rate-limit checks and retries GitNotifications.ts — GitHub notification inbox state and thread actions GitStaging.ts — Git status/add/commit operations - RepoWatcher.ts — Hybrid poll loop (Waybar cache → 10s poll), PubSub state + RepoWatcher.ts — Hybrid poll loop (initial poll → 10s poll), PubSub state relativeTime.ts — Shared compact relative timestamp formatter WorkflowRuns.ts — Watched GitHub Actions run state for locally checked-out HEAD commits workflowStatus.ts — Shared GitHub Actions status classification helpers @@ -98,7 +98,6 @@ src/ OutputLog.ts — Scrollable output log service Renderer.ts — OpenTUI renderer service Toast.ts — Toast notification overlay service - WaybarCache.ts — Waybar cache JSON reader for fast startup tui/ App.ts — Top-level app shell, view stack, global keyboard, action routing MainMenu.ts — MenuList menu built from menu registry @@ -114,7 +113,6 @@ src/ lib/ extractNativeLib.ts — Native .so extraction from bunfs initState.ts — First-use setup state marker helpers - omarchySync.ts — First-use Omarchy repo clone/sync helpers packageSetup.ts — Strict package and mise setup helpers for init/install selfUpdate.ts — Binary rebuild logic skillCheck.ts — Skill reference validation logic @@ -132,7 +130,7 @@ src/ 4. `App` manages a view stack (main menu ↔ diff view ↔ git log view ↔ workflows view ↔ notifications view ↔ omarchy menu) 5. Menu items have typed actions: `command` (suspend/resume), `silent` (background), `notify` (background + toast), `view` (navigate), `submenu` (nested) 6. `CommandRunner` handles suspend/resume for terminal commands, silent background execution, and notify-style commands with toast feedback -7. `RepoWatcher` loads Waybar cache for instant diff first paint, then polls every 10s +7. `RepoWatcher` runs an initial poll for first paint, then polls every 10s ### Menu Registry @@ -157,7 +155,7 @@ MenuItem action types: - **Services**: `Context.Service` + static `layer` property for Effect services - **Static layers**: Each service class exposes `ServiceName.layer` (not a separate `*Live` export). Layer is built with `Layer.effect(ServiceName, Effect.gen(...))` -- **Domain errors**: `Schema.TaggedErrorClass` per service (`DotDiffError`, `GitStagingError`). WaybarCache has no error type +- **Domain errors**: `Schema.TaggedErrorClass` per service (`DotDiffError`, `GitStagingError`) - **Error handling**: `Effect.catch` (v4 rename of `catchAll`) for recovery; tagged errors flow through the type channel - **Named spans**: `Effect.fn("Name")` for effectful functions with arguments; `Effect.gen` + `Effect.withSpan("Name")` for zero-arg named effects (since `Effect.fn` returns a function, not an Effect) - **Testable time**: `Clock.currentTimeMillis` for timestamps instead of `new Date()` @@ -281,11 +279,10 @@ After that bootstrap build, run the checked-out binary directly. If private dotf ## External Dependencies -- `~/.cache/waybar/git-diff-waybar.json` — Waybar cache for fast startup - `NOTES` / `DOT_NOTES_DIR` — notes vault used by the standalone `notes` CLI/MCP server and OpenCode note commands - `DOT_USAGE_DIR` — usage event root for `dot usage` (default `$XDG_STATE_HOME/tool-usage`). `DOT_USAGE_DISABLE` disables live dot recording - `~/.config/dotfiles-private/dot-git.yml` — private git repo config for clone/bootstrap, doctor checks, `dot git-diff`, `dot git-log`, `dot git-workflows`, and `dot git-notifications --bar-json`; `activity`, `workflows`, and `notifications` each require explicit `enabled` plus 5-field cron `schedule` keys, and `notifications.bar.ignore_bot_activity` controls status-bar bot noise -- `gh` authenticated with a classic token carrying `notifications` or `repo` scope — required for `dot git-notifications` and its Waybar module +- `gh` authenticated with a classic token carrying `notifications` or `repo` scope — required for `dot git-notifications` and its status-bar module - `lazygit` — launched via suspend/resume on Enter in diff view - `opencode` — CLI launched via suspend/resume for interactive sessions from the diff view - `omarchy` — various subcommands for desktop management @@ -330,7 +327,7 @@ dot init --help # init help prints without side effects dot help # help prints ``` -`dot init` clones the managed Omarchy repos into `~/.config/{waybar,uwsm}`. If a stock Omarchy config directory already exists there and is not a git repo, init moves it aside with a `.dot-init-backup-*` suffix before cloning; do not delete those backups automatically. Hyprland config is a stowed dotfiles package (`hypr/.config/hypr/`, conf-only) laid down with `--no-folding`, with the runtime `~/.config/hypr/host` symlink selecting the host overrides. Ghostty config is also stowed from `ghostty/.config/ghostty/`; `dot stow` backs up the retired `timmo001/omarchy-ghostty` clone before linking the stowed config. +UWSM environment overrides are stowed from `uwsm/.config/uwsm/env.d/90-dotfiles`, while Quattro owns its defaults under `/usr/share`. `dot stow` removes the retired `timmo001/omarchy-uwsm` checkout without importing its generated migration files. Hyprland config is a stowed dotfiles package (`hypr/.config/hypr/`) laid down with `--no-folding`, with the runtime `~/.config/hypr/host` symlink selecting the host overrides. Ghostty config is also stowed from `ghostty/.config/ghostty/`; `dot stow` backs up the retired `timmo001/omarchy-ghostty` clone before linking the stowed config. ## Logging Style diff --git a/dot/src/cli/spec.ts b/dot/src/cli/spec.ts index bae980a9..427ad3d0 100644 --- a/dot/src/cli/spec.ts +++ b/dot/src/cli/spec.ts @@ -169,18 +169,12 @@ export const cliCommands: readonly CliCommandSpec[] = [ completion: "file", description: "Init log path (default: ~/.local/state/dot/init.log)", }, - { - name: "--branch", - valueName: "name", - description: "Branch override for Omarchy repos", - }, helpOption, ], examples: [ "dot init --noninteractive", "dot init --host laptop --noninteractive", "dot init --force --noninteractive", - "dot init --branch main", ], }, { @@ -296,12 +290,12 @@ export const cliCommands: readonly CliCommandSpec[] = [ "Herdr integration Herdr binary and OpenCode integration installed", "GitHub MCP auth gh token available for DOT_GH_MCP_BEARER", "Git config Managed include is active", - "Workflow runs Repo list, status bar config, legacy watcher cleanup", - "Git notifications API scope and status bar notification module wiring", + "Workflow runs Repo list and legacy watcher cleanup", + "Git notifications API scope and notification access", "Doctor startup Startup notification timer", "uwsm session PATH ~/.local/bin on the uwsm/systemd user-environment PATH", "Daily volume reset Laptop-only optional timer", - "Omarchy repos Diff repos + worktree branch correctness", + "Omarchy config Managed repos and Hypr host-link correctness", "Legacy Hypr repo Flags a retired omarchy-hypr clone at ~/.config/hypr", "Neovim theme link Repairs a mislocated omarchy-nvim theme.lua symlink", "Private access Private dotfiles overlay enabled or explains why it is disabled", diff --git a/dot/src/commands/Init.ts b/dot/src/commands/Init.ts index bdbc1a84..345536a9 100644 --- a/dot/src/commands/Init.ts +++ b/dot/src/commands/Init.ts @@ -1,4 +1,4 @@ -import { Effect, Schema } from "effect"; +import { Duration, Effect, Schema } from "effect"; import { existsSync, lstatSync, @@ -10,6 +10,7 @@ import { basename, join } from "path"; import { Config } from "../services/Config.js"; import { CommandExecutor } from "../services/CommandExecutor.js"; import { OutputLog } from "../services/OutputLog.js"; +import { Launcher } from "../services/Launcher.js"; import { agentsSync } from "./AgentsSync.js"; import { install } from "./Install.js"; import { setupPrivateRepo } from "./SetupPrivateRepo.js"; @@ -21,7 +22,6 @@ import { installMissingArchPackages, installMiseTools, } from "../lib/packageSetup.js"; -import { syncOmarchyRepos } from "../lib/omarchySync.js"; import { ensureLocalesGenerated } from "../lib/localeSetup.js"; import { configureFirewallRules } from "../lib/firewallSetup.js"; import { installGhExtensions } from "../lib/ghExtensions.js"; @@ -45,14 +45,15 @@ import type { ConfigService } from "../services/Config.js"; const GIT_INCLUDE_PATH = "~/.config/git/config.dotfiles"; const DOCTOR_STARTUP_TIMER_UNIT = "dot-doctor-startup.timer"; +const RESUME_MONITOR_SERVICE_UNIT = "dot-on-resume-monitor.service"; const DEFAULT_INIT_OMARCHY_HOST = "desktop"; const INIT_OMARCHY_HOSTS = ["desktop", "laptop"] as const; const ETC_SHELLS = "/etc/shells"; +const OMARCHY_HOST_PERSIST_TIMEOUT_SECONDS = 10; /** Upper bound (seconds) for each init phase. */ const INIT_STEP_TIMEOUT_SECONDS = { locale: 3 * 60, - omarchyRepos: 6 * 60, hostLinks: 60, install: 5 * 60, mise: 10 * 60, @@ -78,7 +79,6 @@ interface InitOptions { readonly confirm: boolean; readonly noninteractive: boolean; readonly force: boolean; - readonly branch?: string; readonly host?: string; readonly log?: string; } @@ -87,7 +87,6 @@ interface InitOptionsDraft { confirm: boolean; noninteractive: boolean; force: boolean; - branch?: string; host?: string; log?: string; } @@ -116,7 +115,6 @@ const booleanInitOptions = new Map([ ]); const valueInitOptions = new Map([ - ["--branch", (options, value) => void (options.branch = value)], ["--host", (options, value) => void (options.host = value)], ["--log", (options, value) => void (options.log = value)], ]); @@ -300,7 +298,7 @@ function printInitHelp(): void { console.log(`Usage: dot init [options] Run the one-time first-use setup workflow for a fresh machine. Init prepares -repos, stow links, mise tools, packages, and machine hooks. After init +stow links, mise tools, packages, and machine hooks. After init completes, run dot doctor, then use dot update for ongoing maintenance. Options: @@ -310,13 +308,11 @@ Options: --force Re-run init even if the machine looks initialised --host Hypr host to link before stow (default: OMARCHY_HOST or desktop) --log Init log path (default: ~/.local/state/dot/init.log) - --branch Branch override for Omarchy repos --help, -h Show this help message Examples: dot init --noninteractive - dot init --host laptop --noninteractive - dot init --branch main`); + dot init --host laptop --noninteractive`); } function initOmarchyHost(options: InitOptions): string { @@ -408,10 +404,53 @@ function resolveInitOptions( }); } +function persistOmarchyHostEnv( + host: string, +): Effect.Effect { + return Effect.gen(function* () { + const executor = yield* CommandExecutor; + const log = yield* OutputLog; + const file = "/etc/environment"; + const line = `OMARCHY_HOST=${host}`; + // Idempotent: no-op when already correct, otherwise replace any existing + // OMARCHY_HOST line or append one. pam_env reads /etc/environment at login, + // so the value reaches the graphical session and every terminal. + const script = [ + `grep -qx '${line}' ${file} && exit 0`, + `if grep -q '^OMARCHY_HOST=' ${file}; then`, + ` sed -i 's|^OMARCHY_HOST=.*|${line}|' ${file}`, + `else`, + ` printf '%s\\n' '${line}' >> ${file}`, + `fi`, + ].join("\n"); + + const persistCommand = + process.getuid?.() === 0 + ? (["bash", ["-c", script]] as const) + : (yield* executor.exitCode("which", ["pkexec"])) === 0 + ? (["pkexec", ["bash", "-c", script]] as const) + : (["sudo", ["-n", "bash", "-c", script]] as const); + + const exitCode = yield* executor + .exitCode(persistCommand[0], persistCommand[1]) + .pipe( + Effect.timeout(Duration.seconds(OMARCHY_HOST_PERSIST_TIMEOUT_SECONDS)), + Effect.catch(() => Effect.succeed(1)), + ); + if (exitCode === 0) { + yield* log.info(`Persisted ${line} to ${file}`); + } else { + yield* log.warn( + `Could not persist OMARCHY_HOST to ${file} (exit ${exitCode}); set it manually`, + ); + } + }); +} + function ensureInitHyprHostLink( config: ConfigService, options: InitOptions, -): Effect.Effect { +): Effect.Effect { return Effect.gen(function* () { const log = yield* OutputLog; if (!config.omarchy.enabled) return; @@ -419,16 +458,36 @@ function ensureInitHyprHostLink( const host = initOmarchyHost(options); yield* log.section("Omarchy Host Links"); - // Select the host now so host-suffixed stow packages and the Hypr host - // link resolve correctly during the stow phase. + // Validate the requested host against the stowed package source so a typo + // fails fast, even on a fresh machine before hypr is stowed. + const sourceHostDir = join( + config.publicDotfiles, + "hypr", + ".config", + "hypr", + "hosts", + host, + ); + if (!existsSync(sourceHostDir)) { + return yield* fail( + `Unknown Hypr host '${host}': missing ${displayPath(sourceHostDir)}. Pass --host with a configured host.`, + ); + } + + // Select the host now so host-suffixed stow packages and the Hypr host link + // resolve correctly during the stow phase. setEnv(ENV.OMARCHY_HOST, host); - const hostDir = join(hyprRepoPath(config), "hosts", host); - if (!existsSync(hostDir)) { - // Hypr config is now a stowed dotfiles package; the hosts directory and - // host link are created during the install/stow phase. + // Persist the host for future login sessions so terminals, status scripts, + // and dot doctor see OMARCHY_HOST without a transient init env. + yield* persistOmarchyHostEnv(host); + + // The live host directory only exists once hypr is stowed; when it is not + // there yet, the stow phase creates the host link after stowing. + const liveHostDir = join(hyprRepoPath(config), "hosts", host); + if (!existsSync(liveHostDir)) { yield* log.info( - `Hypr host config ${displayPath(hostDir)} not present yet; stow will create the host link`, + `Hypr host '${host}' selected; host link will be created during stow`, ); return; } @@ -539,24 +598,18 @@ function runUserSystemctl( }); } -function enableDoctorStartupTimer(): Effect.Effect< - void, - InitError, - CommandExecutor | OutputLog -> { +function enableUserUnit( + unit: string, + sectionTitle: string, +): Effect.Effect { return Effect.gen(function* () { const executor = yield* CommandExecutor; const log = yield* OutputLog; - const unitPath = join( - CONFIG_DIR, - "systemd", - "user", - DOCTOR_STARTUP_TIMER_UNIT, - ); + const unitPath = join(CONFIG_DIR, "systemd", "user", unit); - yield* log.section("Enable Doctor Startup Timer"); + yield* log.section(sectionTitle); if ((yield* executor.exitCode("which", ["systemctl"])) !== 0) { - yield* log.warn("Skipping doctor startup timer (systemctl not found)"); + yield* log.warn(`Skipping ${unit} (systemctl not found)`); return; } @@ -565,8 +618,8 @@ function enableDoctorStartupTimer(): Effect.Effect< } yield* runUserSystemctl(["daemon-reload"]); - yield* runUserSystemctl(["enable", "--now", DOCTOR_STARTUP_TIMER_UNIT]); - yield* log.info(`Enabled ${DOCTOR_STARTUP_TIMER_UNIT}`); + yield* runUserSystemctl(["enable", "--now", unit]); + yield* log.info(`Enabled ${unit}`); }); } @@ -728,7 +781,13 @@ function ensureLoginShellZsh(): Effect.Effect< } /** Run the one-time first-use setup workflow for a fresh machine. */ -export function init(rawArgs: readonly string[]) { +export function init( + rawArgs: readonly string[], +): Effect.Effect< + void, + unknown, + Config | CommandExecutor | OutputLog | Launcher +> { return Effect.gen(function* () { const config = yield* Config; const log = yield* OutputLog; @@ -753,13 +812,6 @@ export function init(rawArgs: readonly string[]) { INIT_STEP_TIMEOUT_SECONDS.locale, ensureLocalesGenerated, ); - yield* requiredInitStep( - "Omarchy Repositories", - INIT_STEP_TIMEOUT_SECONDS.omarchyRepos, - syncOmarchyRepos({ - branch: options.branch, - }), - ); yield* requiredInitStep( "Omarchy Host Links", INIT_STEP_TIMEOUT_SECONDS.hostLinks, @@ -826,7 +878,12 @@ export function init(rawArgs: readonly string[]) { yield* requiredInitStep( "Enable Doctor Startup Timer", INIT_STEP_TIMEOUT_SECONDS.doctorTimer, - enableDoctorStartupTimer(), + enableUserUnit(DOCTOR_STARTUP_TIMER_UNIT, "Enable Doctor Startup Timer"), + ); + yield* requiredInitStep( + "Enable Resume Monitor", + INIT_STEP_TIMEOUT_SECONDS.doctorTimer, + enableUserUnit(RESUME_MONITOR_SERVICE_UNIT, "Enable Resume Monitor"), ); yield* requiredInitStep( "Sync Agents", diff --git a/dot/src/commands/Install.ts b/dot/src/commands/Install.ts index 254a976f..bddf4698 100644 --- a/dot/src/commands/Install.ts +++ b/dot/src/commands/Install.ts @@ -1,4 +1,5 @@ import { Effect } from "effect"; +import { existsSync } from "fs"; import { join } from "path"; import { Config } from "../services/Config.js"; import { OutputLog } from "../services/OutputLog.js"; @@ -10,6 +11,7 @@ import { ensureHyprHostLink, } from "../lib/omarchyHost.js"; import { ensureStowInstalled } from "../lib/packageSetup.js"; +import { applyOmarchyShellConfig } from "../lib/omarchyShellConfig.js"; import { backupConflictingPublicTargets, backupFileIfUnmanaged, @@ -18,6 +20,8 @@ import { formatBackupMove, findExternalSkillSymlinks, removeExternalSymlinks, + removeRetiredPublicStowLinks, + removeLegacyUwsmRepo, restoreExternalSymlinks, type BackupMove, type ExternalSymlink, @@ -46,6 +50,9 @@ export const install = Effect.gen(function* () { yield* ensureStowInstalled; yield* log.section("Backup"); + for (const path of removeRetiredPublicStowLinks(config.publicDotfiles)) { + yield* log.info(`Removed retired stow link: ${displayPath(path)}`); + } const legacyGhosttyMove = yield* Effect.sync(() => backupLegacyGhosttyRepo(config.publicDotfiles), ); @@ -54,6 +61,12 @@ export const install = Effect.gen(function* () { `Backed up retired Ghostty repo: ${formatBackupMove(legacyGhosttyMove)}`, ); } + const removedLegacyUwsm = yield* Effect.sync(() => removeLegacyUwsmRepo()); + if (removedLegacyUwsm) { + yield* log.info( + `Removed retired UWSM repo: ${displayPath(removedLegacyUwsm)}`, + ); + } const knownMoves = yield* Effect.sync(() => backupPublicFiles(config.publicDotfiles), ); @@ -93,8 +106,35 @@ export const install = Effect.gen(function* () { log, ); - yield* log.section("Omarchy Host Links"); - yield* ensureHyprHostLink(config, log); + const resumeMonitorUnit = "dot-on-resume-monitor.service"; + const resumeMonitorPath = join( + config.omarchy.repoBase, + "systemd", + "user", + resumeMonitorUnit, + ); + if (config.omarchy.enabled && existsSync(resumeMonitorPath)) { + yield* log.section("Resume Monitor"); + const daemonReloadExit = yield* launcher.stream( + "systemctl --user daemon-reload", + ); + const enableExit = + daemonReloadExit === 0 + ? yield* launcher.stream( + `systemctl --user enable --now ${resumeMonitorUnit}`, + ) + : daemonReloadExit; + if (enableExit === 0) { + yield* log.info(`Enabled ${resumeMonitorUnit}`); + } else { + yield* log.warn( + `Could not enable ${resumeMonitorUnit} (systemctl exit ${enableExit})`, + ); + } + } + + yield* log.section("Omarchy Shell Config"); + yield* applyOmarchyShellConfig; if (config.canUsePrivate && config.privateDotfiles) { const privateDotfiles = config.privateDotfiles; @@ -240,6 +280,8 @@ const stowRepo = ( if (isHypr) { yield* ensureHyprConfigLink(repoDir, log); } else { + // Unstow first (clean slate). Hyprland is excluded because removing + // hyprland.lua even briefly makes its live reload enter emergency mode. const unstowExit = yield* launcher.stream(`stow -D ${folder}`, { cwd: repoDir, }); @@ -269,6 +311,11 @@ const stowRepo = ( removeExternalSymlinks(externalLinks); } } + // Keep ~/.config/hypr a real directory so the runtime host symlink and + // Hyprland runtime state live in the live tree, not the stow source. + if (folder === "hypr") { + flags.push("--no-folding"); + } // Install mode uses --adopt for public scope if (scope === "public") { @@ -289,5 +336,14 @@ const stowRepo = ( exitCode: exit, }); } + + // Apply any added or changed config and clear any prior emergency state. + // Ignore failure: Hyprland may not be running (fresh install, headless). + if (isHypr) { + yield* ensureHyprHostLink(config, log); + yield* launcher + .stream("hyprctl reload", { cwd: repoDir }) + .pipe(Effect.catch(() => Effect.void)); + } } }); diff --git a/dot/src/commands/Stow.ts b/dot/src/commands/Stow.ts index 74220af0..2e72da63 100644 --- a/dot/src/commands/Stow.ts +++ b/dot/src/commands/Stow.ts @@ -15,6 +15,7 @@ import { ensureHyprHostLink, } from "../lib/omarchyHost.js"; import { ensureNvimThemeLink } from "../lib/omarchyNvim.js"; +import { applyOmarchyShellConfig } from "../lib/omarchyShellConfig.js"; import { writeRepoPicker, writeRepoShortcuts } from "../lib/repoShortcuts.js"; import { backupUnmanagedStowTargets, @@ -24,6 +25,8 @@ import { removeExternalSymlinks, removeStowedSkillOwner, removeStaleSkillSymlinks, + removeRetiredPublicStowLinks, + removeLegacyUwsmRepo, restoreExternalSymlinks, type ExternalSymlink, } from "../lib/stowConflicts.js"; @@ -42,6 +45,10 @@ const AGENTS_PRIVATE_IGNORES = [ * * Matches legacy behaviour: enumerates stow package directories, logs each one, * and applies per-folder stow with appropriate flags. + * + * @returns `true` when the generated Omarchy `shell.json` changed during this + * run, so the caller can reload the running shell. Always `false` for + * private-only runs or when the shell config step is skipped. */ export const stow = (opts?: { readonly publicOnly?: boolean; @@ -55,6 +62,8 @@ export const stow = (opts?: { const runPublic = !opts?.privateOnly; const runPrivate = !opts?.publicOnly; + let shellConfigChanged = false; + if (runPrivate && config.canUsePrivate && config.gitConfig.valid) { const shortcutsPath = yield* Effect.sync(() => writeRepoShortcuts(config.cacheDir, [ @@ -82,6 +91,9 @@ export const stow = (opts?: { if (runPublic) { yield* log.section("Stow Public Dotfiles"); + for (const path of removeRetiredPublicStowLinks(config.publicDotfiles)) { + yield* log.info(`Removed retired stow link: ${displayPath(path)}`); + } const legacyGhosttyMove = yield* Effect.sync(() => backupLegacyGhosttyRepo(config.publicDotfiles), ); @@ -90,6 +102,14 @@ export const stow = (opts?: { `[public] backed up retired Ghostty repo: ${formatBackupMove(legacyGhosttyMove)}`, ); } + const removedLegacyUwsm = yield* Effect.sync(() => + removeLegacyUwsmRepo(), + ); + if (removedLegacyUwsm) { + yield* log.info( + `[public] removed retired UWSM repo: ${displayPath(removedLegacyUwsm)}`, + ); + } const ignoredTargets = new Set([ join(".agents", "skills", "dotfiles-stow", "SKILL.md"), ]); @@ -115,11 +135,38 @@ export const stow = (opts?: { } yield* stowRepo(config.publicDotfiles, "public", launcher, log, config); - yield* log.section("Omarchy Host Links"); - yield* ensureHyprHostLink(config, log); + const resumeMonitorUnit = "dot-on-resume-monitor.service"; + const resumeMonitorPath = join( + config.omarchy.repoBase, + "systemd", + "user", + resumeMonitorUnit, + ); + if (config.omarchy.enabled && existsSync(resumeMonitorPath)) { + yield* log.section("Resume Monitor"); + const daemonReloadExit = yield* launcher.stream( + "systemctl --user daemon-reload", + ); + const enableExit = + daemonReloadExit === 0 + ? yield* launcher.stream( + `systemctl --user enable --now ${resumeMonitorUnit}`, + ) + : daemonReloadExit; + if (enableExit === 0) { + yield* log.info(`Enabled ${resumeMonitorUnit}`); + } else { + yield* log.warn( + `Could not enable ${resumeMonitorUnit} (systemctl exit ${enableExit})`, + ); + } + } yield* log.section("Omarchy Neovim Theme"); yield* ensureNvimThemeLink(log); + + yield* log.section("Omarchy Shell Config"); + shellConfigChanged = yield* applyOmarchyShellConfig; } if (runPrivate) { @@ -141,6 +188,8 @@ export const stow = (opts?: { ); } } + + return shellConfigChanged; }); /** Stow all folders in a single repo */ @@ -178,10 +227,11 @@ const stowRepo = ( const isHypr = folder === "hypr"; if (isHypr) { - // Never unstow hypr: Hyprland autoreload regenerates a default stub the - // instant hyprland.conf goes missing, and that stub real file then - // blocks the restow. Repair the link atomically instead and let the - // idempotent stow below fill in any missing files with no gap. + // Never unstow hypr: Hyprland watches its live config and auto-reloads + // on change. Removing the symlinks (even briefly) drops Hyprland into + // emergency mode, and it may regenerate a stub real config file + // that then blocks the restow. Repair the link atomically instead and + // let the idempotent stow below fill in any missing files with no gap. yield* ensureHyprConfigLink(repoDir, log); } else { // Unstow first, then restow (equivalent to --restow per folder) @@ -246,6 +296,7 @@ const stowRepo = ( // Apply any added or changed config and clear any prior emergency state. // Ignore failure: Hyprland may not be running (headless, SSH). if (isHypr) { + yield* ensureHyprHostLink(config, log); yield* launcher .stream("hyprctl reload", { cwd: repoDir }) .pipe(Effect.catch(() => Effect.void)); diff --git a/dot/src/commands/Update.ts b/dot/src/commands/Update.ts index a4612af2..419b98bb 100644 --- a/dot/src/commands/Update.ts +++ b/dot/src/commands/Update.ts @@ -397,6 +397,7 @@ const restoreHerdrPlugins = Effect.gen(function* () { "plugin", "install", "natori-hrj/herdr-lazy", + "--yes", ]); if (installExitCode !== 0) { return yield* new UpdateError({ @@ -494,6 +495,47 @@ const runResumeRefresh = Effect.gen(function* () { yield* log.info("On-resume helper started"); }); +/** + * Reload the running Omarchy shell after its generated `shell.json` changed. + * + * Restarts the shell so it reloads the generated layout and discovers any new + * plugins. Runs `omarchy` via the dispatcher and forces `QT_QPA_PLATFORM=wayland` on the + * restart so the relaunched shell attaches its layer-shell bar even when + * `dot update` is triggered from an environment that defaults to `xcb` (an SSH + * session, a systemd unit, or an agent shell); launching the shell under XCB + * silently drops the bar. `omarchy restart shell` refuses while the session is + * locked, which is treated as a non-fatal skip. No-op when Omarchy is disabled. + */ +const reloadOmarchyShell = Effect.gen(function* () { + const config = yield* Config; + const log = yield* OutputLog; + const executor = yield* CommandExecutor; + + if (!config.omarchy.enabled) return; + + yield* log.section("Reload Shell"); + + const exitCode = yield* executor.exitCode("omarchy", ["restart", "shell"], { + env: { QT_QPA_PLATFORM: "wayland" }, + }); + + if (exitCode !== 0) { + yield* log.warn( + `Shell reload skipped or failed (exit ${exitCode}; session may be locked)`, + ); + return; + } + + yield* log.info("Reloaded Omarchy shell (shell.json changed)"); +}); + +/** Reload the Omarchy shell only when stow rewrote its generated config. */ +export function reloadOmarchyShellIfChanged( + shellConfigChanged: boolean, +): Effect.Effect { + return shellConfigChanged ? reloadOmarchyShell : Effect.void; +} + /** Exit code from `dot update --check` when in-scope updates are available. */ export const UPDATE_CHECK_AVAILABLE_EXIT = 10; @@ -768,6 +810,7 @@ export const update = (opts?: UpdateOptions) => ); } + let shellConfigChanged = false; if (doStow) { yield* requiredUpdateStep( "Stow", @@ -783,11 +826,13 @@ export const update = (opts?: UpdateOptions) => yield* mcpSync; - yield* runStow(); + shellConfigChanged = yield* runStow(); }), ); } + yield* reloadOmarchyShellIfChanged(shellConfigChanged); + if (doApp) { yield* requiredUpdateStep( "Rebuild", diff --git a/dot/src/doctor/checks/omarchy.ts b/dot/src/doctor/checks/omarchy.ts index 1778349c..0b7cfcc3 100644 --- a/dot/src/doctor/checks/omarchy.ts +++ b/dot/src/doctor/checks/omarchy.ts @@ -16,8 +16,6 @@ import type { ConfigService } from "../../services/Config.js"; /** Map repo name to expected GitHub org/repo slug (matches legacy omarchy_repo_slug) */ function omarchyRepoSlug(repoName: string): string | null { switch (repoName) { - case "waybar": - return "timmo001/omarchy-waybar"; case "uwsm": return "timmo001/omarchy-uwsm"; default: diff --git a/dot/src/doctor/checks/opencodeServer.ts b/dot/src/doctor/checks/opencodeServer.ts index b48291c7..c7a769b5 100644 --- a/dot/src/doctor/checks/opencodeServer.ts +++ b/dot/src/doctor/checks/opencodeServer.ts @@ -72,7 +72,9 @@ export function opencodeServerResults( existsSync(autostartPath) && readFileSync(autostartPath, "utf8") .split("\n") - .some((line) => /^\s*exec-once\s*=.*\bopencode-server\b/.test(line)); + .some((line) => + /^\s*o\.exec_on_start\(["']opencode-server["']\)/.test(line), + ); if (startsServer) { return [ @@ -97,7 +99,7 @@ export function opencodeServerResults( export const checkOpencodeServer = Effect.gen(function* () { const config = yield* Config; return opencodeServerResults( - join(hyprRepoPath(config), "hosts", "desktop", "autostart.conf"), + join(hyprRepoPath(config), "hosts", "desktop", "autostart.lua"), OPENCODE_ENV_PATH, ); }); diff --git a/dot/src/doctor/checks/systemd.ts b/dot/src/doctor/checks/systemd.ts index 94082a58..bf297c34 100644 --- a/dot/src/doctor/checks/systemd.ts +++ b/dot/src/doctor/checks/systemd.ts @@ -1,6 +1,6 @@ import { Effect } from "effect"; import { accessSync, constants, existsSync, lstatSync, readFileSync } from "fs"; -import { join, dirname, resolve } from "path"; +import { join, resolve } from "path"; import { CommandExecutor, type CommandExecutorService, @@ -9,7 +9,6 @@ import { Config } from "../../services/Config.js"; import type { ConfigService } from "../../services/Config.js"; import { GitHub } from "../../git/services/GitHub.js"; import { CONFIG_DIR, HOME_DIR, displayPath } from "../../lib/paths.js"; -import { ENV, envString } from "../../lib/env.js"; import { resolvedOmarchyHost } from "../../lib/omarchyHost.js"; import type { CheckResult } from "../types.js"; @@ -19,7 +18,23 @@ const LEGACY_WORKFLOW_WATCH_TIMER_UNIT = "git-workflow-watch.timer"; const DOCTOR_STARTUP_TIMER_UNIT = "dot-doctor-startup.timer"; const DAILY_VOLUME_ZERO_TIMER_UNIT = "daily-volume-zero.timer"; const LOCAL_BIN_DIR = join(HOME_DIR, ".local", "bin"); -const UWSM_ENV_FILE = join(CONFIG_DIR, "uwsm", "env"); +const RESUME_MONITOR_SERVICE_UNIT = "dot-on-resume-monitor.service"; +const DOCTOR_STARTUP_NOTIFY_SCRIPT = join( + HOME_DIR, + ".local", + "bin", + "dot-doctor-notify", +); +const RESUME_MONITOR_SCRIPT = join( + HOME_DIR, + ".local", + "bin", + "on-resume-monitor", +); + +function userSystemdUnitPath(unit: string): string { + return join(CONFIG_DIR, "systemd", "user", unit); +} function pathExistsOrSymlink(path: string): boolean { try { @@ -66,6 +81,131 @@ function addObsoletePathCheck( } } +function addExecutablePresenceCheck( + results: CheckResult[], + path: string, + okMessage: string, + warnMessage: string, + detail?: string, +): void { + results.push( + executableExists(path) + ? { severity: "ok", message: okMessage } + : { + severity: "warn", + message: warnMessage, + ...(detail && { detail }), + }, + ); +} + +function addFilePresenceCheck( + results: CheckResult[], + path: string, + okMessage: string, + warnMessage: string, + detail: string, +): void { + results.push( + existsSync(path) + ? { severity: "ok", message: okMessage } + : { severity: "warn", message: warnMessage, detail }, + ); +} + +const checkRequiredUserUnit = ( + results: CheckResult[], + executor: CommandExecutorService, + unit: string, + label: string, + enableDetail: string, +) => + Effect.gen(function* () { + const hasSystemctl = + (yield* executor.exitCode("which", ["systemctl"])) === 0; + if (!hasSystemctl) { + results.push({ + severity: "warn", + message: `Skipping ${label.toLowerCase()} checks (systemctl not found)`, + }); + return; + } + + const enabled = yield* executor.exitCode("systemctl", [ + "--user", + "is-enabled", + unit, + ]); + if (enabled === 0) { + results.push({ severity: "ok", message: `${label} enabled: ${unit}` }); + } else { + results.push({ + severity: "warn", + message: `${label} is disabled: ${unit}`, + detail: enableDetail, + }); + } + + const active = yield* executor.exitCode("systemctl", [ + "--user", + "is-active", + unit, + ]); + if (active === 0) { + results.push({ severity: "ok", message: `${label} active: ${unit}` }); + } else { + results.push({ + severity: "warn", + message: `${label} is not active: ${unit}`, + detail: enableDetail, + }); + } + }); + +interface RequiredUserUnitSetup { + readonly scriptPath: string; + readonly scriptOkMessage: string; + readonly scriptWarnMessage: string; + readonly scriptDetail?: string; + readonly unitPath: string; + readonly unitOkMessage: string; + readonly unitWarnMessage: string; + readonly unitDetail: string; + readonly unit: string; + readonly unitLabel: string; +} + +const checkRequiredUserUnitSetup = (setup: RequiredUserUnitSetup) => + Effect.gen(function* () { + const executor = yield* CommandExecutor; + const results: CheckResult[] = []; + const enableDetail = `Enable with: systemctl --user enable --now ${setup.unit}`; + + addExecutablePresenceCheck( + results, + setup.scriptPath, + setup.scriptOkMessage, + setup.scriptWarnMessage, + setup.scriptDetail, + ); + addFilePresenceCheck( + results, + setup.unitPath, + setup.unitOkMessage, + setup.unitWarnMessage, + setup.unitDetail, + ); + yield* checkRequiredUserUnit( + results, + executor, + setup.unit, + setup.unitLabel, + enableDetail, + ); + + return results; + }); + const checkObsoleteUserUnit = ( results: CheckResult[], executor: CommandExecutorService, @@ -110,129 +250,6 @@ const checkObsoleteUserUnit = ( } }); -// --------------------------------------------------------------------------- -// Waybar config walk helpers (matches legacy _waybar_config_walk pattern) -// --------------------------------------------------------------------------- - -/** Walk a Waybar config and its includes, returning true if any file contains the needle */ -function waybarConfigWalkContains(configPath: string, needle: string): boolean { - if (!existsSync(configPath)) return false; - try { - const content = readFileSync(configPath, "utf-8"); - if (content.includes(needle)) return true; - // Check includes - for (const includePath of parseWaybarIncludes(configPath, content)) { - if (waybarConfigWalkContains(includePath, needle)) return true; - } - } catch { - /* ignore */ - } - return false; -} - -/** Parse "include" array entries from a Waybar JSONC config file */ -function parseWaybarIncludes( - configPath: string, - content: string, -): readonly string[] { - const match = content.match(/"include"\s*:\s*\[([^\]]*)\]/); - if (!match) return []; - const configDir = dirname(configPath); - return match[1] - .split(",") - .map((e) => e.trim().replace(/^"|"$/g, "")) - .filter(Boolean) - .map((e) => { - const expanded = e.replace(/^~/, HOME_DIR); - return expanded.startsWith("/") ? expanded : join(configDir, expanded); - }); -} - -function activeWaybarConfigPath(config: ConfigService): string { - const omarchyHost = resolvedOmarchyHost(config) ?? ""; - const waybarConfigDir = join(CONFIG_DIR, "waybar"); - const hostConfig = omarchyHost - ? join(waybarConfigDir, `config.${omarchyHost}.jsonc`) - : ""; - return hostConfig && existsSync(hostConfig) - ? hostConfig - : join(waybarConfigDir, "config.jsonc"); -} - -function addLocalScriptCheck( - results: CheckResult[], - scriptName: string, - label: string, - missingDetail: string, -): void { - const script = join(HOME_DIR, ".local", "bin", scriptName); - results.push( - executableExists(script) - ? { - severity: "ok", - message: `${label} script is executable: ${displayPath(script)}`, - } - : { - severity: "warn", - message: `${label} script is missing or not executable: ${displayPath(script)}`, - detail: missingDetail, - }, - ); -} - -function addWaybarHiddenCssCheck( - results: CheckResult[], - selector: string, - label: string, - missingDetail: string, -): void { - const waybarStyle = join(CONFIG_DIR, "waybar", "style.css"); - if (!existsSync(waybarStyle)) { - results.push({ - severity: "warn", - message: `Waybar style file is missing: ${displayPath(waybarStyle)}`, - }); - return; - } - - try { - const styleContent = readFileSync(waybarStyle, "utf-8"); - results.push( - styleContent.includes(selector) - ? { - severity: "ok", - message: `${label} Waybar hidden-empty CSS found: ${displayPath(waybarStyle)}`, - } - : { - severity: "warn", - message: `${label} Waybar hidden-empty CSS is missing: ${displayPath(waybarStyle)}`, - detail: missingDetail, - }, - ); - } catch { - /* ignore */ - } -} - -function addWaybarConfigContainsCheck( - results: CheckResult[], - waybarConfig: string, - needle: string, - okMessage: string, - warnMessage: string, - detail?: string, -): void { - if (waybarConfigWalkContains(waybarConfig, needle)) { - results.push({ severity: "ok", message: okMessage }); - } else { - results.push({ - severity: "warn", - message: warnMessage, - ...(detail && { detail }), - }); - } -} - /** Check workflow runs integration and absence of the legacy notification watcher. */ export const checkWorkflowRuns = Effect.gen(function* () { const executor = yield* CommandExecutor; @@ -348,55 +365,10 @@ export const checkWorkflowRuns = Effect.gen(function* () { }); } - addLocalScriptCheck( - results, - "git-workflows-bar", - "Workflow runs", - "Run dot stow to install the workflow runs module script", - ); - addWaybarHiddenCssCheck( - results, - "#custom-git-workflows.hidden", - "Workflow runs", - "Update the Waybar style so the workflow icon hides when there are no recent runs needing attention", - ); - - const waybarConfig = activeWaybarConfigPath(config); - - if (existsSync(waybarConfig)) { - results.push({ - severity: "ok", - message: `Workflow runs active Waybar config: ${displayPath(waybarConfig)}`, - }); - - // Walk config and includes to check for module, click actions, and ordering - const configContains = (needle: string): boolean => - waybarConfigWalkContains(waybarConfig, needle); - - if (configContains("git-workflow-watch")) { - results.push({ - severity: "error", - message: `Active Waybar config still references obsolete git-workflow-watch: ${displayPath(waybarConfig)}`, - detail: - "Update/re-stow the Waybar config on this machine, or remove the legacy git-workflow-watch module/action references from the active Waybar config", - }); - } else { - results.push({ - severity: "ok", - message: "Active Waybar config has no legacy workflow-watch actions", - }); - } - } else { - results.push({ - severity: "warn", - message: `Active Waybar config is missing: ${displayPath(waybarConfig)}`, - }); - } - return results; }); -/** Check GitHub notifications API access and Waybar integration. */ +/** Check GitHub notifications API access. */ export const checkGitNotifications = Effect.gen(function* () { const github = yield* GitHub; const config = yield* Config; @@ -430,141 +402,35 @@ export const checkGitNotifications = Effect.gen(function* () { } } - addLocalScriptCheck( - results, - "git-notifications-bar", - "Git notifications", - "Run dot stow to install the Git notifications module script", - ); - addWaybarHiddenCssCheck( - results, - "#custom-git-notifications.hidden", - "Git notifications", - "Update the Waybar style so the notification icon hides when the inbox is clear", - ); - - const waybarConfig = activeWaybarConfigPath(config); - - if (existsSync(waybarConfig)) { - results.push({ - severity: "ok", - message: `Git notifications active Waybar config: ${displayPath(waybarConfig)}`, - }); - - addWaybarConfigContainsCheck( - results, - waybarConfig, - '"custom/git-notifications"', - "Git notifications Waybar module is present in the active config", - `Git notifications Waybar module is missing from ${displayPath(waybarConfig)}`, - "Add custom/git-notifications before custom/git-diff in the active Waybar config", - ); - addWaybarConfigContainsCheck( - results, - waybarConfig, - '"on-click": "git-notifications-bar open"', - "Git notifications Waybar left click opens the filtered TUI", - `Git notifications Waybar left-click action is missing in ${displayPath(waybarConfig)}`, - ); - addWaybarConfigContainsCheck( - results, - waybarConfig, - '"on-click-right": "git-notifications-bar refresh"', - "Git notifications Waybar right click refreshes the cache", - `Git notifications Waybar right-click refresh action is missing in ${displayPath(waybarConfig)}`, - ); - } else { - results.push({ - severity: "warn", - message: `Active Waybar config is missing: ${displayPath(waybarConfig)}`, - }); - } - return results; }); /** Check doctor startup notification timer */ -export const checkDoctorStartup = Effect.gen(function* () { - const executor = yield* CommandExecutor; - const results: CheckResult[] = []; - - const notifyScript = join(HOME_DIR, ".local", "bin", "dot-doctor-notify"); - const unitPath = join( - CONFIG_DIR, - "systemd", - "user", - DOCTOR_STARTUP_TIMER_UNIT, - ); - - if (existsSync(notifyScript)) { - results.push({ - severity: "ok", - message: `Doctor startup notify script found: ${displayPath(notifyScript)}`, - }); - } else { - results.push({ - severity: "warn", - message: `Doctor startup notify script missing or not executable: ${displayPath(notifyScript)}`, - }); - } - - if (existsSync(unitPath)) { - results.push({ - severity: "ok", - message: `Doctor startup timer unit file found: ${displayPath(unitPath)}`, - }); - } else { - results.push({ - severity: "warn", - message: `Doctor startup timer unit file missing: ${displayPath(unitPath)}`, - detail: "Run dot stow (or dot install) to link systemd user units", - }); - } - - const hasSystemctl = (yield* executor.exitCode("which", ["systemctl"])) === 0; - if (hasSystemctl) { - const enabled = yield* executor.exitCode("systemctl", [ - "--user", - "is-enabled", - DOCTOR_STARTUP_TIMER_UNIT, - ]); - if (enabled === 0) { - results.push({ - severity: "ok", - message: `Doctor startup timer enabled: ${DOCTOR_STARTUP_TIMER_UNIT}`, - }); - } else { - results.push({ - severity: "warn", - message: `Doctor startup timer is disabled: ${DOCTOR_STARTUP_TIMER_UNIT}`, - detail: `Enable with: systemctl --user enable --now ${DOCTOR_STARTUP_TIMER_UNIT}`, - }); - } - - const active = yield* executor.exitCode("systemctl", [ - "--user", - "is-active", - DOCTOR_STARTUP_TIMER_UNIT, - ]); - if (active === 0) { - results.push({ - severity: "ok", - message: `Doctor startup timer active: ${DOCTOR_STARTUP_TIMER_UNIT}`, - }); - } else { - results.push({ - severity: "warn", - message: `Doctor startup timer is not active: ${DOCTOR_STARTUP_TIMER_UNIT}`, - }); - } - } else { - results.push({ - severity: "warn", - message: "Skipping doctor startup timer checks (systemctl not found)", - }); - } +export const checkDoctorStartup = checkRequiredUserUnitSetup({ + scriptPath: DOCTOR_STARTUP_NOTIFY_SCRIPT, + scriptOkMessage: `Doctor startup notify script found: ${displayPath(DOCTOR_STARTUP_NOTIFY_SCRIPT)}`, + scriptWarnMessage: `Doctor startup notify script missing or not executable: ${displayPath(DOCTOR_STARTUP_NOTIFY_SCRIPT)}`, + unitPath: userSystemdUnitPath(DOCTOR_STARTUP_TIMER_UNIT), + unitOkMessage: `Doctor startup timer unit file found: ${displayPath(userSystemdUnitPath(DOCTOR_STARTUP_TIMER_UNIT))}`, + unitWarnMessage: `Doctor startup timer unit file missing: ${displayPath(userSystemdUnitPath(DOCTOR_STARTUP_TIMER_UNIT))}`, + unitDetail: "Run dot stow (or dot install) to link systemd user units", + unit: DOCTOR_STARTUP_TIMER_UNIT, + unitLabel: "Doctor startup timer", +}); - return results; +/** Check resume recovery monitor service used after hypridle is removed. */ +export const checkResumeMonitor = checkRequiredUserUnitSetup({ + scriptPath: RESUME_MONITOR_SCRIPT, + scriptOkMessage: `Resume monitor script is executable: ${displayPath(RESUME_MONITOR_SCRIPT)}`, + scriptWarnMessage: `Resume monitor script is missing or not executable: ${displayPath(RESUME_MONITOR_SCRIPT)}`, + scriptDetail: + "Run dot stow (or dot install) to link the resume monitor script", + unitPath: userSystemdUnitPath(RESUME_MONITOR_SERVICE_UNIT), + unitOkMessage: `Resume monitor service unit file found: ${displayPath(userSystemdUnitPath(RESUME_MONITOR_SERVICE_UNIT))}`, + unitWarnMessage: `Resume monitor service unit file missing: ${displayPath(userSystemdUnitPath(RESUME_MONITOR_SERVICE_UNIT))}`, + unitDetail: "Run dot stow (or dot install) to link systemd user units", + unit: RESUME_MONITOR_SERVICE_UNIT, + unitLabel: "Resume monitor service", }); /** Check daily volume reset timer (laptop-only, informational) */ @@ -575,9 +441,8 @@ export const checkDailyVolumeReset = Effect.gen(function* () { const host = resolvedOmarchyHost(config) ?? "unset"; const script = join(HOME_DIR, ".local", "bin", "daily-volume-zero"); - const systemdDir = join(CONFIG_DIR, "systemd", "user"); - const serviceUnit = join(systemdDir, "daily-volume-zero.service"); - const timerUnit = join(systemdDir, DAILY_VOLUME_ZERO_TIMER_UNIT); + const serviceUnit = userSystemdUnitPath("daily-volume-zero.service"); + const timerUnit = userSystemdUnitPath(DAILY_VOLUME_ZERO_TIMER_UNIT); if (existsSync(script)) { results.push({ @@ -690,44 +555,19 @@ function userEnvironmentPathEntries( * Check that ~/.local/bin is on the uwsm/systemd user-environment PATH. * * `uwsm app` resolves binaries against the systemd user-environment PATH, which - * is seeded by ~/.config/uwsm/env (not the login shell). Stowed ~/.local/bin - * shims only resolve under `uwsm app` when that PATH includes ~/.local/bin. + * is seeded by Omarchy's package-owned UWSM bootstrap (not the login shell). + * Stowed ~/.local/bin shims only resolve when that PATH includes ~/.local/bin. */ export const checkLocalBinPath = Effect.gen(function* () { const executor = yield* CommandExecutor; - const results: CheckResult[] = []; - - // Durable source: the uwsm env file should add ~/.local/bin to the session PATH. - if (existsSync(UWSM_ENV_FILE)) { - const envContent = readFileSync(UWSM_ENV_FILE, "utf-8"); - if (/(\$HOME|~)\/\.local\/bin/.test(envContent)) { - results.push({ - severity: "ok", - message: `uwsm env adds ~/.local/bin to PATH: ${displayPath(UWSM_ENV_FILE)}`, - }); - } else { - results.push({ - severity: "warn", - message: `uwsm env does not add ~/.local/bin to PATH: ${displayPath(UWSM_ENV_FILE)}`, - detail: - "Add 'export PATH=$HOME/.local/bin:$PATH' to the omarchy-uwsm fork env so uwsm app resolves stowed ~/.local/bin shims", - }); - } - } else { - results.push({ - severity: "warn", - message: `uwsm env file missing: ${displayPath(UWSM_ENV_FILE)}`, - }); - } - - // Live session PATH that uwsm app resolves against. const hasSystemctl = (yield* executor.exitCode("which", ["systemctl"])) === 0; if (!hasSystemctl) { - results.push({ - severity: "warn", - message: "Skipping uwsm session PATH check (systemctl not found)", - }); - return results; + return [ + { + severity: "warn", + message: "Skipping uwsm session PATH check (systemctl not found)", + }, + ] satisfies CheckResult[]; } const showEnvironment = yield* executor @@ -738,18 +578,20 @@ export const checkLocalBinPath = Effect.gen(function* () { ); if (onPath) { - results.push({ - severity: "ok", - message: `~/.local/bin is on the uwsm session PATH: ${displayPath(LOCAL_BIN_DIR)}`, - }); - } else { - results.push({ + return [ + { + severity: "ok", + message: `~/.local/bin is on the uwsm session PATH: ${displayPath(LOCAL_BIN_DIR)}`, + }, + ] satisfies CheckResult[]; + } + + return [ + { severity: "warn", message: "~/.local/bin is not on the uwsm session PATH", detail: - "Relaunch Hyprland after adding ~/.local/bin to the uwsm env; uwsm app cannot resolve stowed ~/.local/bin shims without it", - }); - } - - return results; + "Relaunch Hyprland so the package-owned Omarchy UWSM bootstrap refreshes the session environment", + }, + ] satisfies CheckResult[]; }); diff --git a/dot/src/doctor/runner.ts b/dot/src/doctor/runner.ts index c87ce601..53b8b4a5 100644 --- a/dot/src/doctor/runner.ts +++ b/dot/src/doctor/runner.ts @@ -19,6 +19,7 @@ import { checkGitNotifications, checkWorkflowRuns, checkDoctorStartup, + checkResumeMonitor, checkDailyVolumeReset, checkLocalBinPath, } from "./checks/systemd.js"; @@ -82,8 +83,9 @@ const sections: readonly SectionDef[] = [ { name: "Git notification checks", check: checkGitNotifications }, { name: "Doctor startup notification", check: checkDoctorStartup }, { name: "uwsm session PATH", check: checkLocalBinPath }, + { name: "Resume recovery monitor", check: checkResumeMonitor }, { name: "Daily volume reset", check: checkDailyVolumeReset }, - { name: "Omarchy repository checks", check: checkOmarchy }, + { name: "Omarchy config checks", check: checkOmarchy }, { name: "Legacy Hypr repo check", check: checkLegacyHyprRepo }, { name: "Neovim theme link", check: checkNvimThemeLink }, { name: "Private access", check: checkPrivateAccess }, diff --git a/dot/src/git/commands/Diff.ts b/dot/src/git/commands/Diff.ts index 78885363..8821cb5b 100644 --- a/dot/src/git/commands/Diff.ts +++ b/dot/src/git/commands/Diff.ts @@ -58,7 +58,9 @@ export const diffBarJson = (opts?: DiffScanOptions) => }, )).filter((repo): repo is DiffRepo => repo !== null); - const text = changed.length > 0 ? `\uF418 ${changed.length}` : ""; + // Always emit the icon and count so the widget shows "0" when everything + // is up to date, rather than collapsing to an empty (hidden) cell. + const text = `\uF418 ${changed.length}`; const tooltip = changed.length > 0 ? `Repositories with changes pending: ${changed.map((r) => r.name).join("; ")}` diff --git a/dot/src/git/commands/Notifications.ts b/dot/src/git/commands/Notifications.ts index 7830fb63..3e5e631b 100644 --- a/dot/src/git/commands/Notifications.ts +++ b/dot/src/git/commands/Notifications.ts @@ -137,7 +137,8 @@ function notificationBarText( summary: ReturnType, ): string { if (state.message) return "\uf071 ?"; - if (summary.unreadCount === 0) return ""; + // Always emit the count (including "0") so the bar widget has an icon to + // reveal dimmed on hover; the "hidden" class still collapses it when clear. return `\uf0f3 ${summary.unreadCount}`; } diff --git a/dot/src/git/services/GitDiffWaybarCache.ts b/dot/src/git/services/GitDiffWaybarCache.ts deleted file mode 100644 index 999f8b4a..00000000 --- a/dot/src/git/services/GitDiffWaybarCache.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Context, Effect, Layer, Schema } from "effect"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import { CACHE_DIR } from "../../lib/paths.js"; - -/** Shape of the JSON written by the Waybar `git-diff` module */ -interface GitDiffWaybarCacheData { - /** Primary display text (e.g. repo count) */ - readonly text: string; - /** Tooltip with repository names (e.g. "Repositories with changes pending: dotfiles; notes") */ - readonly tooltip: string; - /** CSS class indicating change state (e.g. "dots-changed", "dots-ok", "dots-unknown") */ - readonly class: string; -} - -/** Service interface for reading the Waybar git diff cache */ -interface GitDiffWaybarCacheService { - /** Load and parse the Waybar cache JSON, returning null if unavailable */ - readonly load: () => Effect.Effect; - /** Extract changed repository names from the tooltip string */ - readonly parseChangedNames: ( - data: GitDiffWaybarCacheData, - ) => readonly string[]; -} - -class GitDiffWaybarCacheError extends Schema.TaggedErrorClass()( - "GitDiffWaybarCacheError", - { - message: Schema.String, - }, -) {} - -/** Effect service for {@link GitDiffWaybarCacheService} */ -export class GitDiffWaybarCache extends Context.Service< - GitDiffWaybarCache, - GitDiffWaybarCacheService ->()("GitDiffWaybarCache") { - static readonly layer = Layer.succeed(GitDiffWaybarCache, { - load: () => - Effect.tryPromise({ - try: async () => { - const raw = await readFile(getCachePath(), "utf-8"); - const data = JSON.parse(raw) as GitDiffWaybarCacheData; - if (!data.tooltip || !data.class) return null; - return data; - }, - catch: (error) => - new GitDiffWaybarCacheError({ message: String(error) }), - }).pipe(Effect.catch(() => Effect.succeed(null))), - - parseChangedNames: (data: GitDiffWaybarCacheData): readonly string[] => { - // Tooltip format: "Repositories with changes pending: dotfiles; notes" - // or "Repositories with changes pending: dotfiles" - const match = data.tooltip.match(/:\s*(.+)$/); - if (!match) return []; - return match[1] - .split(/[;,]/) - .map((s) => s.trim()) - .filter(Boolean); - }, - }); -} - -function getCachePath(): string { - return join(CACHE_DIR, "waybar", "git-diff-waybar.json"); -} diff --git a/dot/src/git/services/RepoWatcher.ts b/dot/src/git/services/RepoWatcher.ts index b9f92d69..8a83b1d2 100644 --- a/dot/src/git/services/RepoWatcher.ts +++ b/dot/src/git/services/RepoWatcher.ts @@ -11,7 +11,6 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import type { Repo, RepoState } from "../../types.js"; import { DotDiff } from "./DotDiff.js"; -import { GitDiffWaybarCache } from "./GitDiffWaybarCache.js"; const log = (msg: string) => console.error(`[dot:Watcher] ${msg}`); @@ -35,7 +34,6 @@ export class RepoWatcher extends Context.Service< Effect.gen(function* () { log("Initialising RepoWatcher..."); const dotDiff = yield* DotDiff; - const waybarCache = yield* GitDiffWaybarCache; const pubsub = yield* PubSub.unbounded(); let currentState: RepoState = { @@ -67,45 +65,8 @@ export class RepoWatcher extends Context.Service< }), ); - // Fast startup: try Waybar cache first, then full poll - const initialLoad = Effect.gen(function* () { - log("Trying Waybar cache for fast start..."); - const cache = yield* waybarCache.load(); - - if (cache && cache.class !== "dots-unknown") { - const changedNames = waybarCache.parseChangedNames(cache); - log( - `Waybar cache hit: class=${cache.class}, changedNames=[${changedNames.join(", ")}]`, - ); - const all = yield* dotDiff - .listAll() - .pipe(Effect.catch(() => Effect.succeed([] as readonly Repo[]))); - - if (all.length > 0) { - const changedNameSet = new Set(changedNames); - const changed = all.filter((r) => { - const baseName = r.name.includes(":") - ? r.name.split(":").pop()! - : r.name; - return changedNameSet.has(baseName) || changedNameSet.has(r.name); - }); - const now = yield* Clock.currentTimeMillis; - const state = buildRepoState(all, changed, new Date(now)); - currentState = state; - yield* PubSub.publish(pubsub, state); - log( - `Fast start: ${changed.length} changed, ${state.unchanged.length} unchanged`, - ); - return; - } - } - - log("Waybar cache miss — falling back to full poll"); - yield* poll; - }).pipe(Effect.withSpan("RepoWatcher.initialLoad")); - - // Run initial load - yield* initialLoad; + // Fast first paint: run a full poll before the background fiber starts + yield* poll; log("Initial load complete"); // Start background poll fiber (10s interval, matching lazygit) diff --git a/dot/src/index.ts b/dot/src/index.ts index d965861f..bf39d979 100644 --- a/dot/src/index.ts +++ b/dot/src/index.ts @@ -584,7 +584,7 @@ if (mode.type === "native") { stow({ publicOnly: args.includes("--public"), privateOnly: args.includes("--private"), - }), + }).pipe(Effect.asVoid), doctor: (args) => doctor({ openOpencode: args.includes("--open-opencode"), @@ -674,8 +674,6 @@ if (mode.type === "native") { const { Renderer } = await import("./services/Renderer.js"); const { Toast } = await import("./services/Toast.js"); - const { GitDiffWaybarCache } = - await import("./git/services/GitDiffWaybarCache.js"); const { RepoWatcher } = await import("./git/services/RepoWatcher.js"); const { createCommandRunner } = await import("./services/CommandRunner.js"); const { loadTheme } = await import("./theme.js"); @@ -868,7 +866,6 @@ if (mode.type === "native") { Layer.provideMerge(DashboardLayer), Layer.provideMerge(GitNotifications.layer), Layer.provideMerge(GitHub.layer), - Layer.provideMerge(GitDiffWaybarCache.layer), Layer.provideMerge(Toast.layer(theme)), Layer.provideMerge(Renderer.layer(theme, nativeLibPath)), Layer.provideMerge(OutputLog.tuiLayer), diff --git a/dot/src/lib/env.ts b/dot/src/lib/env.ts index 4f54e7b2..de5ab8f4 100644 --- a/dot/src/lib/env.ts +++ b/dot/src/lib/env.ts @@ -15,14 +15,12 @@ export const ENV = { DOT_GITHUB_RATE_LIMIT_MIN_REMAINING: "DOT_GITHUB_RATE_LIMIT_MIN_REMAINING", DOT_GITHUB_RATE_LIMIT_TTL_SECONDS: "DOT_GITHUB_RATE_LIMIT_TTL_SECONDS", DOT_GITHUB_RETRIES: "DOT_GITHUB_RETRIES", - DOT_INCLUDE_OMARCHY_DIFF_REPOS: "DOT_INCLUDE_OMARCHY_DIFF_REPOS", DOT_INIT_LOG_FILE: "DOT_INIT_LOG_FILE", DOT_INIT_NONINTERACTIVE: "DOT_INIT_NONINTERACTIVE", DOT_LOG_FILE: "DOT_LOG_FILE", DOT_LOG_MIRROR_FILE: "DOT_LOG_MIRROR_FILE", DOT_MCP_CONFIG_FILE: "DOT_MCP_CONFIG_FILE", DOT_NOTES_DIR: "DOT_NOTES_DIR", - DOT_OMARCHY_BRANCH: "DOT_OMARCHY_BRANCH", DOT_PRIVATE_BROWSER_CHECKS_FILE: "DOT_PRIVATE_BROWSER_CHECKS_FILE", DOT_PRIVATE_PACKAGE_MAP_FILE: "DOT_PRIVATE_PACKAGE_MAP_FILE", DOT_PRIVATE_PACKAGE_REPO_FILE: "DOT_PRIVATE_PACKAGE_REPO_FILE", @@ -46,6 +44,7 @@ export const ENV = { NO_COLOR: "NO_COLOR", NOTES: "NOTES", OMARCHY_HOST: "OMARCHY_HOST", + OMARCHY_PATH: "OMARCHY_PATH", OMARCHY_REPO_BASE_DIR: "OMARCHY_REPO_BASE_DIR", OPENCODE: "OPENCODE", OPENCODE_APP_INFO: "OPENCODE_APP_INFO", diff --git a/dot/src/lib/omarchyHost.ts b/dot/src/lib/omarchyHost.ts index 8358b930..ad674ba3 100644 --- a/dot/src/lib/omarchyHost.ts +++ b/dot/src/lib/omarchyHost.ts @@ -223,7 +223,7 @@ const updateHyprHostLink = ( }); /** Path of the Hypr main config within both the hypr stow package and `~`. */ -const HYPR_CONFIG_REL = join(".config", "hypr", "hyprland.conf"); +const HYPR_CONFIG_REL = join(".config", "hypr", "hyprland.lua"); /** * Spell a packaged file's symlink the way GNU Stow does: relative to the stow @@ -244,7 +244,7 @@ function stowLinkContent( } /** - * Atomically ensure `~/.config/hypr/hyprland.conf` is the stow-owned symlink + * Atomically ensure `~/.config/hypr/hyprland.lua` is the stow-owned symlink * before the hypr package is stowed. * * Hyprland enables config autoreload by default and writes a default stub @@ -283,7 +283,7 @@ export const ensureHyprConfigLink = ( symlinkSync(linkContent, tmpLink); renameSync(tmpLink, linkPath); yield* log.info( - `Repaired Hypr config link (${displayPath(linkPath)} -> hyprland.conf)`, + `Repaired Hypr config link (${displayPath(linkPath)} -> hyprland.lua)`, ); }); diff --git a/dot/src/lib/omarchyShellConfig.ts b/dot/src/lib/omarchyShellConfig.ts new file mode 100644 index 00000000..b053adc3 --- /dev/null +++ b/dot/src/lib/omarchyShellConfig.ts @@ -0,0 +1,537 @@ +import { Effect } from "effect"; +import { + chmodSync, + existsSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "fs"; +import { join } from "path"; +import { Config } from "../services/Config.js"; +import { OutputLog } from "../services/OutputLog.js"; +import { CONFIG_DIR, HOME_DIR, displayPath } from "./paths.js"; +import { ENV, envString } from "./env.js"; +import { resolvedOmarchyHost } from "./omarchyHost.js"; + +/** A single bar layout entry: a plugin id plus inline per-instance settings. */ +interface BarEntry { + readonly id: string; + readonly [key: string]: unknown; +} + +/** The bar layout columns of an Omarchy `shell.json`. */ +interface ShellLayout { + left: BarEntry[]; + center: BarEntry[]; + right: BarEntry[]; +} + +/** The `bar` block of an Omarchy `shell.json` (unknown fields preserved). */ +interface ShellBar { + position?: string; + layout: ShellLayout; + [key: string]: unknown; +} + +/** + * The parts of Omarchy's `shell.json` this generator reads and mutates. All + * other fields (`version`, `centerAnchor`, clock formats, weather, future + * additions) are preserved verbatim via the index signatures. + */ +interface ShellConfig { + idle?: { screensaver: number; lock: number }; + bar: ShellBar; + [key: string]: unknown; +} + +/** + * Class-name to colour map ported from the legacy Waybar `style.css`. Used by + * the `timmo.command` / `timmo.stream-command` widgets to colour their output. + */ +const COLOR = { + purple: "#ac77e5", + teal: "#2bb3b1", + red: "#e06c75", + amber: "#e5c07b", + green: "#98c379", + blue: "#61afef", + grey: "#9b9b9b", + rust: "#a55555", + orange: "#e7ad63", + tan: "#c6a47a", + vocCritical: "#bf6a4e", + co2Critical: "#d56f69", + cream: "#fef6ea", +} as const; + +const HA = "http://homeassistant.local:8123"; + +/** Widget id of Omarchy's default workspaces bar entry (left column). */ +const WORKSPACES_ID = "omarchy.workspaces"; + +/** Widget id of Omarchy's default clock bar entry (default centre anchor). */ +const CLOCK_ID = "omarchy.clock"; + +/** Widget id of Omarchy's default weather bar entry (center column). */ +const WEATHER_ID = "omarchy.weather"; + +/** Widget id of Omarchy's default system-update bar entry (center anchor). */ +const SYSTEM_UPDATE_ID = "omarchy.system-update"; + +/** Widget id of Omarchy's default tray bar entry (right-column anchor). */ +const TRAY_ID = "omarchy.tray"; + +/** Widget id of Omarchy's default network bar entry (right-column anchor). */ +const NETWORK_ID = "omarchy.network"; + +/** Build a polling `timmo.command` bar entry. */ +function command(settings: Omit): BarEntry { + return { id: "timmo.command", ...settings }; +} + +/** Build a streaming `timmo.stream-command` bar entry. */ +function stream(settings: Omit): BarEntry { + return { id: "timmo.stream-command", ...settings }; +} + +/** Resolve the host-specific temperature module settings. */ +function temperatureEntry(host: string): BarEntry { + const desktop = host === "desktop"; + const entity = desktop + ? "sensor.meter_d828_temperature" + : "sensor.meter_plus_378b_temperature"; + const name = desktop ? "Meter D828 Temperature" : "Meter Plus Temperature"; + const page = desktop ? "office" : "living-room"; + return command({ + run: `ha-module-bar temperature --entity ${entity} --name '${name}' --icon 󰔏`, + interval: 15000, + onClick: `omarchy-launch-webapp '${HA}/lovelace/${page}?more-info-entity-id=${entity}'`, + classColors: { temperature: COLOR.cream }, + hideClasses: ["hidden"], + }); +} + +/** + * Laptop-only dining-room temperature module. A second Home Assistant + * temperature sensor shown alongside the main one, matching the living-room + * temperature module (plain reading, no gating). + */ +function diningTemperatureEntry(): BarEntry { + const entity = "sensor.meter_plus_433c_temperature"; + return command({ + run: `ha-module-bar temperature --entity ${entity} --name 'Dining Room Temperature' --icon 󰩰`, + interval: 15000, + onClick: `omarchy-launch-webapp '${HA}/lovelace/home?more-info-entity-id=${entity}'`, + classColors: { temperature: COLOR.cream }, + hideClasses: ["hidden"], + }); +} + +/** Resolve the host-specific CO2 module settings. */ +function co2Entry(host: string): BarEntry { + const desktop = host === "desktop"; + const entity = desktop + ? "sensor.meter_d828_carbon_dioxide" + : "sensor.apollo_air_1_806d64_co2"; + const name = desktop ? "Meter D828 CO2" : "Apollo Air 1 CO2"; + return command({ + run: `ha-module-bar co2-alert --entity ${entity} --name '${name}' --icon 󰟤`, + interval: 15000, + onClick: `omarchy-launch-webapp '${HA}/lovelace/environment?more-info-entity-id=${entity}'`, + classColors: { warning: COLOR.orange, critical: COLOR.co2Critical }, + hideClasses: ["hidden"], + }); +} + +/** Resolve the host-specific doorbell module settings. */ +function doorbellEntry(host: string): BarEntry { + const base = + "doorbell-popup --open-only --camera-entity camera.front_door_snapshot"; + const triggerCommand = + host === "desktop" + ? `${base} --no-auto-close --monitor DP-1` + : host === "laptop" + ? `${base} --no-auto-close --monitor eDP-1 --width 380 --height 450` + : base; + return stream({ + run: + "ha-module-bar doorbell --entity input_boolean.doorbell --icon 󰂚 " + + "--stream-key doorbell.input_boolean.doorbell --trigger-state on " + + `--trigger-command '${triggerCommand}' --trigger-on transition ` + + "--trigger-initial false --trigger-cooldown 2 " + + "--trigger-key doorbell.popup.input_boolean.doorbell", + onClick: `omarchy-launch-webapp '${HA}/lovelace/home?more-info-entity-id=camera.front_door_snapshot'`, + classColors: { active: COLOR.rust }, + hideClasses: ["hidden"], + }); +} + +/** + * Personal workspaces module that replaces Omarchy's default `omarchy.workspaces` + * widget. The `timmo.workspaces` plugin drops persistent workspaces (only the + * workspaces that currently exist are shown) and renders the focused workspace + * as its number at full opacity, with the rest dimmed — the old Waybar + * behaviour. Opacity is set explicitly here so it is tunable in one place. + */ +function workspacesEntry(): BarEntry { + return { id: "timmo.workspaces", activeOpacity: 1, inactiveOpacity: 0.5 }; +} + +/** Personal calendar module appended to the default left column. */ +function calendarEntry(): BarEntry { + return command({ + run: "ha-module-bar current-next-event --entity input_text.current_next_event_in_an_hour --icon 󰃭", + interval: 30000, + onClick: + "launch-work-browser --tab 'https://calendar.google.com/calendar/u/0/r?pli=1'", + hideClasses: ["hidden"], + }); +} + +/** Personal status widgets inserted into the center column (host-independent). */ +function customCenterEntries(): BarEntry[] { + return [ + stream({ + run: "ha-watch-singleton --module time-check --entity input_boolean.time_check --icon 󱑎 --text-on 'Check the time' --tooltip-on 'Time Check (input_boolean.time_check): On' --tooltip-off 'Time Check (input_boolean.time_check): Off' --class-on active --class-off inactive --hide-off", + onClick: "timmo-run-command go-automate ha ib t time_check", + onClickRight: "timmo-run-command go-automate ha ib t time_check", + classColors: { active: COLOR.purple }, + hideClasses: ["hidden"], + }), + stream({ + run: "ha-watch-singleton --module in-a-call --entity input_boolean.in_a_call --icon --tooltip-on 'In a Call (input_boolean.in_a_call): On' --tooltip-off 'In a Call (input_boolean.in_a_call): Off' --class-on active --class-off inactive --hide-off", + onClick: "timmo-run-command go-automate ha ib t in_a_call", + onClickRight: "timmo-run-command go-automate ha ib t in_a_call", + classColors: { active: COLOR.teal }, + hideClasses: ["hidden"], + }), + command({ + run: "ha-module-bar nas-activity --icon 󰒋", + interval: 5000, + onClick: `omarchy-launch-webapp '${HA}/lovelace/network?more-info-entity-id=sensor.nas_activity'`, + classColors: { active: COLOR.teal }, + hideClasses: ["hidden"], + }), + command({ + run: "dot git-notifications --bar-json", + interval: 60000, + refreshTarget: "timmo.git-notifications", + loadingText: "\uf0f3 ..", + loadingClass: "notifications-unknown", + onClick: + "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot git-notifications --bar-filter", + onClickRight: "omarchy-shell -q timmo.git-notifications refresh", + classColors: { + "notifications-unknown": COLOR.grey, + "notifications-attention": COLOR.red, + "notifications-unread": COLOR.amber, + }, + hideClasses: ["hidden"], + }), + command({ + run: "dot git-diff --bar-json", + interval: 60000, + refreshTarget: "timmo.git-diff", + loadingText: "\uf418 ..", + loadingClass: "dots-unknown", + onClick: + "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot tui git-diff", + onClickRight: + "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot tui git-diff --tab other", + classColors: { + "dots-ok": COLOR.grey, + "dots-unknown": COLOR.grey, + "dots-attention": COLOR.amber, + "dots-pull-only": COLOR.green, + "dots-extra-only": COLOR.blue, + }, + // The "0" (all repos clean) state hides like the other status widgets + // and is revealed dimmed on center-cluster hover; non-zero counts always + // show. The bar-json still emits " 0" so there is an icon to reveal. + hideClasses: ["dots-ok"], + }), + command({ + run: "dot git-workflows --bar-json --since \"$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)\"", + interval: 60000, + loadingText: "\uf111 ..", + loadingClass: "workflows-unknown", + onClick: + "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot git-workflows --since \"$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)\"", + classColors: { + "workflows-unknown": COLOR.grey, + "workflows-attention": COLOR.red, + }, + hideClasses: ["hidden"], + }), + command({ + run: "package-updates-bar status", + interval: 60000, + refreshTarget: "timmo.package-updates", + loadingText: "󰏗 ..", + loadingClass: "package-updates-unknown", + onClickRight: "package-updates-bar refresh", + classColors: { + "package-updates-unknown": COLOR.grey, + "package-updates": COLOR.amber, + }, + hideClasses: ["hidden"], + }), + command({ + run: "twitch-notifications --status-bar-json --max-chars 60", + interval: 5000, + onClick: "twitch-menu", + onClickRight: "twitch-notifications-restart", + classColors: { + live: COLOR.purple, + active: COLOR.grey, + inactive: COLOR.rust, + }, + // Running with no live channels ("active") hides like the other status + // widgets and reveals dimmed on center hover. "live" stays visible, and + // "inactive" (the daemon is stopped) stays visible too so its absence is + // noticeable. The module emits its bell icon either way. + hideClasses: ["active"], + }), + ]; +} + +/** Home Assistant sensors inserted before the default right-column cluster. */ +function customRightEntries(host: string): BarEntry[] { + return [ + stream({ + run: "ha-watch-singleton --module heating --entity sensor.thermostat_status --icon 󰈸 --tooltip-on 'Thermostat Status (sensor.thermostat_status)' --class-on heating --class-off hidden --hide-off", + onClick: `omarchy-launch-webapp '${HA}/lovelace/home?more-info-entity-id=sensor.thermostat_status'`, + classColors: { heating: COLOR.orange }, + hideClasses: ["hidden"], + }), + // voc-alert is permanently hidden on desktop, so it is omitted there. + ...(host === "desktop" + ? [] + : [ + command({ + run: "ha-module-bar voc-alert --quality-entity sensor.apollo_air_1_806d64_voc_quality --value-entity sensor.apollo_air_1_806d64_sen55_voc --name 'Apollo Air 1 VOC' --icon 󰵃", + interval: 15000, + onClick: `omarchy-launch-webapp '${HA}/lovelace/environment?more-info-entity-id=sensor.apollo_air_1_806d64_sen55_voc'`, + classColors: { warning: COLOR.tan, critical: COLOR.vocCritical }, + hideClasses: ["hidden"], + }), + ]), + co2Entry(host), + stream({ + run: "ha-watch-singleton --module rain --entity binary_sensor.weather_station_rain_state_piezo --icon 󰖖 --tooltip-on 'Weather Station Rain State Piezo (binary_sensor.weather_station_rain_state_piezo): Raining' --tooltip-off 'Weather Station Rain State Piezo (binary_sensor.weather_station_rain_state_piezo): Not raining' --class-on raining --class-off hidden --hide-off", + onClick: `omarchy-launch-webapp '${HA}/home/areas-048a0fd33b134e3689eda6212a41b99d?more-info-entity-id=binary_sensor.weather_station_rain_state_piezo'`, + classColors: { raining: COLOR.blue }, + hideClasses: ["hidden"], + }), + temperatureEntry(host), + // Laptop shows a second (dining room) temperature alongside the main one. + ...(host === "laptop" ? [diningTemperatureEntry()] : []), + ]; +} + +/** Path to Omarchy's shipped default `shell.json` under `$OMARCHY_PATH`. */ +function omarchyDefaultShellConfigPath(): string { + const base = + envString(ENV.OMARCHY_PATH) ?? join(HOME_DIR, ".local", "share", "omarchy"); + return join(base, "config", "omarchy", "shell.json"); +} + +/** Narrow an unknown value to a `BarEntry[]` (array of `{ id: string, ... }`). */ +function isBarEntryArray(value: unknown): value is BarEntry[] { + return ( + Array.isArray(value) && + value.every( + (entry) => + typeof entry === "object" && + entry !== null && + typeof (entry as { id?: unknown }).id === "string", + ) + ); +} + +/** Narrow parsed JSON to the {@link ShellConfig} shape this generator mutates. */ +function isShellConfig(value: unknown): value is ShellConfig { + if (typeof value !== "object" || value === null) return false; + const bar = (value as { bar?: unknown }).bar; + if (typeof bar !== "object" || bar === null) return false; + const layout = (bar as { layout?: unknown }).layout; + if (typeof layout !== "object" || layout === null) return false; + const { left, center, right } = layout as { + left?: unknown; + center?: unknown; + right?: unknown; + }; + return ( + isBarEntryArray(left) && isBarEntryArray(center) && isBarEntryArray(right) + ); +} + +/** Insert `additions` before the first entry matching `anchorId`, else append. */ +function insertBefore( + entries: BarEntry[], + anchorId: string, + additions: readonly BarEntry[], +): void { + const index = entries.findIndex((entry) => entry.id === anchorId); + if (index === -1) entries.push(...additions); + else entries.splice(index, 0, ...additions); +} + +/** + * Merge personal widgets and host overrides into Omarchy's default shell + * config, mutating `base` in place. Default widgets are kept; personal modules + * are inserted around them ("add, not remove"). The clock stays as the center + * anchor (so the stock bar's config gear, which only renders next to a centered + * clock, still appears); the weather is relocated after the personal widgets + * and before the stock network widget in the right column. + * + * @param base - Parsed Omarchy default `shell.json`. + * @param host - The `OMARCHY_HOST` value (e.g. `desktop`, `laptop`). + */ +export function mergeOmarchyShellConfig( + base: ShellConfig, + host: string, +): ShellConfig { + base.idle = + host === "laptop" + ? { screensaver: 150, lock: 300 } + : { screensaver: 1800, lock: 3600 }; + base.bar.position = host === "laptop" ? "bottom" : "top"; + + const { left, center, right } = base.bar.layout; + + // Left: swap Omarchy's persistent workspaces widget for the personal + // timmo.workspaces widget (no persistent workspaces, focused at full + // opacity), then append the personal calendar module. + const workspacesIndex = left.findIndex((entry) => entry.id === WORKSPACES_ID); + if (workspacesIndex !== -1) left[workspacesIndex] = workspacesEntry(); + left.push(calendarEntry()); + + // Center: keep the clock in place as the center anchor. The stock bar only + // renders the config gear next to a centered clock, so the clock has to stay + // centered for that button to exist. Pull only the weather out (relocated to + // the right column below), insert personal status widgets before the default + // system-update group, and put the doorbell trigger at the very end. Center + // widgets get `revealOnHover` so a class-hidden module fades in dimmed when + // the center cluster is hovered, mirroring the idle indicators (the only bar + // section that exposes a hover-reveal signal). All custom widgets share a + // standard 8px margin (the widget default), so no per-instance margin here. + const reveal = (entry: BarEntry): BarEntry => ({ + ...entry, + revealOnHover: true, + }); + const weatherIndex = center.findIndex((entry) => entry.id === WEATHER_ID); + const weatherEntry = + weatherIndex === -1 ? undefined : center.splice(weatherIndex, 1)[0]; + insertBefore(center, SYSTEM_UPDATE_ID, customCenterEntries().map(reveal)); + center.push(reveal(doorbellEntry(host))); + + // Right: the Home Assistant sensors go before the default tray cluster, and + // weather follows the personal widgets immediately before the stock network + // widget. The clock stays centered. + insertBefore(right, TRAY_ID, customRightEntries(host)); + if (weatherEntry) insertBefore(right, NETWORK_ID, [weatherEntry]); + + // The clock anchors the center so the bar config gear renders next to it. + base.bar.centerAnchor = CLOCK_ID; + + return base; +} + +/** + * Generate and apply the per-host Quickshell `shell.json` by extending + * Omarchy's shipped default with the personal modules. Reads the default from + * `$OMARCHY_PATH/config/omarchy/shell.json`, inserts the custom widgets, and + * writes the result. Idempotent: only writes when the rendered content + * differs. Skips silently when Omarchy is disabled, the host is unknown, + * Omarchy is not installed, or no default shell config exists (pre-Omarchy 4). + * + * @returns `true` when `shell.json` was rewritten (content changed), `false` + * when it was already up to date or the step was skipped. Callers use this to + * decide whether the running shell needs reloading. + */ +export const applyOmarchyShellConfig: Effect.Effect< + boolean, + never, + Config | OutputLog +> = Effect.gen(function* () { + const config = yield* Config; + const log = yield* OutputLog; + + if (!config.omarchy.enabled) return false; + + const host = resolvedOmarchyHost(config); + if (!host) { + yield* log.info( + "Skipping Omarchy shell config (OMARCHY_HOST and Hypr host link are unset)", + ); + return false; + } + + const omarchyDir = join(CONFIG_DIR, "omarchy"); + if (!existsSync(omarchyDir)) { + yield* log.info( + `Skipping Omarchy shell config (${displayPath(omarchyDir)} not found)`, + ); + return false; + } + + const defaultPath = omarchyDefaultShellConfigPath(); + if (!existsSync(defaultPath)) { + yield* log.info( + `Skipping Omarchy shell config (no default at ${displayPath(defaultPath)}; pre-Omarchy 4?)`, + ); + return false; + } + + const parsed = yield* Effect.sync((): unknown => { + try { + return JSON.parse(readFileSync(defaultPath, "utf-8")); + } catch { + return undefined; + } + }); + if (parsed === undefined) { + yield* log.warn( + `Skipping Omarchy shell config (could not read ${displayPath(defaultPath)})`, + ); + return false; + } + + if (!isShellConfig(parsed)) { + yield* log.warn( + `Skipping Omarchy shell config (unexpected default shape in ${displayPath(defaultPath)})`, + ); + return false; + } + + const target = join(omarchyDir, "shell.json"); + const merged = mergeOmarchyShellConfig(parsed, host); + const rendered = `${JSON.stringify(merged, null, 2)}\n`; + + const existing = existsSync(target) + ? yield* Effect.sync(() => readFileSync(target, "utf-8")) + : null; + if (existing === rendered) { + yield* log.info( + `Omarchy shell config up to date: ${displayPath(target)} (host: ${host})`, + ); + return false; + } + + yield* Effect.sync(() => { + const temporary = `${target}.dot-${process.pid}`; + try { + writeFileSync(temporary, rendered, { mode: 0o600 }); + chmodSync(temporary, 0o600); + renameSync(temporary, target); + } finally { + rmSync(temporary, { force: true }); + } + }); + yield* log.info( + `Wrote Omarchy shell config: ${displayPath(target)} (host: ${host})`, + ); + return true; +}); diff --git a/dot/src/lib/omarchySync.ts b/dot/src/lib/omarchySync.ts deleted file mode 100644 index ce6fb5a9..00000000 --- a/dot/src/lib/omarchySync.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Effect, Schema } from "effect"; -import { existsSync, mkdirSync, renameSync } from "fs"; -import { join } from "path"; -import { CommandExecutor } from "../services/CommandExecutor.js"; -import { Config } from "../services/Config.js"; -import { OutputLog } from "../services/OutputLog.js"; -import { - ghRepoCloneCaptured, - gitExitCode, - gitOutput, - gitRemoteOutput, - gitWorkingTreeClean, - isGitRepo, -} from "./git.js"; -import { displayPath } from "./paths.js"; -import { ENV, envString } from "./env.js"; -import type { ConfigService } from "../services/Config.js"; - -/** Domain error for Omarchy repository sync failures. */ -class OmarchySyncError extends Schema.TaggedErrorClass()( - "OmarchySyncError", - { - message: Schema.String, - }, -) {} - -/** Branch overrides accepted by first-use Omarchy sync. */ -export interface OmarchySyncOptions { - /** Branch override for Omarchy repositories. */ - readonly branch?: string; -} - -function fail(message: string): Effect.Effect { - return Effect.fail(new OmarchySyncError({ message })); -} - -const REPO_SLUGS: Readonly> = { - waybar: "timmo001/omarchy-waybar", - uwsm: "timmo001/omarchy-uwsm", -}; - -function repoSlug(repoName: string): string | null { - return REPO_SLUGS[repoName] ?? null; -} - -function ensureRepoBase( - config: ConfigService, -): Effect.Effect { - return Effect.try({ - try: () => mkdirSync(config.omarchy.repoBase, { recursive: true }), - catch: (error) => - new OmarchySyncError({ - message: `Could not create Omarchy repo base ${displayPath(config.omarchy.repoBase)}: ${String(error)}`, - }), - }); -} - -function backupPath(repoPath: string): string { - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - return `${repoPath}.dot-init-backup-${timestamp}`; -} - -function backupExistingTarget( - repoName: string, - repoPath: string, -): Effect.Effect { - return Effect.gen(function* () { - const log = yield* OutputLog; - const target = backupPath(repoPath); - yield* log.warn( - `Moving existing non-git Omarchy ${repoName} config to ${displayPath(target)}`, - ); - yield* Effect.try({ - try: () => renameSync(repoPath, target), - catch: (error) => - new OmarchySyncError({ - message: `Could not back up existing Omarchy target ${displayPath(repoPath)}: ${String(error)}`, - }), - }); - }); -} - -function fallbackBranch(config: ConfigService, repoName: string): string { - return config.omarchy.expectedBranches[repoName] ?? "main"; -} - -function desiredBranch( - config: ConfigService, - opts: OmarchySyncOptions | undefined, - repoName: string, -): string { - return ( - opts?.branch ?? - envString(ENV.DOT_OMARCHY_BRANCH) ?? - fallbackBranch(config, repoName) - ); -} - -function ensureCleanRepo( - repoName: string, - repoPath: string, -): Effect.Effect { - return Effect.gen(function* () { - const clean = yield* gitWorkingTreeClean(repoPath).pipe( - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - if (!clean) { - return yield* fail( - `Omarchy repo ${repoName} has local changes: ${displayPath(repoPath)}`, - ); - } - }); -} - -function checkoutBranch( - repoName: string, - repoPath: string, - branch: string, -): Effect.Effect { - return Effect.gen(function* () { - const log = yield* OutputLog; - if (!branch) return; - - const remoteBranch = yield* gitRemoteOutput( - ["ls-remote", "--exit-code", "--heads", "origin", branch], - { cwd: repoPath }, - ).pipe(Effect.catchTag("GitCommandError", () => Effect.succeed(""))); - if (!remoteBranch) { - return yield* fail(`Branch '${branch}' not found for ${repoName}`); - } - - yield* log.info(`Checking out ${repoName} branch '${branch}'`); - yield* gitRemoteOutput(["fetch", "origin", branch], { cwd: repoPath }).pipe( - Effect.asVoid, - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - - const localBranchExists = - (yield* gitExitCode( - ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], - { cwd: repoPath }, - )) === 0; - - if (localBranchExists) { - yield* gitOutput(["checkout", branch], { cwd: repoPath }).pipe( - Effect.asVoid, - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - } else { - yield* gitOutput(["checkout", "-B", branch, `origin/${branch}`], { - cwd: repoPath, - }).pipe( - Effect.asVoid, - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - } - - yield* gitOutput(["branch", "--set-upstream-to", `origin/${branch}`], { - cwd: repoPath, - }).pipe( - Effect.asVoid, - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - }); -} - -function syncExistingRepo( - repoName: string, - repoPath: string, - slug: string, - branch: string, -): Effect.Effect { - return Effect.gen(function* () { - if (!existsSync(join(repoPath, ".git"))) { - return yield* fail( - `Omarchy target exists but is not a git repo: ${displayPath(repoPath)}`, - ); - } - - const remote = (yield* gitOutput(["remote", "get-url", "origin"], { - cwd: repoPath, - }).pipe( - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - )).trim(); - if (!remote.includes(slug)) { - return yield* fail( - `Omarchy repo ${repoName} remote mismatch (expected ${slug}, found ${remote})`, - ); - } - - yield* ensureCleanRepo(repoName, repoPath); - yield* checkoutBranch(repoName, repoPath, branch); - yield* gitRemoteOutput(["pull", "--rebase", "--no-edit"], { - cwd: repoPath, - }).pipe( - Effect.asVoid, - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - }); -} - -function cloneRepo( - repoPath: string, - slug: string, -): Effect.Effect { - return Effect.gen(function* () { - const log = yield* OutputLog; - yield* log.info(`Cloning ${slug} -> ${displayPath(repoPath)}`); - yield* ghRepoCloneCaptured(slug, repoPath).pipe( - Effect.catchTag("GitCommandError", (error) => fail(error.message)), - ); - }); -} - -function syncRepo( - config: ConfigService, - opts: OmarchySyncOptions | undefined, - repoName: string, -): Effect.Effect { - return Effect.gen(function* () { - const log = yield* OutputLog; - const slug = repoSlug(repoName); - if (!slug) return yield* fail(`Unknown Omarchy repository: ${repoName}`); - const repoPath = join(config.omarchy.repoBase, repoName); - const branch = desiredBranch(config, opts, repoName); - - yield* log.withSpinner( - `Syncing Omarchy ${repoName}`, - Effect.gen(function* () { - if (existsSync(repoPath)) { - if (!isGitRepo(repoPath)) { - yield* backupExistingTarget(repoName, repoPath); - yield* cloneRepo(repoPath, slug); - yield* checkoutBranch(repoName, repoPath, branch); - return; - } - - yield* syncExistingRepo(repoName, repoPath, slug, branch); - return; - } - - yield* cloneRepo(repoPath, slug); - yield* checkoutBranch(repoName, repoPath, branch); - }), - ); - }); -} - -/** Clone or update the Omarchy repositories required by these dotfiles. */ -export function syncOmarchyRepos( - opts?: OmarchySyncOptions, -): Effect.Effect { - return Effect.gen(function* () { - const config = yield* Config; - - if (!config.omarchy.enabled) return; - - yield* ensureRepoBase(config); - for (const repoName of config.omarchy.diffRepos) { - yield* syncRepo(config, opts, repoName); - } - }); -} diff --git a/dot/src/lib/stowConflicts.ts b/dot/src/lib/stowConflicts.ts index 6816179d..6aa8cf01 100644 --- a/dot/src/lib/stowConflicts.ts +++ b/dot/src/lib/stowConflicts.ts @@ -29,6 +29,38 @@ const AGENTS_PRIVATE_IGNORED_ENTRIES = new Set([ ]); const LEGACY_GHOSTTY_REPO_SLUG = "timmo001/omarchy-ghostty"; +const LEGACY_UWSM_REPO_SLUG = "timmo001/omarchy-uwsm"; + +const RETIRED_PUBLIC_STOW_PATHS = [ + "scripts/.local/bin/waybar", + "scripts/.local/bin/git-workflows-bar", + "scripts/.local/share/omarchy/bin/waybar", + "hypr/.config/hypr/hyprland.conf", + "hypr/.config/hypr/hypridle.conf", + "hypr/.config/hypr/hyprlock.conf", + "hypr/.config/hypr/autostart.conf", + "hypr/.config/hypr/bindings.conf", + "hypr/.config/hypr/envs.conf", + "hypr/.config/hypr/float-app.conf", + "hypr/.config/hypr/input.conf", + "hypr/.config/hypr/looknfeel.conf", + "hypr/.config/hypr/monitors.conf", + "hypr/.config/hypr/bin/hyprsunset-clear-dim", + "hypr/.config/hypr/bin/hyprsunset-dim-step", + "hypr/.config/hypr/bin/hyprsunset-toggle-dim", + ...["desktop", "laptop"].flatMap((host) => + [ + "autostart.conf", + "bindings.conf", + "envs.conf", + "hypridle.conf", + "hyprlock.conf", + "input.conf", + "looknfeel.conf", + "monitors.conf", + ].map((file) => `hypr/.config/hypr/hosts/${host}/${file}`), + ), +] as const; /** Stored external symlink for save/restore around stow. */ export interface ExternalSymlink { @@ -76,6 +108,32 @@ export function formatBackupMove(move: BackupMove): string { return `${displayPath(move.source)} -> ${displayPath(move.destination)}`; } +/** Remove dangling live links for files retired by the Omarchy 4 migration. */ +export function removeRetiredPublicStowLinks( + publicDotfiles: string, + homeDir = HOME_DIR, +): string[] { + const removed: string[] = []; + + for (const sourceRelative of RETIRED_PUBLIC_STOW_PATHS) { + const packageSeparator = sourceRelative.indexOf("/"); + const targetRelative = sourceRelative.slice(packageSeparator + 1); + const source = join(publicDotfiles, sourceRelative); + const target = join(homeDir, targetRelative); + + try { + if (!lstatSync(target).isSymbolicLink()) continue; + if (resolve(dirname(target), readlinkSync(target)) !== source) continue; + unlinkSync(target); + removed.push(target); + } catch { + // Missing, unreadable, or no longer owned by this stow source. + } + } + + return removed; +} + /** Backup unmanaged targets that would block stow from owning active packages. */ export function backupUnmanagedStowTargets( repoDir: string, @@ -135,6 +193,16 @@ export function backupLegacyGhosttyRepo( ); } +/** Remove the retired UWSM fork before the stowed Quattro override takes over. */ +export function removeLegacyUwsmRepo( + source = join(HOME_DIR, ".config", "uwsm"), +): string | null { + if (!isGitRepoWithSlug(source, LEGACY_UWSM_REPO_SLUG)) return null; + + rmSync(source, { recursive: true }); + return source; +} + /** * Back up live public stow targets whose content differs from their committed * repo source before an `--adopt` stow, returning their home-relative display @@ -248,6 +316,10 @@ function targetAlreadyOwnedBySource(source: string, target: string): boolean { } function isLegacyGhosttyRepo(source: string): boolean { + return isGitRepoWithSlug(source, LEGACY_GHOSTTY_REPO_SLUG); +} + +function isGitRepoWithSlug(source: string, slug: string): boolean { try { const stat = lstatSync(source); if (!stat.isDirectory()) return false; @@ -259,7 +331,7 @@ function isLegacyGhosttyRepo(source: string): boolean { if (!existsSync(gitConfig)) return false; try { - return readFileSync(gitConfig, "utf8").includes(LEGACY_GHOSTTY_REPO_SLUG); + return readFileSync(gitConfig, "utf8").includes(slug); } catch { return false; } diff --git a/dot/src/lib/stowFolders.ts b/dot/src/lib/stowFolders.ts index edf955a1..9d205a39 100644 --- a/dot/src/lib/stowFolders.ts +++ b/dot/src/lib/stowFolders.ts @@ -32,6 +32,8 @@ const INTERNAL_FOLDERS = new Set(INTERNAL_STOW_FOLDERS); * and `~/.local/state` toggles live alongside the stowed config. * - `.config/nvim`: `omarchy-nvim-setup` owns `~/.config/nvim` and writes into * it (the `lua/plugins/theme.lua` symlink and its default plugin files). + * - `.config/uwsm`: UWSM and Omarchy may add package or migration-owned files + * alongside the stowed user environment override. * - `.config/systemd/user`: omarchy ships units here and `systemctl --user * enable` writes `*.target.wants/` symlinks and drop-in directories. * - `.local/bin`: mise, npm, pip, cargo, and other tools install binaries here @@ -49,6 +51,7 @@ const NO_FOLDING_TARGET_PREFIXES = [ ".config/herdr", ".config/hypr", ".config/nvim", + ".config/uwsm", ".config/systemd/user", ".local/bin", ".local/share/applications", diff --git a/dot/src/menu.ts b/dot/src/menu.ts index 72104a8a..7f7c3676 100644 --- a/dot/src/menu.ts +++ b/dot/src/menu.ts @@ -454,7 +454,7 @@ const dotItems: readonly MenuItem[] = [ success: "Services restarted", }), undefined, - ["resume", "suspend", "sleep", "wake", "waybar", "restart", "system"], + ["resume", "suspend", "sleep", "wake", "shell", "restart", "system"], "System", ), item( diff --git a/dot/src/services/CommandExecutor.ts b/dot/src/services/CommandExecutor.ts index 070710a9..95576231 100644 --- a/dot/src/services/CommandExecutor.ts +++ b/dot/src/services/CommandExecutor.ts @@ -130,7 +130,10 @@ export interface CommandExecutorService { readonly exitCode: ( cmd: string, args: readonly string[], - opts?: { readonly cwd?: string }, + opts?: { + readonly cwd?: string; + readonly env?: Readonly>; + }, ) => Effect.Effect; /** Run a command with inherited stdio (stdin/stdout/stderr pass through) */ @@ -299,6 +302,7 @@ export class CommandExecutor extends Context.Service< stderr: "ignore", cwd: opts?.cwd, detached: true, + ...(opts?.env ? { env: { ...process.env, ...opts.env } } : {}), }); killOnAbort(proc, signal); return proc.exited; diff --git a/dot/src/services/Config.ts b/dot/src/services/Config.ts index fd0087d0..460ecd29 100644 --- a/dot/src/services/Config.ts +++ b/dot/src/services/Config.ts @@ -26,7 +26,7 @@ import { ENV, envString } from "../lib/env.js"; export interface OmarchyRepoConfig { /** Base directory for omarchy repos (default: ~/.config) */ readonly repoBase: string; - /** Repos to include in diff (e.g. ["waybar", "uwsm"]) */ + /** Repos to include in diff (e.g. ["uwsm"]) */ readonly diffRepos: readonly string[]; /** Repos with multiple worktree branches */ readonly worktreeRepos: readonly string[]; @@ -114,15 +114,11 @@ export class Config extends Context.Service()("Config") { // Omarchy config const omarchyRepoBase = envString(ENV.OMARCHY_REPO_BASE_DIR) ?? CONFIG_DIR; - const omarchyDiffRepos = ["waybar", "uwsm"]; + const omarchyDiffRepos: readonly string[] = []; const omarchyWorktreeRepos: readonly string[] = []; const omarchyWorktreeBranches = ["desktop", "laptop"]; - const omarchyExpectedBranches = { - waybar: "main", - uwsm: "main", - } satisfies Readonly>; - const omarchyEnabled = - (envString(ENV.DOT_INCLUDE_OMARCHY_DIFF_REPOS) ?? "1") !== "0"; + const omarchyExpectedBranches: Readonly> = {}; + const omarchyEnabled = true; const omarchy: OmarchyRepoConfig = { repoBase: omarchyRepoBase, diff --git a/dot/src/services/OutputLog.ts b/dot/src/services/OutputLog.ts index 8e688b89..5ec898d8 100644 --- a/dot/src/services/OutputLog.ts +++ b/dot/src/services/OutputLog.ts @@ -206,7 +206,7 @@ export class OutputLog extends Context.Service()( const paths = logFiles(defaultLogFile); // Create/prune log files lazily on first emit so query/machine commands - // that never log (e.g. Waybar bar-json polls) leave no files behind. + // that never log (e.g. status-bar JSON polls) leave no files behind. let initialised = false; const ensureInitialised = (): void => { if (initialised) return; @@ -268,7 +268,7 @@ export class OutputLog extends Context.Service()( const paths = logFiles(defaultLogFile); // Create/prune log files lazily on first emit so query/machine commands - // that never log (e.g. Waybar bar-json polls) leave no files behind. + // that never log (e.g. status-bar JSON polls) leave no files behind. let initialised = false; const ensureInitialised = (): void => { if (initialised) return; diff --git a/dot/src/tui/Toast.ts b/dot/src/tui/Toast.ts index 7885f313..e7acc8f2 100644 --- a/dot/src/tui/Toast.ts +++ b/dot/src/tui/Toast.ts @@ -84,7 +84,7 @@ export class Toast { * If `id` matches the current toast, the message and variant are replaced * in-place. Otherwise the previous toast is dismissed and a new one shown. * - * @param id - Stable grouping identifier (e.g. "memory", "restart.waybar") + * @param id - Stable grouping identifier (e.g. "memory", "restart.shell") * @param message - Display text * @param variant - Controls border colour and auto-dismiss timing */ diff --git a/dot/src/types.ts b/dot/src/types.ts index 43431757..05500d7e 100644 --- a/dot/src/types.ts +++ b/dot/src/types.ts @@ -15,7 +15,7 @@ export interface Repo { * `dot update --check` to core/system repos. * * - `dotfiles`: public or private dotfiles repositories - * - `omarchy`: Omarchy system repos (waybar, uwsm) + * - `omarchy`: Omarchy system repositories * - `notes`: the notes vault repository * - `private`: schedule-gated activity repos from `dot-git.yml` */ diff --git a/dot/tests/commands/Update.test.ts b/dot/tests/commands/Update.test.ts index 14a7c7d2..45ac480c 100644 --- a/dot/tests/commands/Update.test.ts +++ b/dot/tests/commands/Update.test.ts @@ -1,5 +1,115 @@ import { describe, expect, test } from "bun:test"; -import { herdrLazyPluginRoot } from "../../src/commands/Update.js"; +import { Effect, Layer, Stream } from "effect"; +import { + herdrLazyPluginRoot, + reloadOmarchyShellIfChanged, +} from "../../src/commands/Update.js"; +import { CommandExecutor } from "../../src/services/CommandExecutor.js"; +import { Config, type ConfigService } from "../../src/services/Config.js"; +import { emptyDotGitConfig } from "../../src/services/GitConfig.js"; +import { OutputLog } from "../../src/services/OutputLog.js"; +import { emptyMcpConfig } from "../../src/mcp/sync/loadSpec.js"; + +function config(enabled: boolean): ConfigService { + return { + publicDotfiles: "/tmp/dotfiles", + privateDotfiles: null, + canUsePrivate: false, + privateReason: "test", + notesDir: "/tmp/notes", + omarchy: { + repoBase: "/tmp", + diffRepos: [], + worktreeRepos: [], + worktreeBranches: [], + expectedBranches: {}, + enabled, + }, + gitConfig: emptyDotGitConfig("/tmp/dot-git.yml"), + mcpConfig: emptyMcpConfig("/tmp/mcp.yml"), + cacheDir: "/tmp/cache", + stateDir: "/tmp/state", + logDir: "/tmp/state/logs", + }; +} + +describe("reloadOmarchyShellIfChanged", () => { + test("restarts under Wayland", async () => { + const calls: Array<{ + command: string; + args: readonly string[]; + options?: { readonly env?: Readonly> }; + }> = []; + const messages: string[] = []; + const layers = Layer.mergeAll( + Layer.succeed(Config, config(true)), + Layer.succeed(CommandExecutor, { + run: () => Effect.die("run should not be called"), + stream: () => Stream.die("stream should not be called"), + inherit: () => Effect.die("inherit should not be called"), + exitCode: (command, args, options) => + Effect.sync(() => { + calls.push({ command, args, options }); + return 0; + }), + }), + Layer.succeed(OutputLog, { + info: (message) => Effect.sync(() => void messages.push(message)), + warn: () => Effect.void, + error: () => Effect.void, + section: () => Effect.void, + stream: Stream.empty, + flush: Effect.succeed(""), + withSpinner: (_label, effect) => effect, + updateSpinner: () => Effect.void, + }), + ); + + await Effect.runPromise( + reloadOmarchyShellIfChanged(true).pipe(Effect.provide(layers)), + ); + + expect(calls).toEqual([ + { + command: "omarchy", + args: ["restart", "shell"], + options: { env: { QT_QPA_PLATFORM: "wayland" } }, + }, + ]); + expect(messages).toContain("Reloaded Omarchy shell (shell.json changed)"); + }); + + test("does nothing when the config did not change or Omarchy is disabled", async () => { + for (const [changed, enabled] of [ + [false, true], + [true, false], + ] as const) { + const layers = Layer.mergeAll( + Layer.succeed(Config, config(enabled)), + Layer.succeed(CommandExecutor, { + run: () => Effect.die("run should not be called"), + stream: () => Stream.die("stream should not be called"), + inherit: () => Effect.die("inherit should not be called"), + exitCode: () => Effect.die("exitCode should not be called"), + }), + Layer.succeed(OutputLog, { + info: () => Effect.void, + warn: () => Effect.void, + error: () => Effect.void, + section: () => Effect.void, + stream: Stream.empty, + flush: Effect.succeed(""), + withSpinner: (_label, effect) => effect, + updateSpinner: () => Effect.void, + }), + ); + + await Effect.runPromise( + reloadOmarchyShellIfChanged(changed).pipe(Effect.provide(layers)), + ); + } + }); +}); describe("herdrLazyPluginRoot", () => { test("returns the installed Herdr Lazy plugin root", () => { diff --git a/dot/tests/doctor/checks/opencodeServer.test.ts b/dot/tests/doctor/checks/opencodeServer.test.ts index 912e5b76..0e8596df 100644 --- a/dot/tests/doctor/checks/opencodeServer.test.ts +++ b/dot/tests/doctor/checks/opencodeServer.test.ts @@ -16,7 +16,7 @@ function tempPaths() { process.env.TMPDIR ?? "/tmp", `opencode-server-test-${process.pid}-${Date.now()}-${tempRoots.length}`, ); - const autostartPath = join(root, "hypr", "autostart.conf"); + const autostartPath = join(root, "hypr", "autostart.lua"); const envPath = join(root, "opencode", ".env"); mkdirSync(join(root, "hypr"), { recursive: true }); mkdirSync(join(root, "opencode"), { recursive: true }); @@ -27,7 +27,7 @@ function tempPaths() { describe("opencodeServerResults", () => { test("accepts desktop autostart and a configured password", () => { const { autostartPath, envPath } = tempPaths(); - writeFileSync(autostartPath, "exec-once = opencode-server\n"); + writeFileSync(autostartPath, 'o.exec_on_start("opencode-server")\n'); writeFileSync(envPath, "OPENCODE_SERVER_PASSWORD='configured'\n"); expect(opencodeServerResults(autostartPath, envPath)).toEqual([ @@ -44,7 +44,7 @@ describe("opencodeServerResults", () => { test("warns when desktop autostart does not start the server", () => { const { autostartPath, envPath } = tempPaths(); - writeFileSync(autostartPath, "exec-once = another-service\n"); + writeFileSync(autostartPath, 'o.exec_on_start("another-service")\n'); expect(opencodeServerResults(autostartPath, envPath)).toEqual([ { @@ -57,7 +57,7 @@ describe("opencodeServerResults", () => { test("checks the local password whenever desktop autostart is enabled", () => { const { autostartPath, envPath } = tempPaths(); - writeFileSync(autostartPath, "exec-once = opencode-server\n"); + writeFileSync(autostartPath, 'o.exec_on_start("opencode-server")\n'); expect(opencodeServerResults(autostartPath, envPath)[1]).toEqual({ severity: "warn", diff --git a/dot/tests/lib/omarchyShellConfig.test.ts b/dot/tests/lib/omarchyShellConfig.test.ts new file mode 100644 index 00000000..bfe333e0 --- /dev/null +++ b/dot/tests/lib/omarchyShellConfig.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, test } from "bun:test"; +import { mergeOmarchyShellConfig } from "../../src/lib/omarchyShellConfig.js"; + +function baseConfig() { + return { + version: 1, + custom: { preserved: true }, + bar: { + position: "top", + customBarField: "preserved", + layout: { + left: [ + { id: "custom.left" }, + { id: "omarchy.workspaces", persistent: true }, + ], + center: [ + { id: "omarchy.clock", format: "HH:mm" }, + { id: "omarchy.weather", location: "home" }, + { id: "omarchy.system-update" }, + { id: "custom.center" }, + ], + right: [ + { id: "custom.right" }, + { id: "omarchy.tray" }, + { id: "omarchy.agents", customAgentField: "preserved" }, + { id: "omarchy.network" }, + ], + }, + }, + }; +} + +function commandRuns( + entries: ReadonlyArray<{ readonly id: string; readonly run?: unknown }>, +): string[] { + return entries.flatMap((entry) => + typeof entry.run === "string" ? [entry.run] : [], + ); +} + +describe("mergeOmarchyShellConfig", () => { + test("preserves defaults while placing personal widgets around their anchors", () => { + const base = baseConfig(); + const merged = mergeOmarchyShellConfig(base, "desktop"); + + expect(merged).toBe(base); + expect(merged.custom).toEqual({ preserved: true }); + expect(merged.idle).toEqual({ screensaver: 1800, lock: 3600 }); + expect(merged.bar.customBarField).toBe("preserved"); + expect(merged.bar.centerAnchor).toBe("omarchy.clock"); + expect(merged.bar.position).toBe("top"); + + expect(merged.bar.layout.left[1]).toEqual({ + id: "timmo.workspaces", + activeOpacity: 1, + inactiveOpacity: 0.5, + }); + expect(merged.bar.layout.left.at(-1)?.id).toBe("timmo.command"); + + const centerIds = merged.bar.layout.center.map(({ id }) => id); + expect(centerIds).not.toContain("omarchy.weather"); + expect(centerIds.indexOf("timmo.command")).toBeLessThan( + centerIds.indexOf("omarchy.system-update"), + ); + expect(merged.bar.layout.center.at(-1)).toMatchObject({ + id: "timmo.stream-command", + revealOnHover: true, + }); + expect( + merged.bar.layout.center + .filter(({ id }) => id.startsWith("timmo.")) + .every((entry) => entry.revealOnHover === true), + ).toBe(true); + + const rightIds = merged.bar.layout.right.map(({ id }) => id); + expect(rightIds.indexOf("timmo.command")).toBeLessThan( + rightIds.indexOf("omarchy.weather"), + ); + expect(rightIds.indexOf("omarchy.weather")).toBeLessThan( + rightIds.indexOf("omarchy.network"), + ); + expect(merged.bar.layout.right).toContainEqual({ + id: "omarchy.weather", + location: "home", + }); + expect(merged.bar.layout.right).toContainEqual({ id: "omarchy.tray" }); + expect(merged.bar.layout.right).toContainEqual({ + id: "omarchy.agents", + customAgentField: "preserved", + }); + }); + + test("selects desktop-specific sensors and doorbell placement", () => { + const merged = mergeOmarchyShellConfig(baseConfig(), "desktop"); + const runs = commandRuns([ + ...merged.bar.layout.center, + ...merged.bar.layout.right, + ]).join("\n"); + + expect(runs).toContain("sensor.meter_d828_temperature"); + expect(runs).toContain("sensor.meter_d828_carbon_dioxide"); + expect(runs).toContain("ha-module-bar"); + expect(runs).toContain("dot git-diff --bar-json"); + expect(runs).toContain("dot git-notifications --bar-json"); + expect(runs).toContain("dot git-workflows --bar-json"); + expect(runs).toContain("package-updates-bar status"); + expect(runs).not.toContain("ha-bar-module"); + expect(runs).not.toContain("voc-alert"); + expect(runs).not.toContain("sensor.meter_plus_433c_temperature"); + expect(runs).toContain("--monitor DP-1"); + }); + + test("selects laptop-specific layout, sensors, and doorbell placement", () => { + const merged = mergeOmarchyShellConfig(baseConfig(), "laptop"); + const runs = commandRuns([ + ...merged.bar.layout.center, + ...merged.bar.layout.right, + ]).join("\n"); + + expect(merged.bar.position).toBe("bottom"); + expect(merged.idle).toEqual({ screensaver: 150, lock: 300 }); + expect(runs).toContain("sensor.meter_plus_378b_temperature"); + expect(runs).toContain("sensor.apollo_air_1_806d64_co2"); + expect(runs).toContain("voc-alert"); + expect(runs).toContain("sensor.meter_plus_433c_temperature"); + expect(runs).toContain("--monitor eDP-1 --width 380 --height 450"); + }); +}); diff --git a/dot/tests/lib/stowConflicts.test.ts b/dot/tests/lib/stowConflicts.test.ts index 5de8fbd5..47f7d89b 100644 --- a/dot/tests/lib/stowConflicts.test.ts +++ b/dot/tests/lib/stowConflicts.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { existsSync, + lstatSync, mkdirSync, mkdtempSync, readFileSync, @@ -19,6 +20,8 @@ import { backupUnmanagedStowTargets, removeStowedSkillOwner, removeStaleSkillSymlinks, + removeRetiredPublicStowLinks, + removeLegacyUwsmRepo, } from "../../src/lib/stowConflicts.js"; const previousOmarchyHost = process.env[ENV.OMARCHY_HOST]; @@ -70,6 +73,65 @@ afterEach(() => { } }); +describe("removeLegacyUwsmRepo", () => { + test("removes the retired fork including generated migration files", () => { + const root = tempRoot(); + const source = join(root, "uwsm"); + mkdirSync(join(source, ".git"), { recursive: true }); + mkdirSync(join(source, "env.d"), { recursive: true }); + writeFileSync( + join(source, ".git", "config"), + "url = git@github.com:timmo001/omarchy-uwsm.git\n", + ); + writeFileSync( + join(source, "env.d", "99-omarchy-upgrade-env"), + "generated\n", + ); + writeFileSync( + join(source, "env.omarchy-upgrade-to-quattro.20260812115936.bak"), + "generated\n", + ); + + expect(removeLegacyUwsmRepo(source)).toBe(source); + expect(existsSync(source)).toBe(false); + }); + + test("leaves unrelated repositories untouched", () => { + const root = tempRoot(); + const source = join(root, "uwsm"); + mkdirSync(join(source, ".git"), { recursive: true }); + writeFileSync( + join(source, ".git", "config"), + "url = git@github.com:someone/uwsm.git\n", + ); + + expect(removeLegacyUwsmRepo(source)).toBeNull(); + expect(existsSync(source)).toBe(true); + }); +}); + +describe("removeRetiredPublicStowLinks", () => { + test("removes only retired links owned by the public stow source", () => { + const root = tempRoot(); + const publicDotfiles = join(root, "dotfiles"); + const homeDir = join(root, "home"); + const retiredSource = join(publicDotfiles, "scripts/.local/bin/waybar"); + const retiredTarget = join(homeDir, ".local/bin/waybar"); + const unrelatedTarget = join(homeDir, ".local/bin/git-workflows-bar"); + mkdirSync(dirname(retiredSource), { recursive: true }); + mkdirSync(dirname(retiredTarget), { recursive: true }); + symlinkSync(retiredSource, retiredTarget); + symlinkSync("/tmp/external-git-workflows-bar", unrelatedTarget); + + expect(removeRetiredPublicStowLinks(publicDotfiles, homeDir)).toEqual([ + retiredTarget, + ]); + expect(existsSync(retiredTarget)).toBe(false); + expect(existsSync(unrelatedTarget)).toBe(false); + expect(() => lstatSync(unrelatedTarget)).not.toThrow(); + }); +}); + describe("backupUnmanagedStowTargets", () => { test("leaves explicitly ignored targets in place", () => { const root = tempRoot(); diff --git a/dot/tests/lib/stowFolders.test.ts b/dot/tests/lib/stowFolders.test.ts index 65b54d7d..8e87e413 100644 --- a/dot/tests/lib/stowFolders.test.ts +++ b/dot/tests/lib/stowFolders.test.ts @@ -3,9 +3,9 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import type { ConfigService } from "../../src/services/Config.js"; +import { ENV } from "../../src/lib/env.js"; import { emptyDotGitConfig } from "../../src/services/GitConfig.js"; import { emptyMcpConfig } from "../../src/mcp/sync/loadSpec.js"; -import { ENV } from "../../src/lib/env.js"; import { listStowFolders, requiresNoFolding, @@ -20,11 +20,11 @@ function tempRoot(): string { return root; } -function fakeConfig(repoBase: string): ConfigService { +function fakeConfig(repoBase: string, host = "desktop"): ConfigService { const omarchyRepoBase = join(repoBase, ".omarchy"); - const desktopHost = join(omarchyRepoBase, "hypr", "hosts", "desktop"); - mkdirSync(desktopHost, { recursive: true }); - symlinkSync(desktopHost, join(omarchyRepoBase, "hypr", "host"), "dir"); + const hostDir = join(omarchyRepoBase, "hypr", "hosts", host); + mkdirSync(hostDir, { recursive: true }); + symlinkSync(hostDir, join(omarchyRepoBase, "hypr", "host"), "dir"); return { publicDotfiles: repoBase, @@ -84,6 +84,45 @@ describe("listStowFolders", () => { "scripts--desktop", ]); }); + + test("selects the requested host-specific package", () => { + process.env[ENV.OMARCHY_HOST] = "laptop"; + const root = tempRoot(); + mkdirSync(join(root, "scripts")); + mkdirSync(join(root, "scripts--desktop")); + mkdirSync(join(root, "scripts--laptop")); + + expect(listStowFolders(root, fakeConfig(root)).sort()).toEqual([ + "scripts", + "scripts--laptop", + ]); + }); + + test("falls back to the persisted Hypr host link", () => { + delete process.env[ENV.OMARCHY_HOST]; + const root = tempRoot(); + mkdirSync(join(root, "scripts")); + mkdirSync(join(root, "scripts--desktop")); + mkdirSync(join(root, "scripts--laptop")); + + expect(listStowFolders(root, fakeConfig(root, "laptop")).sort()).toEqual([ + "scripts", + "scripts--laptop", + ]); + }); + + test("prefers the environment host over the persisted Hypr host link", () => { + process.env[ENV.OMARCHY_HOST] = "desktop"; + const root = tempRoot(); + mkdirSync(join(root, "scripts")); + mkdirSync(join(root, "scripts--desktop")); + mkdirSync(join(root, "scripts--laptop")); + + expect(listStowFolders(root, fakeConfig(root, "laptop")).sort()).toEqual([ + "scripts", + "scripts--desktop", + ]); + }); }); describe("requiresNoFolding", () => { @@ -91,10 +130,12 @@ describe("requiresNoFolding", () => { const root = tempRoot(); mkdirSync(join(root, "scripts", ".local", "bin"), { recursive: true }); mkdirSync(join(root, "herdr", ".config", "herdr"), { recursive: true }); + mkdirSync(join(root, "uwsm", ".config", "uwsm"), { recursive: true }); mkdirSync(join(root, "plain", ".config", "example"), { recursive: true }); expect(requiresNoFolding(root, "scripts")).toBe(true); expect(requiresNoFolding(root, "herdr")).toBe(true); + expect(requiresNoFolding(root, "uwsm")).toBe(true); expect(requiresNoFolding(root, "plain")).toBe(false); }); }); diff --git a/dotfiles-skills/.agents/skills/dotfiles-stow/SKILL.md b/dotfiles-skills/.agents/skills/dotfiles-stow/SKILL.md index 42f8e544..e1d79e23 100644 --- a/dotfiles-skills/.agents/skills/dotfiles-stow/SKILL.md +++ b/dotfiles-skills/.agents/skills/dotfiles-stow/SKILL.md @@ -29,3 +29,9 @@ Use this skill for changes to user config managed by GNU Stow through the public - Reviewed third-party snapshots -> `~/repos/skills/` plus `imports.json` Do not run `dot clean` unless the user explicitly asks to remove stowed configuration. Preserve unrelated worktree changes and continue with public-safe work when the private overlay is unavailable. + +## Omarchy Host Overrides + +- Hyprland config is stowed from `hypr/.config/hypr/`, with host overrides selected through `~/.config/hypr/host`. +- The Hypr package is stowed non-destructively: `dot stow` and `dot install` never unstow it first, so `hyprland.lua` cannot disappear during Hyprland's live reload. +- Keep the generated Omarchy `shell.json`, Lua host layout, and canonical docs in sync when their contracts change. diff --git a/fish/.config/fish/completions/dot.fish b/fish/.config/fish/completions/dot.fish index a37e9179..cd6715ee 100644 --- a/fish/.config/fish/completions/dot.fish +++ b/fish/.config/fish/completions/dot.fish @@ -12,7 +12,6 @@ complete -c dot -l interactive -d 'Enable the Hypr host questionnaire when no ho complete -c dot -l force -d 'Re-run init even if the machine looks initialised' -n '__fish_seen_subcommand_from init' complete -c dot -l host -d 'Hypr host to link before stow (default: OMARCHY_HOST or desktop)' -r -n '__fish_seen_subcommand_from init' complete -c dot -l log -d 'Init log path (default: ~/.local/state/dot/init.log)' -r -F -n '__fish_seen_subcommand_from init' -complete -c dot -l branch -d 'Branch override for Omarchy repos' -r -n '__fish_seen_subcommand_from init' complete -c dot -s h -l help -d 'Show this help message' -n '__fish_seen_subcommand_from init' complete -c dot -n '__fish_use_subcommand' -a 'install' -d 'Ensure prerequisites, then backup/adopt dotfiles' complete -c dot -s h -l help -d 'Show this help message' -n '__fish_seen_subcommand_from install' diff --git a/ghostty/.config/ghostty/config b/ghostty/.config/ghostty/config index 9eb8ac8e..b4e23ae2 100644 --- a/ghostty/.config/ghostty/config +++ b/ghostty/.config/ghostty/config @@ -1,5 +1,5 @@ # Dynamic theme colors -config-file = ?"~/.config/omarchy/current/theme/ghostty.conf" +config-file = ?"~/.local/state/omarchy/current/theme/ghostty.conf" # Theme font-family = "JetBrainsMono Nerd Font" diff --git a/hypr/.config/hypr/.gitignore b/hypr/.config/hypr/.gitignore new file mode 100644 index 00000000..680eea08 --- /dev/null +++ b/hypr/.config/hypr/.gitignore @@ -0,0 +1,4 @@ +.claude/ +shaders/ +.state/ +host diff --git a/hypr/.config/hypr/.luarc.json b/hypr/.config/hypr/.luarc.json new file mode 100644 index 00000000..afb4466c --- /dev/null +++ b/hypr/.config/hypr/.luarc.json @@ -0,0 +1,11 @@ +{ + "workspace": { + "library": [ + "/usr/share/hypr/stubs" + ], + "checkThirdParty": false + }, + "diagnostics": { + "globals": ["hl", "o"] + } +} diff --git a/hypr/.config/hypr/AGENTS.md b/hypr/.config/hypr/AGENTS.md new file mode 100644 index 00000000..d0685e6a --- /dev/null +++ b/hypr/.config/hypr/AGENTS.md @@ -0,0 +1,16 @@ +# HYPR AGENTS + +Instructions for coding agents working in the Hyprland config package. + +## Host Override Layout + +- This config is stowed from the dotfiles repo as the `hypr` package, with host-specific overrides. +- Shared entry files live at the package root. +- Host overrides live under `hosts/desktop/` and `hosts/laptop/`. +- `dot stow` creates `~/.config/hypr/host` as a symlink to `hosts/$OMARCHY_HOST`. +- This package is stowed non-destructively: `dot stow` and `dot install` skip the usual unstow-then-restow for `hypr` (its symlinks, notably `hyprland.lua`, never vanish mid-stow) and reload Hyprland afterwards, so Hyprland's live-config autoreload never trips into emergency mode. Preserve this if you edit the stow loop in `dot/src/commands/{Stow,Install}.ts`. + +## Documentation Sync + +- If this host override arrangement changes, update this package's `README.md` and `AGENTS.md` plus the related documentation and skill guidance in `~/.config/dotfiles` together. +- Keep host-specific instructions accurate for both laptop and desktop overrides. diff --git a/hypr/.config/hypr/README.md b/hypr/.config/hypr/README.md new file mode 100644 index 00000000..afd2f8db --- /dev/null +++ b/hypr/.config/hypr/README.md @@ -0,0 +1,12 @@ +# Omarchy Hyprland Config + +My Hyprland Config for [omarchy](https://omarchy.org), stowed from my [dotfiles](https://github.com/timmo001/dotfiles/tree/distro/arch-omarchy) as the `hypr` package. + +This config uses Lua entry files with host-specific overrides. + +- Shared entry files live at the package root (`hypr/.config/hypr/`). +- Host overrides live under `hosts/desktop/` and `hosts/laptop/`. +- `dot stow` lays down the package with `--no-folding` and creates `~/.config/hypr/host` as a symlink to `hosts/$OMARCHY_HOST`. +- This package is stowed non-destructively: `dot stow` and `dot install` skip the usual unstow-then-restow for `hypr` so its symlinks (notably `hyprland.lua`) never disappear mid-stow, then reload Hyprland afterwards. This keeps Hyprland's live-config autoreload from catching a missing config and dropping into emergency mode. + +If this host override arrangement changes, update this `README.md`, this package's `AGENTS.md`, and the related documentation and skill guidance in `~/.config/dotfiles` together. diff --git a/hypr/.config/hypr/autostart.conf b/hypr/.config/hypr/autostart.conf deleted file mode 100644 index 13b838e5..00000000 --- a/hypr/.config/hypr/autostart.conf +++ /dev/null @@ -1,11 +0,0 @@ -# Shared autostart processes. -exec-once = $systemBridge -exec-once = $browserPersonal -exec-once = uwsm app -- kdeconnect-indicator -exec-once = uwsm-app -s b -- twitch-notifications -exec-once = uwsm-app -s b -- env USAGEBAR_DISABLE_BROWSER_COOKIES=1 herdr server - -# Host-specific additions selected by dot via ~/.config/hypr/host. -# hyprlang noerror true -source = ~/.config/hypr/host/autostart.conf -# hyprlang noerror false diff --git a/hypr/.config/hypr/autostart.lua b/hypr/.config/hypr/autostart.lua new file mode 100644 index 00000000..9678df92 --- /dev/null +++ b/hypr/.config/hypr/autostart.lua @@ -0,0 +1,10 @@ +-- Extra autostart processes. +-- o.launch_on_start("my-service") + +o.exec_on_start("timmo-run-command system-bridge backend") +o.exec_on_start([[uwsm app -- chromium --new-window --ozone-platform=wayland --profile-directory="Default" --force-device-scale-factor=0.8]]) +o.exec_on_start("uwsm app -- kdeconnect-indicator") +o.exec_on_start("uwsm-app -s b -- twitch-notifications") +o.exec_on_start("uwsm-app -s b -- env USAGEBAR_DISABLE_BROWSER_COOKIES=1 herdr server") + +require("hypr.host.autostart") diff --git a/hypr/.config/hypr/bin/hyprsunset-clear-dim b/hypr/.config/hypr/bin/hyprsunset-clear-dim deleted file mode 100755 index 8cb7cf1a..00000000 --- a/hypr/.config/hypr/bin/hyprsunset-clear-dim +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -CONFIG="$HOME/.config/hypr/host/hyprsunset.conf" -STATE_DIR="$HOME/.config/hypr/.state" -STATE_FILE="$STATE_DIR/hyprsunset-dim-enabled" -LEVEL_FILE="$STATE_DIR/hyprsunset-dim-level" - -if ! pgrep -x hyprsunset >/dev/null; then - setsid uwsm-app -- hyprsunset --config "$CONFIG" & - sleep 1 -fi - -hyprctl hyprsunset identity -hyprctl hyprsunset gamma 100 -rm -f "$STATE_FILE" "$LEVEL_FILE" diff --git a/hypr/.config/hypr/bin/hyprsunset-dim-step b/hypr/.config/hypr/bin/hyprsunset-dim-step deleted file mode 100755 index 54d21491..00000000 --- a/hypr/.config/hypr/bin/hyprsunset-dim-step +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -CONFIG="$HOME/.config/hypr/host/hyprsunset.conf" -STEP=5 -MIN_GAMMA=5 -MAX_GAMMA=100 -STATE_DIR="$HOME/.config/hypr/.state" -STATE_FILE="$STATE_DIR/hyprsunset-dim-enabled" -LEVEL_FILE="$STATE_DIR/hyprsunset-dim-level" -TOGGLE_SCRIPT="$HOME/.config/hypr/bin/hyprsunset-toggle-dim" - -if ! pgrep -x hyprsunset >/dev/null; then - setsid uwsm-app -- hyprsunset --config "$CONFIG" & - sleep 1 -fi - -if [[ ! -f "$STATE_FILE" ]]; then - "$TOGGLE_SCRIPT" >/dev/null 2>&1 -fi - -if [[ -f "$LEVEL_FILE" ]]; then - CURRENT_GAMMA=$(cat "$LEVEL_FILE") -else - CURRENT_GAMMA=$(hyprctl hyprsunset gamma 2>/dev/null | grep -oE '[0-9]+') -fi -if [[ -z "$CURRENT_GAMMA" ]]; then - CURRENT_GAMMA=$MAX_GAMMA -fi - -case "$1" in - up) NEW_GAMMA=$((CURRENT_GAMMA + STEP)) ;; - down) NEW_GAMMA=$((CURRENT_GAMMA - STEP)) ;; - *) exit 1 ;; -esac - -if (( NEW_GAMMA < MIN_GAMMA )); then - NEW_GAMMA=$MIN_GAMMA -elif (( NEW_GAMMA > MAX_GAMMA )); then - NEW_GAMMA=$MAX_GAMMA -fi - -mkdir -p "$STATE_DIR" -hyprctl hyprsunset identity -hyprctl hyprsunset gamma "$NEW_GAMMA" -echo "$NEW_GAMMA" > "$LEVEL_FILE" -omarchy notification send "󰖲" "Dim level" "${NEW_GAMMA}%" diff --git a/hypr/.config/hypr/bin/hyprsunset-toggle-dim b/hypr/.config/hypr/bin/hyprsunset-toggle-dim deleted file mode 100755 index 6431ce65..00000000 --- a/hypr/.config/hypr/bin/hyprsunset-toggle-dim +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -CONFIG="$HOME/.config/hypr/host/hyprsunset.conf" -ON_GAMMA=50 -OFF_GAMMA=100 -STATE_DIR="$HOME/.config/hypr/.state" -STATE_FILE="$STATE_DIR/hyprsunset-dim-enabled" -LEVEL_FILE="$STATE_DIR/hyprsunset-dim-level" - -if ! pgrep -x hyprsunset >/dev/null; then - setsid uwsm-app -- hyprsunset --config "$CONFIG" & - sleep 1 -fi - -CURRENT_GAMMA=$(hyprctl hyprsunset gamma 2>/dev/null | grep -oE '[0-9]+') - -if [[ "$CURRENT_GAMMA" == "$OFF_GAMMA" ]]; then - mkdir -p "$STATE_DIR" - hyprctl hyprsunset identity - hyprctl hyprsunset gamma $ON_GAMMA - touch "$STATE_FILE" - echo "$ON_GAMMA" > "$LEVEL_FILE" - omarchy notification send "󰖲" "Dim mode enabled" -else - hyprctl hyprsunset identity - hyprctl hyprsunset gamma $OFF_GAMMA - rm -f "$STATE_FILE" - omarchy notification send "󰖲" "Dim mode disabled" -fi diff --git a/hypr/.config/hypr/bindings.conf b/hypr/.config/hypr/bindings.conf deleted file mode 100644 index c387a0b2..00000000 --- a/hypr/.config/hypr/bindings.conf +++ /dev/null @@ -1,122 +0,0 @@ -# Screen recording -bindd = SHIFT ALT, PRINT, Screenrecording, exec, omarchy screenrecord - -# Application bindings -bindd = SUPER ALT, RETURN, Tmux, exec, uwsm-app -- xdg-terminal-exec --dir="$(omarchy-cmd-terminal-cwd)" tmux new -$terminal = uwsm app -- ghostty-host-config -$fileManager = uwsm app -- thunar -$browserPersonal = uwsm app -- chromium --new-window --ozone-platform=wayland --profile-directory="Default" --force-device-scale-factor=0.8 -$browserWork = launch-work-browser -$discord = launch-work-discord -$slack = launch-work-slack -$systemBridge = timmo-run-command system-bridge backend - -# Resume recovery -bindd = SUPER SHIFT, W, Resume recovery, exec, on-resume --no-auto-open -# Let Ghostty confirm the close itself; keep Omarchy's normal close behaviour elsewhere. -unbind = SUPER, W -bindd = SUPER, W, Close window, exec, close-active-window -# Hyprland runs all binds for the same chord in order; unbind clears tiling-v2 SUPER+TAB (next workspace). -unbind = SUPER, TAB -# Unbind Ctrl+Alt+Tab / Ctrl+Alt+Shift+Tab so Ghostty can use them for tmux window switching -# (were: focus next/previous monitor) -unbind = CTRL ALT, TAB -unbind = CTRL ALT SHIFT, TAB -bindd = SUPER, TAB, Workspace relayout, exec, ~/.local/bin/workspace-relayout -# unbind clears tiling-v2 SUPER ALT+TAB (next window in group). -unbind = SUPER ALT, TAB -bindd = SUPER ALT, TAB, Workspace relayout edit, exec, ~/.local/bin/workspace-relayout --edit -bindd = SUPER ALT, W, Workspace menu, exec, workspace-menu -bindd = SUPER ALT, D, Dot dashboard, exec, uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot dashboard -bindd = SUPER CTRL, P, Power Profile, exec, power-profile-menu -bindd = SUPER SHIFT, F, Add floating application, exec, /usr/bin/float-app add - -# Terminal -bindd = SUPER, return, Terminal, exec, $terminal --working-directory=$(omarchy-cmd-terminal-cwd) -bindd = SUPER SHIFT, return, Floating Terminal, exec, uwsm app -- xdg-terminal-exec --app-id=org.omarchy.terminal -bindd = SUPER, Q, Herdr, exec, $terminal -e herdr session attach default -bindd = SUPER SHIFT, Q, Floating Herdr, exec, uwsm app -- xdg-terminal-exec --app-id=org.omarchy.terminal -e herdr session attach default -unbind = CTRL SHIFT, T -bindd = CTRL SHIFT, T, New terminal tab, exec, terminal-tab-action new -unbind = CTRL SHIFT, W -bindd = CTRL SHIFT, W, Close terminal tab, exec, terminal-tab-action close -unbind = CTRL, TAB -bindd = CTRL, TAB, Next terminal tab, exec, terminal-tab-action next -unbind = CTRL SHIFT, TAB -bindd = CTRL SHIFT, TAB, Previous terminal tab, exec, terminal-tab-action previous -bindd = SUPER SHIFT, T, New Ghostty tab, exec, terminal-tab-action ghostty-new - -# File manager -bindd = SUPER, E, File manager, exec, $fileManager - -# Browser -bindd = SUPER, B, Browser, exec, $browserPersonal -bindd = SUPER SHIFT, B, Browser (private), exec, $browserPersonal --private - -# Browser Work -bindd = SUPER ALT, B, Browser Work, exec, $browserWork - -# Slack -bindd = SUPER SHIFT, S, Slack, exec, $slack - -# Music -# bindd = SUPER, M, Music, exec, WINIT_UNIX_BACKEND=wayland GDK_BACKEND=wayland WEBKIT_DISABLE_DMABUF_RENDERER=1 ~/repos/music-assistant/desktop-companion/src-tauri/target/release/music-assistant-companion -bindd = SUPER, M, Music Assistant, exec, omarchy-launch-webapp "http://homeassistant.local:8095" - -bindd = SUPER ALT, N, Notes, exec, uwsm app -- xdg-terminal-exec --app-id=TUI.float -e notes --all - -# 1Password -bindd = SUPER SHIFT, SLASH, Passwords, exec, uwsm app -- 1password - -# YouTube -bindd = SUPER, Y, YouTube, exec, omarchy-launch-webapp "https://www.youtube.com/feed/subscriptions" - -# Twitter -bindd = SUPER, X, X, exec, omarchy-launch-webapp "https://twitter.com/notifications" -bindd = SUPER SHIFT, X, X Post, exec, omarchy-launch-webapp "https://x.com/compose/post" - -# Twitch -bindd = SUPER ALT, T, Twitch, exec, omarchy-launch-webapp "https://twitch.tv/directory/following/live" - -# GitHub -bindd = SUPER ALT, G, GitHub Notifications, exec, omarchy-launch-webapp "https://github.com/notifications" - -# Discord -bindd = SUPER, D, Discord, exec, $discord - -# Home Assistant -bindd = SUPER, H, Home Assistant, exec, omarchy-launch-webapp "http://homeassistant.local:8123" -bindd = SUPER ALT, H, Handoffs, exec, uwsm app -- xdg-terminal-exec --app-id=TUI.float -e notes handoffs --all - -# Home Assistant Assist (Borderless Chrome window) -bindd = SUPER, A, Home Assistant Assist, exec, omarchy-launch-webapp "http://homeassistant.local:8123/?conversation=1" - -# In a call -bind = SUPER SHIFT, C, exec, timmo-run-command go-automate ha ib t in_a_call - -# Mic On -bind = SUPER SHIFT, M, exec, pactl set-source-mute @DEFAULT_SOURCE@ toggle - -# Twitch notifications menu -bind = CTRL ALT, T, exec, ~/.local/bin/twitch-menu -bind = CTRL ALT SHIFT, T, exec, ~/.local/bin/twitch-menu channels - -# Git diff -bind = CTRL ALT, R, exec, uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot tui git-diff -bind = CTRL ALT SHIFT, R, exec, uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot tui git-diff --tab other - -# Times -# Overrides Omarchy's default SUPER+CTRL+ALT+T single local time notification. -unbind = SUPER CTRL ALT, T -bindd = SUPER CTRL ALT, T, Show times, exec, omarchy notification send "" "Times" "$(~/.local/bin/times --notify)" -u low - -# Precise window resizing (fractional, like 1% volume with ALT) -bindd = SUPER ALT, code:20, Shrink window width (precise), resizeactive, -2 0 # ALT + - -bindd = SUPER ALT, code:21, Expand window width (precise), resizeactive, 2 0 # ALT + = -bindd = SUPER ALT SHIFT, code:20, Shrink window height (precise), resizeactive, 0 -2 -bindd = SUPER ALT SHIFT, code:21, Expand window height (precise), resizeactive, 0 2 - -# Host-specific additions selected by dot via ~/.config/hypr/host. -# hyprlang noerror true -source = ~/.config/hypr/host/bindings.conf -# hyprlang noerror false diff --git a/hypr/.config/hypr/bindings.lua b/hypr/.config/hypr/bindings.lua new file mode 100644 index 00000000..f83f2580 --- /dev/null +++ b/hypr/.config/hypr/bindings.lua @@ -0,0 +1,119 @@ +-- Keep only your personal keybinding overrides here. Add new bindings or +-- unbind defaults before replacing them. + +-- See current bindings and descriptions: +-- omarchy menu keybindings --print + +-- To disable every Omarchy default binding, set this in +-- ~/.config/hypr/hyprland.lua before require("default.hypr.omarchy"), then add +-- only the bindings you want below: +-- omarchy_default_bindings = false + +-- To disable all preinstalled app/webapp bindings, set: +-- omarchy_preinstalled_bindings = false + +-- Add a new binding. +-- o.bind("SUPER + SHIFT + R", "SSH", "alacritty -e ssh your-server") + +-- Change an existing binding by unbinding it first, then binding the key again. +-- This example changes SUPER+SPACE from the launcher to the Omarchy root menu. +-- hl.unbind("SUPER + SPACE") +-- o.bind("SUPER + SPACE", "Omarchy menu", "omarchy-menu toggle root") + +-- Disable a default binding without replacing it. +-- hl.unbind("SUPER + SHIFT + B") + +-- Logitech MX Keys examples: +-- o.bind("SUPER + SHIFT + S", nil, "omarchy-capture-screenshot") +-- o.bind("SUPER + H", nil, "voxtype record toggle") +-- o.bind("SUPER + PERIOD", nil, "omarchy-shell shell toggle omarchy.emojis") + +-- Screen recording +o.bind("SHIFT + ALT + PRINT", "Screenrecording", "omarchy screenrecord") + +local terminal = "uwsm app -- ghostty-host-config" +local file_manager = "uwsm app -- thunar" +local browser_personal = [[uwsm app -- chromium --new-window --ozone-platform=wayland --profile-directory="Default" --force-device-scale-factor=0.8]] +local browser_work = "launch-work-browser" +local discord = "launch-work-discord" +local slack = "launch-work-slack" + +-- Resume recovery +o.bind("SUPER + SHIFT + R", "Resume recovery", "on-resume --no-auto-open") + +-- Hyprland runs all binds for the same chord in order; unbind clears default bindings first. +hl.unbind("SUPER + TAB") +hl.unbind("CTRL + ALT + TAB") +hl.unbind("CTRL + ALT + SHIFT + TAB") +-- unbind clears tiling-v2 SUPER + ALT + TAB (next window in group). +hl.unbind("SUPER + ALT + TAB") +o.bind("SUPER + TAB", "Workspace relayout", "~/.local/bin/workspace-relayout") +o.bind("SUPER + ALT + TAB", "Workspace relayout edit", "~/.local/bin/workspace-relayout --edit") +o.bind("SUPER + ALT + W", "Workspace menu", "workspace-menu") +o.bind("SUPER + ALT + D", "Dot dashboard", "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot dashboard") +o.bind("SUPER + CTRL + ALT + P", "Power Profile", "power-profile-menu") +hl.unbind("SUPER + SHIFT + F") +o.bind("SUPER + SHIFT + F", "Add floating application", "/usr/bin/float-app add") + +-- Terminal +hl.unbind("SUPER + RETURN") +o.bind("SUPER + RETURN", "Terminal", terminal .. " --working-directory=$(omarchy-cmd-terminal-cwd)") +o.bind("SUPER + Q", "Herdr", terminal .. " -e herdr session attach default") +o.bind("SUPER + SHIFT + Q", "Floating Herdr", "uwsm app -- xdg-terminal-exec --app-id=org.omarchy.terminal -e herdr session attach default") +hl.unbind("CTRL + SHIFT + T") +o.bind("CTRL + SHIFT + T", "New terminal tab", "terminal-tab-action new") +hl.unbind("CTRL + SHIFT + W") +o.bind("CTRL + SHIFT + W", "Close terminal tab", "terminal-tab-action close") +hl.unbind("CTRL + TAB") +o.bind("CTRL + TAB", "Next terminal tab", "terminal-tab-action next") +hl.unbind("CTRL + SHIFT + TAB") +o.bind("CTRL + SHIFT + TAB", "Previous terminal tab", "terminal-tab-action previous") +o.bind("SUPER + SHIFT + T", "New Ghostty tab", "terminal-tab-action ghostty-new") + +-- File manager +o.bind("SUPER + E", "File manager", file_manager) + +-- Browser +o.bind("SUPER + B", "Browser", browser_personal) +hl.unbind("SUPER + SHIFT + B") +o.bind("SUPER + SHIFT + B", "Browser (private)", browser_personal .. " --private") +o.bind("SUPER + ALT + B", "Browser Work", browser_work) + +-- Chat and apps +o.bind("SUPER + CTRL + SHIFT + S", "Slack", slack) +o.bind("SUPER + M", "Music Assistant", [[omarchy-launch-webapp "http://homeassistant.local:8095"]]) +o.bind("SUPER + ALT + N", "Notes", "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e notes --all") +o.bind("SUPER + Y", "YouTube", [[omarchy-launch-webapp "https://www.youtube.com/feed/subscriptions"]]) +o.bind("SUPER + ALT + X", "X Notifications", [[omarchy-launch-webapp "https://twitter.com/notifications"]]) +o.bind("SUPER + ALT + T", "Twitch", [[omarchy-launch-webapp "https://twitch.tv/directory/following/live"]]) +o.bind("SUPER + CTRL + ALT + G", "GitHub Notifications", [[omarchy-launch-webapp "https://github.com/notifications"]]) +o.bind("SUPER + D", "Discord", discord) + +-- Home Assistant +o.bind("SUPER + H", "Home Assistant", [[omarchy-launch-webapp "http://homeassistant.local:8123"]]) +o.bind("SUPER + ALT + H", "Handoffs", "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e notes handoffs --all") +o.bind("SUPER + A", "Home Assistant Assist", [[omarchy-launch-webapp "http://homeassistant.local:8123/?conversation=1"]]) + +-- Local automations +o.bind("SUPER + CTRL + SHIFT + C", nil, "timmo-run-command go-automate ha ib t in_a_call") +o.bind("SUPER + CTRL + SHIFT + M", nil, "pactl set-source-mute @DEFAULT_SOURCE@ toggle") +o.bind("CTRL + ALT + T", nil, "~/.local/bin/twitch-menu") +o.bind("CTRL + ALT + SHIFT + T", nil, "~/.local/bin/twitch-menu channels") +o.bind("CTRL + ALT + R", nil, "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot tui git-diff") +o.bind("CTRL + ALT + SHIFT + R", nil, "uwsm app -- xdg-terminal-exec --app-id=TUI.float -e dot tui git-diff --tab other") + +-- Overrides Omarchy's default SUPER+CTRL+ALT+T single local time notification. +hl.unbind("SUPER + CTRL + ALT + T") +o.bind("SUPER + CTRL + ALT + T", "Show times", [[omarchy notification send "" "Times" "$(~/.local/bin/times --notify)" -u low]]) + +-- Precise window resizing (fractional, like 1% volume with ALT) +hl.unbind("SUPER + ALT + code:20") +hl.unbind("SUPER + ALT + code:21") +hl.unbind("SUPER + SHIFT + ALT + code:20") +hl.unbind("SUPER + SHIFT + ALT + code:21") +o.bind("SUPER + ALT + code:20", "Shrink window width (precise)", hl.dsp.window.resize({ x = -2, y = 0, relative = true })) +o.bind("SUPER + ALT + code:21", "Expand window width (precise)", hl.dsp.window.resize({ x = 2, y = 0, relative = true })) +o.bind("SUPER + ALT + SHIFT + code:20", "Shrink window height (precise)", hl.dsp.window.resize({ x = 0, y = -2, relative = true })) +o.bind("SUPER + ALT + SHIFT + code:21", "Expand window height (precise)", hl.dsp.window.resize({ x = 0, y = 2, relative = true })) + +require("hypr.host.bindings") diff --git a/hypr/.config/hypr/envs.conf b/hypr/.config/hypr/envs.conf deleted file mode 100644 index fb283323..00000000 --- a/hypr/.config/hypr/envs.conf +++ /dev/null @@ -1,22 +0,0 @@ -# Extra env variables -# Note: You must relaunch Hyprland after changing envs (use Super+Esc, then Relaunch) -# env = MY_GLOBAL_ENV,setting - -# Wayland -env = ELECTRON_ENABLE_WAYLAND,1 - -# GPU and VA-API env is host-specific: Nvidia on desktop, Intel iHD on laptop. -# Set per host below via ~/.config/hypr/host/envs.conf. - -# Cursor -env = HYPRCURSOR_THEME,catppuccin-mocha-dark-cursors -env = XCURSOR_SIZE,20 -env = HYPRCURSOR_SIZE,20 - -# Open every Plannotator surface in its dedicated Chromium window. -env = PLANNOTATOR_BROWSER,plannotator-browser - -# Host marker selected by dot via ~/.config/hypr/host. -# hyprlang noerror true -source = ~/.config/hypr/host/envs.conf -# hyprlang noerror false diff --git a/hypr/.config/hypr/envs.lua b/hypr/.config/hypr/envs.lua new file mode 100644 index 00000000..a6743069 --- /dev/null +++ b/hypr/.config/hypr/envs.lua @@ -0,0 +1,15 @@ +-- Wayland +hl.env("ELECTRON_ENABLE_WAYLAND", "1") + +-- GPU and VA-API env is host-specific: Nvidia on desktop, Intel iHD on laptop. +-- Set per host in hosts//envs.lua, loaded via require("hypr.host.envs") below. + +-- Cursor +hl.env("HYPRCURSOR_THEME", "catppuccin-mocha-dark-cursors") +hl.env("XCURSOR_SIZE", "20") +hl.env("HYPRCURSOR_SIZE", "20") + +-- Open every Plannotator surface in its dedicated Chromium window. +hl.env("PLANNOTATOR_BROWSER", "plannotator-browser") + +require("hypr.host.envs") diff --git a/hypr/.config/hypr/float-app.conf b/hypr/.config/hypr/float-app.conf deleted file mode 100644 index 466bd095..00000000 --- a/hypr/.config/hypr/float-app.conf +++ /dev/null @@ -1,9 +0,0 @@ -# Generated by float-app. Do not edit by hand. -windowrule = float on, match:tag floating-window -windowrule = center on, match:tag floating-window -windowrule = size 875 600, match:tag floating-window -windowrule = tag +floating-window, match:class ^chrome-capture\.notes\.timmo\.dev__-Default$ -windowrule = tag +floating-window, match:class ^org\.kde\.kdeconnect\.app$ -windowrule = tag +floating-window, match:class ^org\.omarchy\.terminal$ -windowrule = tag +floating-window, match:class ^com\.vysp3r\.ProtonPlus$ -windowrule = tag +floating-window, match:class ^music-assistant-companion$ diff --git a/hypr/.config/hypr/float-app.lua b/hypr/.config/hypr/float-app.lua new file mode 100644 index 00000000..d79d801b --- /dev/null +++ b/hypr/.config/hypr/float-app.lua @@ -0,0 +1,6 @@ +-- Generated by float-app. Do not edit by hand. +hl.window_rule({ match = { class = "^chrome-capture\\.notes\\.timmo\\.dev__-Default$" }, tag = "+floating-window" }) +hl.window_rule({ match = { class = "^chrome-beta\\.floatplane\\.com__post_CRPhG7Qjyb-Default$" }, tag = "+floating-window" }) +hl.window_rule({ match = { class = "^music-assistant-companion$" }, tag = "+floating-window" }) +hl.window_rule({ match = { class = "^org\\.kde\\.kdeconnect\\.app$" }, tag = "+floating-window" }) +hl.window_rule({ match = { class = "^com\\.vysp3r\\.ProtonPlus$" }, tag = "+floating-window" }) diff --git a/hypr/.config/hypr/hosts/desktop/autostart.conf b/hypr/.config/hypr/hosts/desktop/autostart.conf deleted file mode 100644 index 46166825..00000000 --- a/hypr/.config/hypr/hosts/desktop/autostart.conf +++ /dev/null @@ -1,6 +0,0 @@ -# Desktop autostart additions. -exec-once = solaar --window=hide -exec-once = opencode-server - -# Workspace layouts -exec-once = workspace-setup --sleep=5 diff --git a/hypr/.config/hypr/hosts/desktop/autostart.lua b/hypr/.config/hypr/hosts/desktop/autostart.lua new file mode 100644 index 00000000..1a94f21c --- /dev/null +++ b/hypr/.config/hypr/hosts/desktop/autostart.lua @@ -0,0 +1,5 @@ +o.exec_on_start("solaar --window=hide") +o.exec_on_start("opencode-server") + +-- Workspace layouts +o.exec_on_start("workspace-setup --sleep=5") diff --git a/hypr/.config/hypr/hosts/desktop/bindings.conf b/hypr/.config/hypr/hosts/desktop/bindings.conf deleted file mode 100644 index 276f88a4..00000000 --- a/hypr/.config/hypr/hosts/desktop/bindings.conf +++ /dev/null @@ -1,5 +0,0 @@ -# Home Assistant Development Terminal -bindd = SUPER SHIFT, H, Home Assistant DEV Terminal, exec, omarchy-launch-tui hadev - -# Doorbell camera popup (permanent floating) -bindd = SUPER ALT, C, Doorbell Camera Popup, exec, mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"; nohup setsid ~/.config/dotfiles/scripts/.local/bin/doorbell-popup --open-only --no-auto-close --monitor DP-1 >"${XDG_STATE_HOME:-$HOME/.local/state}/doorbell-camera-popup.log" 2>&1 < /dev/null & diff --git a/hypr/.config/hypr/hosts/desktop/bindings.lua b/hypr/.config/hypr/hosts/desktop/bindings.lua new file mode 100644 index 00000000..0dff318f --- /dev/null +++ b/hypr/.config/hypr/hosts/desktop/bindings.lua @@ -0,0 +1,4 @@ +o.bind("SUPER + SHIFT + H", "Home Assistant DEV Terminal", "omarchy-launch-tui hadev") + +-- Local automations +o.bind("SUPER + ALT + C", "Doorbell Camera Popup", [[mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"; nohup setsid ~/.config/dotfiles/scripts/.local/bin/doorbell-popup --open-only --no-auto-close --monitor DP-1 >"${XDG_STATE_HOME:-$HOME/.local/state}/doorbell-camera-popup.log" 2>&1 < /dev/null &]]) diff --git a/hypr/.config/hypr/hosts/desktop/envs.conf b/hypr/.config/hypr/hosts/desktop/envs.conf deleted file mode 100644 index 1d32773b..00000000 --- a/hypr/.config/hypr/hosts/desktop/envs.conf +++ /dev/null @@ -1,8 +0,0 @@ -# Desktop host marker. -env = OMARCHY_HOST,desktop - -# Nvidia + Wayland (desktop has an Nvidia GPU). -env = __GLX_VENDOR_LIBRARY_NAME,nvidia -env = __NV_PRIME_RENDER_OFFLOAD,1 -env = LIBVA_DRIVER_NAME,nvidia -env = NVD_BACKEND,direct diff --git a/hypr/.config/hypr/hosts/desktop/envs.lua b/hypr/.config/hypr/hosts/desktop/envs.lua new file mode 100644 index 00000000..8b010e3f --- /dev/null +++ b/hypr/.config/hypr/hosts/desktop/envs.lua @@ -0,0 +1,7 @@ +hl.env("OMARCHY_HOST", "desktop") + +-- Nvidia + Wayland (desktop has an Nvidia GPU). +hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") +hl.env("__NV_PRIME_RENDER_OFFLOAD", "1") +hl.env("LIBVA_DRIVER_NAME", "nvidia") +hl.env("NVD_BACKEND", "direct") diff --git a/hypr/.config/hypr/hosts/desktop/hypridle.conf b/hypr/.config/hypr/hosts/desktop/hypridle.conf deleted file mode 100644 index c124dd18..00000000 --- a/hypr/.config/hypr/hosts/desktop/hypridle.conf +++ /dev/null @@ -1,17 +0,0 @@ -general { - lock_cmd = omarchy-system-lock # lock screen and 1password - before_sleep_cmd = OMARCHY_LOCK_ONLY=true omarchy-system-lock # lock before suspend without scheduling display off. - after_sleep_cmd = sleep 1 && omarchy-system-wake && on-resume # delay for PAM readiness, then turn on display. - inhibit_sleep = 3 # wait until screen is locked -} - -listener { - timeout = 1800 # 30min - on-timeout = pidof hyprlock || omarchy-launch-screensaver -} - -listener { - timeout = 3600 # 60min - on-timeout = omarchy-system-lock - on-resume = omarchy-system-wake -} diff --git a/hypr/.config/hypr/hosts/desktop/hyprlock.conf b/hypr/.config/hypr/hosts/desktop/hyprlock.conf deleted file mode 100644 index 9550b656..00000000 --- a/hypr/.config/hypr/hosts/desktop/hyprlock.conf +++ /dev/null @@ -1,43 +0,0 @@ -source = ~/.config/omarchy/current/theme/hyprlock.conf - -general { - ignore_empty_input = true -} - -background { - monitor = - color = $color - path = ~/.config/omarchy/current/background - blur_passes = 3 -} - -animations { - enabled = false -} - -input-field { - monitor = - size = 650, 100 - position = 0, 0 - halign = center - valign = center - - inner_color = $inner_color - outer_color = $outer_color - outline_thickness = 4 - - font_family = JetBrainsMono Nerd Font - font_color = $font_color - - placeholder_text = Enter Password - check_color = $check_color - fail_text = $FAIL ($ATTEMPTS) - - rounding = 0 - shadow_passes = 0 - fade_on_empty = false -} - -auth { - fingerprint:enabled = false -} diff --git a/hypr/.config/hypr/hosts/desktop/input.conf b/hypr/.config/hypr/hosts/desktop/input.conf deleted file mode 100644 index d4b996ba..00000000 --- a/hypr/.config/hypr/hosts/desktop/input.conf +++ /dev/null @@ -1 +0,0 @@ -# Desktop currently uses the shared input defaults. diff --git a/hypr/.config/hypr/hosts/desktop/input.lua b/hypr/.config/hypr/hosts/desktop/input.lua new file mode 100644 index 00000000..6f72da9f --- /dev/null +++ b/hypr/.config/hypr/hosts/desktop/input.lua @@ -0,0 +1,5 @@ +hl.config({ + input = { + sensitivity = 0, + }, +}) diff --git a/hypr/.config/hypr/hosts/desktop/looknfeel.conf b/hypr/.config/hypr/hosts/desktop/looknfeel.conf deleted file mode 100644 index ce67b2e1..00000000 --- a/hypr/.config/hypr/hosts/desktop/looknfeel.conf +++ /dev/null @@ -1,39 +0,0 @@ -# Desktop look overrides. -general { - gaps_in = 2 - col.active_border = rgba(ccccffff) -} - -# https://wiki.hyprland.org/Configuring/Variables/#decoration -decoration { - rounding = 4 - - shadow { - enabled = true - range = 2 - render_power = 3 - color = rgba(1a1a1aee) - } - - # https://wiki.hyprland.org/Configuring/Variables/#blur - blur { - enabled = true - size = 3 - passes = 1 - - vibrancy = 0.1696 - } -} - -# Steam: Omarchy floats all steam windows by default. Override only the main -# client window so login/dialog windows can stay floating while the main Steam -# window tiles to avoid the XWayland menu bug. -windowrule = workspace 2 silent, match:class ^(steam)$ -windowrule = tile on, match:class ^(steam)$, match:title ^Steam$ - -# Workspace rules -windowrule = workspace 1, match:class ^(chromium)$ -windowrule = workspace 1 silent, match:class ^(chrome-discord\.com__app) -windowrule = workspace 1 silent, match:class ^(chrome-app\.slack\.com__client) - -windowrule = workspace 3, match:class ^(work-browser)$ diff --git a/hypr/.config/hypr/hosts/desktop/looknfeel.lua b/hypr/.config/hypr/hosts/desktop/looknfeel.lua new file mode 100644 index 00000000..21866345 --- /dev/null +++ b/hypr/.config/hypr/hosts/desktop/looknfeel.lua @@ -0,0 +1,34 @@ +hl.config({ + general = { + gaps_in = 2, + col = { + active_border = "rgba(ccccffff)", + }, + }, + + decoration = { + rounding = 4, + shadow = { + enabled = true, + range = 2, + render_power = 3, + color = "rgba(1a1a1aee)", + }, + blur = { + enabled = true, + size = 3, + passes = 1, + vibrancy = 0.1696, + }, + }, +}) + +-- Steam: keep the main client tiled while login/dialog windows can stay floating. +hl.window_rule({ name = "workspace-steam", match = { class = "^(steam)$" }, workspace = "2 silent" }) +hl.window_rule({ name = "tile-steam-main", match = { class = "^(steam)$", title = "^Steam$" }, float = false }) + +-- Workspace rules. +hl.window_rule({ name = "workspace-chromium", match = { class = "^(chromium)$" }, workspace = "1" }) +hl.window_rule({ name = "workspace-discord", match = { class = "^(chrome-discord\\.com__app)" }, workspace = "1 silent" }) +hl.window_rule({ name = "workspace-slack", match = { class = "^(chrome-app\\.slack\\.com__client)" }, workspace = "1 silent" }) +hl.window_rule({ name = "workspace-work-browser", match = { class = "^(work-browser)$" }, workspace = "3" }) diff --git a/hypr/.config/hypr/hosts/desktop/monitors.conf b/hypr/.config/hypr/hosts/desktop/monitors.conf deleted file mode 100644 index 70ce6f0b..00000000 --- a/hypr/.config/hypr/hosts/desktop/monitors.conf +++ /dev/null @@ -1,19 +0,0 @@ -# See https://wiki.hyprland.org/Configuring/Monitors/ -# List current monitors and resolutions possible: hyprctl monitors -# Format: monitor = [port], resolution, position, scale -# You must relaunch Hyprland after changing any envs (use Super+Esc, then Relaunch) - -env = GDK_SCALE,1.6 -env = QT_SCALE_FACTOR,1.6 - -monitor=DP-1, 3840x2160@143.86Hz, -1350x0, 1.6, transform, 1 -monitor=HDMI-A-2, 3840x2160@240.00Hz, 0x504, 1.6 -monitor=Virtual-1, 3840x2160@60Hz, 0x0, 1.6 -monitor=, preferred, auto, 1.6 - -workspace = 1, name:Main Left, monitor:DP-1, default:true -workspace = 2, name:Main Right A, monitor:HDMI-A-2, default:true -workspace = 3, name:Main Right B, monitor:HDMI-A-2, default:false -workspace = 4, name:Main Right C, monitor:HDMI-A-2, default:false -workspace = 5, name:Main Right D, monitor:HDMI-A-2, default:false -workspace = 6, name:Main Right E, monitor:HDMI-A-2, default:false diff --git a/hypr/.config/hypr/hosts/desktop/monitors.lua b/hypr/.config/hypr/hosts/desktop/monitors.lua new file mode 100644 index 00000000..037b925b --- /dev/null +++ b/hypr/.config/hypr/hosts/desktop/monitors.lua @@ -0,0 +1,20 @@ +-- May want to use 1.666667 (both) +local machine = require("hypr.lib.machine") + +local is_virtual_machine = machine.is_virtual_machine() +local omarchy_gdk_scale = is_virtual_machine and 1 or 1.6 +local omarchy_monitor_scale = is_virtual_machine and 1.0 or 1.6 + +hl.env("GDK_SCALE", tostring(omarchy_gdk_scale)) +hl.env("QT_SCALE_FACTOR", tostring(omarchy_gdk_scale)) + +hl.monitor({ output = "DP-1", mode = "3840x2160@143.86Hz", position = "-1350x0", scale = omarchy_monitor_scale, transform = 1 }) +hl.monitor({ output = "HDMI-A-2", mode = "3840x2160@240.00Hz", position = "0x504", scale = omarchy_monitor_scale }) +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) + +hl.workspace_rule({ workspace = "1", default_name = "Main Left", monitor = "DP-1", default = true }) +hl.workspace_rule({ workspace = "2", default_name = "Main Right A", monitor = "HDMI-A-2", default = true }) +hl.workspace_rule({ workspace = "3", default_name = "Main Right B", monitor = "HDMI-A-2" }) +hl.workspace_rule({ workspace = "4", default_name = "Main Right C", monitor = "HDMI-A-2" }) +hl.workspace_rule({ workspace = "5", default_name = "Main Right D", monitor = "HDMI-A-2" }) +hl.workspace_rule({ workspace = "6", default_name = "Main Right E", monitor = "HDMI-A-2" }) diff --git a/hypr/.config/hypr/hosts/laptop/autostart.conf b/hypr/.config/hypr/hosts/laptop/autostart.conf deleted file mode 100644 index f8beecb1..00000000 --- a/hypr/.config/hypr/hosts/laptop/autostart.conf +++ /dev/null @@ -1,3 +0,0 @@ -# Laptop autostart additions. -exec-once = uwsm app -- hyprsunset --config ~/.config/hypr/host/hyprsunset.conf -exec-once = uwsm app -- power-profile-daemon diff --git a/hypr/.config/hypr/hosts/laptop/autostart.lua b/hypr/.config/hypr/hosts/laptop/autostart.lua new file mode 100644 index 00000000..6e51be6e --- /dev/null +++ b/hypr/.config/hypr/hosts/laptop/autostart.lua @@ -0,0 +1 @@ +o.exec_on_start("uwsm app -- power-profile-daemon") diff --git a/hypr/.config/hypr/hosts/laptop/bindings.conf b/hypr/.config/hypr/hosts/laptop/bindings.conf deleted file mode 100644 index 889cb656..00000000 --- a/hypr/.config/hypr/hosts/laptop/bindings.conf +++ /dev/null @@ -1,20 +0,0 @@ -# Bluetooth -bindd = SUPER ALT CTRL, B, Bluetooth, exec, DEVICE="$(bluetoothctl devices | grep -i 'MOMENTUM 4' | awk '{print $2}')" && bluetoothctl disconnect "$DEVICE" && sleep 1 && bluetoothctl connect "$DEVICE" - -# Doorbell camera popup (permanent floating) -bindd = SUPER ALT, C, Doorbell Camera Popup, exec, mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"; nohup setsid ~/.config/dotfiles/scripts/.local/bin/doorbell-popup --open-only --no-auto-close --camera-entity camera.front_door_snapshot --monitor eDP-1 --width 380 --height 450 >"${XDG_STATE_HOME:-$HOME/.local/state}/doorbell-camera-popup.log" 2>&1 < /dev/null & - -# Gamma-only dimming toggle -bindd = SUPER CTRL, D, Toggle dimming, exec, ~/.config/hypr/bin/hyprsunset-toggle-dim -bindd = SUPER CTRL, minus, Dim down, exec, ~/.config/hypr/bin/hyprsunset-dim-step down -bindd = SUPER CTRL, equal, Dim up, exec, ~/.config/hypr/bin/hyprsunset-dim-step up - -# Dim control with Ctrl + Fn brightness keys -bindeld = CTRL, XF86MonBrightnessUp, Dim up, exec, ~/.config/hypr/bin/hyprsunset-dim-step up -bindeld = CTRL, XF86MonBrightnessDown, Dim down, exec, ~/.config/hypr/bin/hyprsunset-dim-step down - -# Normal brightness disables dim -unbind = , XF86MonBrightnessUp -unbind = , XF86MonBrightnessDown -bindeld = , XF86MonBrightnessUp, Brightness up, exec, ~/.config/hypr/bin/hyprsunset-clear-dim && omarchy-brightness-display +5% -bindeld = , XF86MonBrightnessDown, Brightness down, exec, ~/.config/hypr/bin/hyprsunset-clear-dim && omarchy-brightness-display 5%- diff --git a/hypr/.config/hypr/hosts/laptop/bindings.lua b/hypr/.config/hypr/hosts/laptop/bindings.lua new file mode 100644 index 00000000..cf713eb9 --- /dev/null +++ b/hypr/.config/hypr/hosts/laptop/bindings.lua @@ -0,0 +1,5 @@ +-- Bluetooth +o.bind("SUPER + CTRL + SHIFT + B", "Bluetooth", [[DEVICE="$(bluetoothctl devices | grep -i 'MOMENTUM 4' | awk '{print $2}')" && bluetoothctl disconnect "$DEVICE" && sleep 1 && bluetoothctl connect "$DEVICE"]]) + +-- Local automations +o.bind("SUPER + ALT + C", "Doorbell Camera Popup", [[mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"; nohup setsid ~/.config/dotfiles/scripts/.local/bin/doorbell-popup --open-only --no-auto-close --camera-entity camera.front_door_snapshot --monitor eDP-1 --width 380 --height 450 >"${XDG_STATE_HOME:-$HOME/.local/state}/doorbell-camera-popup.log" 2>&1 < /dev/null &]]) diff --git a/hypr/.config/hypr/hosts/laptop/envs.conf b/hypr/.config/hypr/hosts/laptop/envs.conf deleted file mode 100644 index bebfaf64..00000000 --- a/hypr/.config/hypr/hosts/laptop/envs.conf +++ /dev/null @@ -1,5 +0,0 @@ -# Laptop host marker. -env = OMARCHY_HOST,laptop - -# Intel-only GPU: use the iHD VA-API driver (Mesa EGL is the default). -env = LIBVA_DRIVER_NAME,iHD diff --git a/hypr/.config/hypr/hosts/laptop/envs.lua b/hypr/.config/hypr/hosts/laptop/envs.lua new file mode 100644 index 00000000..bd54c58b --- /dev/null +++ b/hypr/.config/hypr/hosts/laptop/envs.lua @@ -0,0 +1,4 @@ +hl.env("OMARCHY_HOST", "laptop") + +-- Intel-only GPU: use the iHD VA-API driver (Mesa EGL is the default). +hl.env("LIBVA_DRIVER_NAME", "iHD") diff --git a/hypr/.config/hypr/hosts/laptop/hypridle.conf b/hypr/.config/hypr/hosts/laptop/hypridle.conf deleted file mode 100644 index 8f378e24..00000000 --- a/hypr/.config/hypr/hosts/laptop/hypridle.conf +++ /dev/null @@ -1,19 +0,0 @@ -general { - lock_cmd = omarchy-system-lock # lock screen and 1password - before_sleep_cmd = OMARCHY_LOCK_ONLY=true omarchy-system-lock # lock before suspend without scheduling display off. - after_sleep_cmd = sleep 1 && omarchy-system-wake && kbd-backlight-rearm && on-resume # delay for PAM readiness, turn on display, re-arm keyboard backlight. - inhibit_sleep = 3 # wait until screen is locked -} - -# Start screensaver after 2.5 minutes -listener { - timeout = 150 - on-timeout = pidof hyprlock || omarchy-launch-screensaver -} - -# Lock system after 5 minutes (screensaver resets idle timer, so have to just do half + 2s margin) -listener { - timeout = 152 - on-timeout = omarchy-system-lock - on-resume = omarchy-system-wake && kbd-backlight-rearm -} diff --git a/hypr/.config/hypr/hosts/laptop/hyprlock.conf b/hypr/.config/hypr/hosts/laptop/hyprlock.conf deleted file mode 100644 index 8bf34063..00000000 --- a/hypr/.config/hypr/hosts/laptop/hyprlock.conf +++ /dev/null @@ -1,44 +0,0 @@ -source = ~/.config/omarchy/current/theme/hyprlock.conf - -general { - ignore_empty_input = true -} - -background { - monitor = - color = $color - path = ~/.config/omarchy/current/background - blur_passes = 3 -} - -animations { - enabled = false -} - -input-field { - monitor = - size = 650, 100 - position = 0, 0 - halign = center - valign = center - - inner_color = $inner_color - outer_color = $outer_color - outline_thickness = 4 - - font_family = CaskaydiaCove Nerd Font - font_color = $font_color - - placeholder_text = Touch Security Key or Type Password - check_color = $check_color - check_text = Touch your security key - fail_text = $FAIL ($ATTEMPTS) - - rounding = 0 - shadow_passes = 0 - fade_on_empty = false -} - -auth { - fingerprint:enabled = false -} diff --git a/hypr/.config/hypr/hosts/laptop/hyprsunset.conf b/hypr/.config/hypr/hosts/laptop/hyprsunset.conf deleted file mode 100644 index 4e78ded7..00000000 --- a/hypr/.config/hypr/hosts/laptop/hyprsunset.conf +++ /dev/null @@ -1,27 +0,0 @@ -# Makes hyprsunset do nothing to the screen by default -# Without this, the default applies some tint to the monitor -# profile { -# time = 07:00 -# identity = true -# } - -# To enable auto switch to nightlight, set in your .config/hypr/autostart: -# exec-once = uwsm app -- hyprsunset -# and use the following: -# profile { -# time = 20:00 -# temperature = 4000 -# } - -# Gamma-only dimming profile (no color temperature shift) -# max-gamma = 150 -# profile { -# time = 20:00 -# identity = true -# gamma = 0.8 -# } -# profile { -# time = 07:00 -# identity = true -# gamma = 1.0 -# } diff --git a/hypr/.config/hypr/hosts/laptop/input.conf b/hypr/.config/hypr/hosts/laptop/input.conf deleted file mode 100644 index 9dbbbad7..00000000 --- a/hypr/.config/hypr/hosts/laptop/input.conf +++ /dev/null @@ -1,23 +0,0 @@ -# Laptop touchpad and pointer overrides. -input { - sensitivity = 0.35 - - touchpad { - # Disable touchpad when typing - disable_while_typing = true - - # Use natural (inverse) scrolling - natural_scroll = true - - # Use two-finger clicks for right-click instead of lower-right corner - # clickfinger_behavior = true - - # Control the speed of your scrolling - scroll_factor = 0.4 - } -} - -# Scroll speed adjustments -windowrule = match:class Alacritty, scroll_touchpad 1.50 -windowrule = match:class Ghostty, scroll_touchpad 1.50 -windowrule = match:class ^(Chromium|chromium|google-chrome|google-chrome-stable|google-chrome-unstable)$, scroll_touchpad 0.25 diff --git a/hypr/.config/hypr/hosts/laptop/input.lua b/hypr/.config/hypr/hosts/laptop/input.lua new file mode 100644 index 00000000..b7a8a414 --- /dev/null +++ b/hypr/.config/hypr/hosts/laptop/input.lua @@ -0,0 +1,13 @@ +hl.config({ + input = { + sensitivity = 0.35, + touchpad = { + scroll_factor = 0.4, + }, + }, +}) + +-- Scroll speed adjustments +hl.window_rule({ name = "scroll-alacritty", match = { class = "Alacritty" }, scroll_touchpad = 1.50 }) +hl.window_rule({ name = "scroll-ghostty", match = { class = "Ghostty" }, scroll_touchpad = 1.50 }) +hl.window_rule({ name = "scroll-chromium", match = { class = "^(Chromium|chromium|google-chrome|google-chrome-stable|google-chrome-unstable)$" }, scroll_touchpad = 0.25 }) diff --git a/hypr/.config/hypr/hosts/laptop/looknfeel.conf b/hypr/.config/hypr/hosts/laptop/looknfeel.conf deleted file mode 100644 index 1e40694a..00000000 --- a/hypr/.config/hypr/hosts/laptop/looknfeel.conf +++ /dev/null @@ -1,13 +0,0 @@ -# Laptop look overrides. -general { - gaps_in = 0 - col.active_border = rgba(cccccc66) - col.inactive_border = rgba(59595966) -} - -decoration { - rounding = 1 -} - -# Smaller floating terminal for SUPER+SHIFT+Q launcher -windowrule = size 760 500, match:class ^org\.omarchy\.terminal$ diff --git a/hypr/.config/hypr/hosts/laptop/looknfeel.lua b/hypr/.config/hypr/hosts/laptop/looknfeel.lua new file mode 100644 index 00000000..5359cd4f --- /dev/null +++ b/hypr/.config/hypr/hosts/laptop/looknfeel.lua @@ -0,0 +1,15 @@ +hl.config({ + general = { + gaps_in = 0, + col = { + active_border = "rgba(cccccc66)", + inactive_border = "rgba(59595966)", + }, + }, + decoration = { + rounding = 1, + }, +}) + +-- Smaller floating terminal for SUPER+SHIFT+Q launcher +hl.window_rule({ name = "floating-terminal-size", match = { class = "^org\\.omarchy\\.terminal$" }, size = "760 500" }) diff --git a/hypr/.config/hypr/hosts/laptop/monitors.conf b/hypr/.config/hypr/hosts/laptop/monitors.conf deleted file mode 100644 index bce59c20..00000000 --- a/hypr/.config/hypr/hosts/laptop/monitors.conf +++ /dev/null @@ -1,24 +0,0 @@ -# See https://wiki.hyprland.org/Configuring/Monitors/ -# List current monitors and resolutions possible: hyprctl monitors -# Format: monitor = [port], resolution, position, scale -# You must relaunch Hyprland after changing any envs (use Super+Esc, then Relaunch) - -# Optimized for retina-class 2x displays, like 13" 2.8K, 27" 5K, 32" 6K. -# env = GDK_SCALE,2 -# monitor=,preferred,auto,auto - -# Good compromise for 27" or 32" 4K monitors (but fractional!) -# env = GDK_SCALE,1.75 -# monitor=,preferred,auto,1.666667 - -# Straight 1x setup for low-resolution displays like 1080p or 1440p -# env = GDK_SCALE,1 -# monitor=,preferred,auto,1 - -# Example for Framework 13 w/ 6K XDR Apple display -# monitor = DP-5, 6016x3384@60, auto, 2 -# monitor = eDP-1, 2880x1920@120, auto, 2 - -env = GDK_SCALE,2 -env = QT_SCALE_FACTOR,2.0 -monitor=,preferred,auto,2.0 diff --git a/hypr/.config/hypr/hosts/laptop/monitors.lua b/hypr/.config/hypr/hosts/laptop/monitors.lua new file mode 100644 index 00000000..0958150b --- /dev/null +++ b/hypr/.config/hypr/hosts/laptop/monitors.lua @@ -0,0 +1,10 @@ +local machine = require("hypr.lib.machine") + +local is_virtual_machine = machine.is_virtual_machine() +local omarchy_gdk_scale = is_virtual_machine and 1 or 2 +local omarchy_monitor_scale = is_virtual_machine and 1.0 or 2.0 + +hl.env("GDK_SCALE", tostring(omarchy_gdk_scale)) +hl.env("QT_SCALE_FACTOR", tostring(omarchy_gdk_scale)) + +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) diff --git a/hypr/.config/hypr/hypridle.conf b/hypr/.config/hypr/hypridle.conf deleted file mode 100644 index 31a8d71a..00000000 --- a/hypr/.config/hypr/hypridle.conf +++ /dev/null @@ -1,2 +0,0 @@ -# Host-specific override selected by dot via ~/.config/hypr/host. -source = ~/.config/hypr/host/hypridle.conf diff --git a/hypr/.config/hypr/hyprland.conf b/hypr/.config/hypr/hyprland.conf deleted file mode 100644 index 52f1fb9a..00000000 --- a/hypr/.config/hypr/hyprland.conf +++ /dev/null @@ -1,28 +0,0 @@ -# Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/ - -# Use defaults Omarchy defaults (but don't edit these directly!) -source = ~/.local/share/omarchy/default/hypr/autostart.conf -source = ~/.local/share/omarchy/default/hypr/bindings/media.conf -source = ~/.local/share/omarchy/default/hypr/bindings/clipboard.conf -source = ~/.local/share/omarchy/default/hypr/bindings/tiling-v2.conf -source = ~/.local/share/omarchy/default/hypr/bindings/utilities.conf -source = ~/.local/share/omarchy/default/hypr/envs.conf -source = ~/.local/share/omarchy/default/hypr/looknfeel.conf -source = ~/.local/share/omarchy/default/hypr/input.conf -source = ~/.local/share/omarchy/default/hypr/windows.conf -source = ~/.config/omarchy/current/theme/hyprland.conf - -# Change your own setup in these files (and overwrite any settings from defaults!) -# envs first so env vars (Nvidia, cursor theme) are set before monitors/autostart apps launch -source = ~/.config/hypr/envs.conf -source = ~/.config/hypr/monitors.conf -source = ~/.config/hypr/input.conf -source = ~/.config/hypr/bindings.conf -source = ~/.config/hypr/looknfeel.conf -source = ~/.config/hypr/autostart.conf - -# Toggle config flags dynamically -source = ~/.local/state/omarchy/toggles/hypr/*.conf - -# Add any other personal Hyprland configuration below -# windowrule = workspace 5, match:class qemu diff --git a/hypr/.config/hypr/hyprland.lua b/hypr/.config/hypr/hyprland.lua new file mode 100644 index 00000000..7374097f --- /dev/null +++ b/hypr/.config/hypr/hyprland.lua @@ -0,0 +1,31 @@ +-- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/ + +-- Omarchy's bootstrap keeps path setup out of this user config. +dofile((os.getenv("OMARCHY_PATH") or "/usr/share/omarchy") .. "/default/hypr/bootstrap.lua") + +-- Disable all Omarchy default bindings. Add your own in hypr/bindings.lua. +-- omarchy_default_bindings = false +-- +-- Or disable only bindings for Omarchy's preinstalled apps/web apps while +-- keeping core window-manager bindings: +-- omarchy_preinstalled_bindings = false + +-- Load Omarchy defaults. +require("default.hypr.omarchy") + +-- Put your personal overrides in these files. They're loaded after Omarchy's +-- defaults so package updates can improve the defaults without rewriting your +-- ~/.config/hypr files. +-- envs first so env vars (Nvidia, cursor theme) are set before monitors/autostart apps launch +require("hypr.envs") +require("hypr.monitors") +require("hypr.input") +require("hypr.bindings") +require("hypr.looknfeel") +require("hypr.autostart") + +-- Toggle config flags dynamically. +require("default.hypr.toggles") + +-- Add any other personal Hyprland configuration below. +-- o.window("qemu", { workspace = "5" }) diff --git a/hypr/.config/hypr/hyprlock.conf b/hypr/.config/hypr/hyprlock.conf deleted file mode 100644 index dd2436c7..00000000 --- a/hypr/.config/hypr/hyprlock.conf +++ /dev/null @@ -1,2 +0,0 @@ -# Host-specific override selected by dot via ~/.config/hypr/host. -source = ~/.config/hypr/host/hyprlock.conf diff --git a/hypr/.config/hypr/hyprsunset.conf b/hypr/.config/hypr/hyprsunset.conf index 1827445d..c4d0f8d3 100644 --- a/hypr/.config/hypr/hyprsunset.conf +++ b/hypr/.config/hypr/hyprsunset.conf @@ -1,6 +1,14 @@ # Makes hyprsunset do nothing to the screen by default # Without this, the default applies some tint to the monitor profile { - time = 00:00 + time = 07:00 identity = true -} \ No newline at end of file +} + +# To enable auto switch to nightlight, set in your .config/hypr/autostart: +# exec-once = uwsm app -- hyprsunset +# and use the following: +# profile { +# time = 20:00 +# temperature = 4000 +# } diff --git a/hypr/.config/hypr/input.conf b/hypr/.config/hypr/input.conf deleted file mode 100644 index 94185ee1..00000000 --- a/hypr/.config/hypr/input.conf +++ /dev/null @@ -1,21 +0,0 @@ -# Control your input devices -# See https://wiki.hypr.land/Configuring/Variables/#input -input { - kb_layout = gb - kb_options = compose:caps # ,grp:alt_space_toggle - - # Change speed of keyboard repeat - repeat_rate = 40 - repeat_delay = 600 - - # Increase sensitity for mouse/trackpad (default: 0) - sensitivity = 0 - - # Set the acceleration profile (adaptive, flat, custom, unset for libinput defaults) - accel_profile = flat -} - -# Host-specific input additions selected by dot via ~/.config/hypr/host. -# hyprlang noerror true -source = ~/.config/hypr/host/input.conf -# hyprlang noerror false diff --git a/hypr/.config/hypr/input.lua b/hypr/.config/hypr/input.lua new file mode 100644 index 00000000..79f31ebd --- /dev/null +++ b/hypr/.config/hypr/input.lua @@ -0,0 +1,76 @@ +-- Keep only your personal input overrides here. Uncommented settings below +-- replace Omarchy's defaults. + +-- Keyboard layout and options. +-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input +-- hl.config({ +-- input = { +-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt. +-- kb_layout = "us,dk,eu", +-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle", +-- +-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards). +-- kb_variant = "intl", +-- +-- -- Change speed of keyboard repeat. +-- repeat_rate = 40, +-- repeat_delay = 250, +-- +-- -- Start with numlock on by default. +-- numlock_by_default = true, +-- +-- -- Increase sensitivity for mouse/trackpad (default: 0). +-- sensitivity = 0.35, +-- +-- -- Turn off mouse acceleration (default: adaptive). +-- accel_profile = "flat", +-- +-- touchpad = { +-- -- Use natural (inverse) scrolling. +-- natural_scroll = true, +-- +-- -- Use two-finger clicks for right-click instead of lower-right corner. +-- clickfinger_behavior = true, +-- +-- -- Control the speed of your scrolling. +-- scroll_factor = 0.4, +-- +-- -- Enable the touchpad while typing. +-- disable_while_typing = false, +-- +-- -- Left-click-and-drag with three fingers. +-- drag_3fg = 1, +-- }, +-- }, +-- }) + +-- App-specific touchpad scroll speeds. +-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 }) +-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 }) + +-- Enable touchpad gestures for changing workspaces. +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/ +-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" }) + +-- Enable touchpad gestures for moving focus (helpful on scrolling layout). +-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end }) +-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end }) + +hl.config({ + input = { + kb_layout = "gb", + kb_options = "compose:caps", + repeat_rate = 40, + repeat_delay = 600, + sensitivity = 0, + numlock_by_default = true, + accel_profile = "flat", + touchpad = { + disable_while_typing = true, + natural_scroll = true, + clickfinger_behavior = true, + }, + }, +}) + +require("hypr.host.input") diff --git a/hypr/.config/hypr/lib/machine.lua b/hypr/.config/hypr/lib/machine.lua new file mode 100644 index 00000000..571fe8c8 --- /dev/null +++ b/hypr/.config/hypr/lib/machine.lua @@ -0,0 +1,34 @@ +local M = {} + +local function read_file(path) + local file = io.open(path, "r") + if not file then return "" end + + local value = file:read("*a") or "" + file:close() + + return value:lower() +end + +local function contains_virtual_machine_marker(value) + return value:find("qemu", 1, true) + or value:find("kvm", 1, true) + or value:find("virtualbox", 1, true) + or value:find("vmware", 1, true) + or value:find("parallels", 1, true) + or value:find("hyper%-v") + or value:find("bochs", 1, true) +end + +function M.is_virtual_machine() + local dmi = table.concat({ + read_file("/sys/class/dmi/id/sys_vendor"), + read_file("/sys/class/dmi/id/product_name"), + read_file("/sys/class/dmi/id/board_vendor"), + read_file("/sys/class/dmi/id/board_name"), + }, "\n") + + return contains_virtual_machine_marker(dmi) ~= nil +end + +return M diff --git a/hypr/.config/hypr/looknfeel.conf b/hypr/.config/hypr/looknfeel.conf deleted file mode 100644 index 1e8dc3c7..00000000 --- a/hypr/.config/hypr/looknfeel.conf +++ /dev/null @@ -1,39 +0,0 @@ -# https://wiki.hypr.land/Configuring/Basics/Variables/#general -# Only our overrides; everything else inherited from default/hypr/looknfeel.conf -general { - gaps_out = 0 - border_size = 1 - resize_on_border = true -} - -# Smart gaps -workspace = w[tv1], gapsout:0, gapsin:0 -workspace = f[1], gapsout:0, gapsin:0 -windowrule = border_size 0, rounding 0, match:float 0, match:workspace w[tv1] -windowrule = border_size 0, rounding 0, match:float 0, match:workspace f[1] - -# Chrome opacity (patch for ~/.local/share/omarchy/default/hypr/apps/chromium.conf) -windowrule = tag +chromium-based-browser, match:class (google-)?[cC]hrom(e|ium)(-stable|-unstable)?|[bB]rave-browser|Microsoft-edge|Vivaldi-stable -windowrule = opacity 1 1, match:tag chromium-based-browser - -# Video websites -windowrule = opacity 1 1, match:initial_title .*twitch\.tv.* -windowrule = opacity 1 1, match:initial_title .*youtube\.com.* -windowrule = opacity 1 1, match:initial_title .*corridordigital\.com.* -windowrule = opacity 1 1, match:initial_title .*floatplane\.com.* -windowrule = opacity 1 1, match:initial_title .*vivaplus\.tv.* -windowrule = opacity 1 1, match:title .*Home Assistant.* -windowrule = opacity 1 1, match:class ^virt-manager$ - -# Shared workspace rules -windowrule = workspace 4, match:class ^(BambuStudio|OrcaSlicer)$ -windowrule = workspace 2 silent, match:class ^plannotator$ - -# Host-specific look and window rules selected by dot via ~/.config/hypr/host. -# hyprlang noerror true -source = ~/.config/hypr/host/looknfeel.conf -# hyprlang noerror false - -# float-app rules: start -source = /home/aidan/.config/hypr/float-app.conf -# float-app rules: end diff --git a/hypr/.config/hypr/looknfeel.lua b/hypr/.config/hypr/looknfeel.lua new file mode 100644 index 00000000..886168e3 --- /dev/null +++ b/hypr/.config/hypr/looknfeel.lua @@ -0,0 +1,92 @@ +-- Change the default Omarchy look'n'feel. + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#general +-- hl.config({ +-- general = { +-- -- No gaps between windows or borders. +-- gaps_in = 0, +-- gaps_out = 0, +-- border_size = 0, +-- +-- -- Change to niri-like side-scrolling layout. +-- layout = "scrolling", +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#decoration +-- hl.config({ +-- decoration = { +-- -- Use round window corners. +-- rounding = 8, +-- +-- -- Dim unfocused windows (0.0 = no dim, 1.0 = fully dimmed). +-- dim_inactive = true, +-- dim_strength = 0.15, +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#animations +-- hl.config({ +-- animations = { +-- -- Disable all animations. +-- enabled = false, +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#layout +-- hl.config({ +-- layout = { +-- -- Avoid overly wide single-window layouts on wide screens. +-- single_window_aspect_ratio = { 1, 1 }, +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ +-- hl.config({ +-- scrolling = { +-- -- See only one column per screen instead of two. +-- column_width = 0.97, +-- }, +-- }) + +hl.config({ + general = { + gaps_out = 0, + border_size = 1, + col = { + inactive_border = "rgba(595959aa)", + }, + resize_on_border = true, + allow_tearing = false, + layout = "dwindle", + }, +}) + +-- Smart gaps +hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 }) +hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 }) +hl.window_rule({ name = "smart-gaps-wtv1", match = { float = false, workspace = "w[tv1]" }, border_size = 0, rounding = 0 }) +hl.window_rule({ name = "smart-gaps-f1", match = { float = false, workspace = "f[1]" }, border_size = 0, rounding = 0 }) + +-- Chrome opacity override. +hl.window_rule({ name = "tag-chromium-based-browser", match = { class = "(google-)?[cC]hrom(e|ium)(-stable|-unstable)?|[bB]rave-browser|Microsoft-edge|Vivaldi-stable" }, tag = "+chromium-based-browser" }) +hl.window_rule({ name = "opaque-chromium-based-browser", match = { tag = "chromium-based-browser" }, opacity = "1 1" }) + +-- Video websites and Home Assistant should be opaque. +hl.window_rule({ name = "opaque-twitch", match = { initial_title = ".*twitch\\.tv.*" }, opacity = "1 1" }) +hl.window_rule({ name = "opaque-youtube", match = { initial_title = ".*youtube\\.com.*" }, opacity = "1 1" }) +hl.window_rule({ name = "opaque-corridor", match = { initial_title = ".*corridordigital\\.com.*" }, opacity = "1 1" }) +hl.window_rule({ name = "opaque-floatplane", match = { initial_title = ".*floatplane\\.com.*" }, opacity = "1 1" }) +hl.window_rule({ name = "opaque-vivaplus", match = { initial_title = ".*vivaplus\\.tv.*" }, opacity = "1 1" }) +hl.window_rule({ name = "opaque-home-assistant", match = { title = ".*Home Assistant.*" }, opacity = "1 1" }) +hl.window_rule({ name = "opaque-virt-manager", match = { class = "^virt-manager$" }, opacity = "1 1" }) + +-- Shared workspace rules. +hl.window_rule({ name = "workspace-slicers", match = { class = "^(BambuStudio|OrcaSlicer)$" }, workspace = "4" }) +hl.window_rule({ name = "workspace-plannotator", match = { class = "^plannotator$" }, workspace = "2 silent" }) + +require("hypr.host.looknfeel") + +-- float-app rules: start +require("float-app") +-- float-app rules: end diff --git a/hypr/.config/hypr/monitors.conf b/hypr/.config/hypr/monitors.conf deleted file mode 100644 index a1860e69..00000000 --- a/hypr/.config/hypr/monitors.conf +++ /dev/null @@ -1,4 +0,0 @@ -# Host-specific override selected by dot via ~/.config/hypr/host. -# hyprlang noerror true -source = ~/.config/hypr/host/monitors.conf -# hyprlang noerror false diff --git a/hypr/.config/hypr/monitors.lua b/hypr/.config/hypr/monitors.lua new file mode 100644 index 00000000..00fbd86f --- /dev/null +++ b/hypr/.config/hypr/monitors.lua @@ -0,0 +1,10 @@ +-- See https://wiki.hypr.land/Configuring/Basics/Monitors/ +-- List current monitors and supported resolutions with: hyprctl monitors all + +-- Configure a specific monitor. +-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 }) + +-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°). +-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 }) + +require("hypr.host.monitors") diff --git a/hypr/.config/hypr/xdph.conf b/hypr/.config/hypr/xdph.conf index 46226a2a..63b66be1 100644 --- a/hypr/.config/hypr/xdph.conf +++ b/hypr/.config/hypr/xdph.conf @@ -1,4 +1,4 @@ screencopy { - custom_picker_binary = hyprland-preview-share-picker allow_token_by_default = true + custom_picker_binary = hyprland-preview-share-picker } diff --git a/mise.toml b/mise.toml index 176c883b..371796e3 100644 --- a/mise.toml +++ b/mise.toml @@ -52,10 +52,10 @@ bun test tests/opencode bash tests/github/opencode-publish.test.sh bash tests/scripts/update.test.sh bash tests/scripts/browser-control-extension-sync.test.sh +bash tests/scripts/mise.test.sh bash tests/scripts/git-default-ref.test.sh bash tests/scripts/repo-shortcuts.test.sh bash tests/scripts/terminal-tab-action.test.sh -bash tests/scripts/package-updates-bar.test.sh bash tests/scripts/workspace-relayout.test.sh bash tests/scripts/workspace-restore.test.sh """ diff --git a/mise/.config/mise/config.toml b/mise/.config/mise/config.toml index 3c9d8007..11b699ee 100644 --- a/mise/.config/mise/config.toml +++ b/mise/.config/mise/config.toml @@ -44,6 +44,10 @@ uv = "0.12.3" version = "1.18.16" minimum_release_age = "2h" +[tools."aqua:earendil-works/pi"] +version = "0.84.1" +minimum_release_age = "2h" + [tools."github:backnotprop/plannotator"] version = "0.26.8" minimum_release_age = "2h" diff --git a/omarchy/.config/omarchy/plugins/timmo.command/Widget.qml b/omarchy/.config/omarchy/plugins/timmo.command/Widget.qml new file mode 100644 index 00000000..597228f3 --- /dev/null +++ b/omarchy/.config/omarchy/plugins/timmo.command/Widget.qml @@ -0,0 +1,203 @@ +// timmo.command — generic polling command bar widget. +// +// Runs `exec` on an interval and renders the resulting status-bar JSON +// (text/tooltip/class) via a WidgetButton, mapping the parsed class to a +// colour and hiding on configured classes. The Waybar custom/* equivalent +// for the Omarchy 4 Quickshell bar. +// +// Per-instance settings (inline on the shell.json bar layout entry): +// run Shell command to run (its stdout is parsed) +// interval Poll interval in milliseconds (default 60000) +// returnType "json" (text/tooltip/class) or "text" (default "json") +// tooltip Whether to show the JSON tooltip (default true) +// onClick Command run on left click +// onClickRight Command run on right click +// onMiddleClick Command run on middle click +// classColors Map of class name -> colour string +// hideClasses Array of class names that hide the widget +// refreshTarget Optional IPC target id exposing a refresh() method +// loadingText Placeholder shown while (re)loading (e.g. "\uf418 ..") +// loadingClass Class used to colour loadingText (e.g. "dots-unknown") +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Ui + +BarWidget { + id: root + + readonly property string exec: setting("run", "") + readonly property int intervalMs: setting("interval", 60000) + readonly property string returnType: setting("returnType", "json") + readonly property bool tooltipEnabled: setting("tooltip", true) + readonly property string onClickCmd: setting("onClick", "") + readonly property string onClickRightCmd: setting("onClickRight", "") + readonly property string onMiddleClickCmd: setting("onMiddleClick", "") + readonly property var classColors: setting("classColors", ({})) + readonly property var hideClasses: setting("hideClasses", []) + readonly property string refreshTarget: setting("refreshTarget", "") + readonly property string loadingText: setting("loadingText", "") + readonly property string loadingClass: setting("loadingClass", "") + readonly property bool revealOnHover: setting("revealOnHover", false) + // Horizontal cell margin, standard 8px across all custom widgets (center, + // right HA, left). The built-in right-side stock widgets keep their own + // margins. Per-instance overridable via the `horizontalMargin` setting. + readonly property real cellMargin: setting("horizontalMargin", 8) + + property string outText: "" + property string outTooltip: "" + property string outClass: "" + property bool loading: false + + readonly property bool hiddenByClass: { + var cls = root.outClass + for (var i = 0; i < root.hideClasses.length; i++) { + if (root.hideClasses[i] === cls) return true + } + return false + } + + // When this is a center widget (revealOnHover, set by the generator) and the + // center cluster is hovered, reveal an otherwise class-hidden module dimmed, + // mirroring the idle indicators. Needs real text (the icon) to show. + readonly property bool hoverRevealed: root.revealOnHover + && root.hiddenByClass + && root.outText !== "" + && !!root.bar + && root.bar.centerSectionRevealHeld === true + + // Whether this widget has anything to draw: the loading placeholder, a + // non-empty value that is not hidden by class, or a class-hidden value + // revealed by hovering the center cluster. Mirrors the WidgetButton `text` + // binding below. Drives `visible`/`implicitWidth` so a hidden module + // collapses to zero width and the bar reflows, instead of reserving the + // button's minimum width as an empty gap. + readonly property bool shown: (root.loading && root.loadingText !== "") + || (!root.hiddenByClass && root.outText !== "") + || root.hoverRevealed + + // Background poll (timer): only show the loading placeholder on a cold + // start when there is no value yet, so warm polls update smoothly. + function poll() { + if (root.exec === "") return + if (root.outText === "" && root.loadingText !== "") root.loading = true + if (!proc.running) proc.running = true + } + + // Explicit refresh (IPC / resume): always show the loading placeholder so + // the reload is visible, like the legacy grey "loading" state. + function refresh() { + if (root.exec === "") return + if (root.loadingText !== "") root.loading = true + if (!proc.running) proc.running = true + } + + function refreshMatchingTarget(target) { + if (root.refreshTarget === target) root.refresh() + } + + function broadcastRefresh() { + var items = root.bar && typeof root.bar.moduleWidgets === "function" + ? root.bar.moduleWidgets(root.moduleName) : [root] + for (var i = 0; i < items.length; i++) { + if (items[i] && typeof items[i].refreshMatchingTarget === "function") { + items[i].refreshMatchingTarget(root.refreshTarget) + } + } + } + + function applyOutput(raw) { + var trimmed = (raw || "").trim() + if (trimmed === "") { + root.outText = "" + root.outTooltip = "" + root.outClass = "" + return + } + if (root.returnType === "json") { + try { + var obj = JSON.parse(trimmed) + root.outText = obj.text !== undefined && obj.text !== null ? String(obj.text) : "" + root.outTooltip = obj.tooltip !== undefined && obj.tooltip !== null ? String(obj.tooltip) : "" + if (obj["class"] !== undefined && obj["class"] !== null) root.outClass = String(obj["class"]) + else if (obj.alt !== undefined && obj.alt !== null) root.outClass = String(obj.alt) + else root.outClass = "" + return + } catch (e) { + // Not JSON — fall through to plain text. + } + } + root.outText = trimmed + root.outTooltip = "" + root.outClass = "" + } + + function colorForClass(cls) { + if (cls && root.classColors && root.classColors[cls]) return root.classColors[cls] + return root.bar ? root.bar.barForeground : Color.foreground + } + + visible: root.shown + implicitWidth: root.shown ? button.implicitWidth : 0 + implicitHeight: button.implicitHeight + + Process { + id: proc + command: ["bash", "-lc", root.exec] + stdout: StdioCollector { + id: outCollector + waitForEnd: true + } + onExited: function (exitCode) { + root.applyOutput(outCollector.text) + root.loading = false + } + } + + Timer { + interval: Math.max(1000, root.intervalMs) + running: root.exec !== "" + repeat: true + triggeredOnStart: true + onTriggered: root.poll() + } + + Loader { + active: root.refreshTarget !== "" + sourceComponent: Component { + IpcHandler { + target: root.refreshTarget + function refresh(): void { + root.broadcastRefresh() + } + } + } + } + + WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + // Match the stock right-side indicators (audio/network/tray), which render + // at caption size. The clock/weather sit at body, but their Weather-Icons + // and digit glyphs are visually lighter than the Material Design / Font + // Awesome icons these modules use, so caption keeps the icons in step. + fontSize: Style.font.caption + horizontalMargin: root.cellMargin + text: root.loading && root.loadingText !== "" ? root.loadingText : ((root.hiddenByClass && !root.hoverRevealed) ? "" : root.outText) + dimmed: root.hoverRevealed + tooltipText: root.tooltipEnabled ? root.outTooltip : "" + foreground: root.loading && root.loadingText !== "" ? root.colorForClass(root.loadingClass) : root.colorForClass(root.outClass) + onPressed: function (b) { + if (!root.bar) return + if (b === Qt.RightButton) { + if (root.onClickRightCmd !== "") root.bar.run(root.onClickRightCmd) + } else if (b === Qt.MiddleButton) { + if (root.onMiddleClickCmd !== "") root.bar.run(root.onMiddleClickCmd) + } else { + if (root.onClickCmd !== "") root.bar.run(root.onClickCmd) + } + } + } +} diff --git a/omarchy/.config/omarchy/plugins/timmo.command/manifest.json b/omarchy/.config/omarchy/plugins/timmo.command/manifest.json new file mode 100644 index 00000000..ccce89ac --- /dev/null +++ b/omarchy/.config/omarchy/plugins/timmo.command/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "timmo.command", + "name": "Command", + "version": "1.0.0", + "author": "Aidan Timson", + "description": "Runs a shell command on an interval and renders its status-bar JSON (text/tooltip/class) output", + "kinds": ["bar-widget"], + "entryPoints": { "barWidget": "Widget.qml" }, + "barWidget": { + "displayName": "Command", + "description": "Polling command module (the Waybar custom/* equivalent)", + "category": "Custom", + "allowMultiple": true + } +} diff --git a/omarchy/.config/omarchy/plugins/timmo.stream-command/Widget.qml b/omarchy/.config/omarchy/plugins/timmo.stream-command/Widget.qml new file mode 100644 index 00000000..5cadb382 --- /dev/null +++ b/omarchy/.config/omarchy/plugins/timmo.stream-command/Widget.qml @@ -0,0 +1,145 @@ +// timmo.stream-command — streaming command bar widget. +// +// Runs a long-running `exec` that emits newline-delimited status-bar JSON +// (text/tooltip/class) and renders the most recent line. Auto-restarts the +// process after it exits. Used for Home Assistant watchers (ha-watch-singleton +// / ha-module-bar doorbell) that the Omarchy 4 bar cannot drive as one-shot +// polling command modules. +// +// Per-instance settings (inline on the shell.json bar layout entry): +// run Long-running command emitting JSON lines on stdout +// tooltip Whether to show the JSON tooltip (default true) +// onClick Command run on left click +// onClickRight Command run on right click +// onMiddleClick Command run on middle click +// classColors Map of class name -> colour string +// hideClasses Array of class names that hide the widget +// restartInterval Delay before restarting after exit, ms (default 5000) +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Ui + +BarWidget { + id: root + + readonly property string exec: setting("run", "") + readonly property bool tooltipEnabled: setting("tooltip", true) + readonly property string onClickCmd: setting("onClick", "") + readonly property string onClickRightCmd: setting("onClickRight", "") + readonly property string onMiddleClickCmd: setting("onMiddleClick", "") + readonly property var classColors: setting("classColors", ({})) + readonly property var hideClasses: setting("hideClasses", []) + readonly property int restartDelayMs: setting("restartInterval", 5000) + readonly property bool revealOnHover: setting("revealOnHover", false) + // Horizontal cell margin, standard 8px across all custom widgets (center, + // right HA, left). The built-in right-side stock widgets keep their own + // margins. Per-instance overridable via the `horizontalMargin` setting. + readonly property real cellMargin: setting("horizontalMargin", 8) + + property string outText: "" + property string outTooltip: "" + property string outClass: "" + + readonly property bool hiddenByClass: { + var cls = root.outClass + for (var i = 0; i < root.hideClasses.length; i++) { + if (root.hideClasses[i] === cls) return true + } + return false + } + + // When this is a center widget (revealOnHover, set by the generator) and the + // center cluster is hovered, reveal an otherwise class-hidden module dimmed, + // mirroring the idle indicators. Needs real text (the icon) to show. + readonly property bool hoverRevealed: root.revealOnHover + && root.hiddenByClass + && root.outText !== "" + && !!root.bar + && root.bar.centerSectionRevealHeld === true + + // Whether this widget has anything to draw: a non-empty value that is not + // hidden by class, or a class-hidden value revealed by hovering the center + // cluster. Mirrors the WidgetButton `text` binding below. Drives + // `visible`/`implicitWidth` so a hidden module collapses to zero width and + // the bar reflows, instead of reserving the button's minimum width as an + // empty gap. + readonly property bool shown: (!root.hiddenByClass && root.outText !== "") + || root.hoverRevealed + + function applyOutput(raw) { + var trimmed = (raw || "").trim() + if (trimmed === "") return + try { + var obj = JSON.parse(trimmed) + root.outText = obj.text !== undefined && obj.text !== null ? String(obj.text) : "" + root.outTooltip = obj.tooltip !== undefined && obj.tooltip !== null ? String(obj.tooltip) : "" + if (obj["class"] !== undefined && obj["class"] !== null) root.outClass = String(obj["class"]) + else if (obj.alt !== undefined && obj.alt !== null) root.outClass = String(obj.alt) + else root.outClass = "" + } catch (e) { + root.outText = trimmed + root.outTooltip = "" + root.outClass = "" + } + } + + function colorForClass(cls) { + if (cls && root.classColors && root.classColors[cls]) return root.classColors[cls] + return root.bar ? root.bar.barForeground : Color.foreground + } + + visible: root.shown + implicitWidth: root.shown ? button.implicitWidth : 0 + implicitHeight: button.implicitHeight + + Process { + id: proc + running: root.exec !== "" + command: ["bash", "-lc", root.exec] + stdout: SplitParser { + onRead: function (line) { + root.applyOutput(line) + } + } + onExited: function (exitCode) { + if (root.exec !== "") restartTimer.start() + } + } + + Timer { + id: restartTimer + interval: Math.max(1000, root.restartDelayMs) + repeat: false + onTriggered: { + if (root.exec !== "" && !proc.running) proc.running = true + } + } + + WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + // Match the stock right-side indicators (audio/network/tray), which render + // at caption size. The clock/weather sit at body, but their Weather-Icons + // and digit glyphs are visually lighter than the Material Design / Font + // Awesome icons these modules use, so caption keeps the icons in step. + fontSize: Style.font.caption + horizontalMargin: root.cellMargin + text: (root.hiddenByClass && !root.hoverRevealed) ? "" : root.outText + dimmed: root.hoverRevealed + tooltipText: root.tooltipEnabled ? root.outTooltip : "" + foreground: root.colorForClass(root.outClass) + onPressed: function (b) { + if (!root.bar) return + if (b === Qt.RightButton) { + if (root.onClickRightCmd !== "") root.bar.run(root.onClickRightCmd) + } else if (b === Qt.MiddleButton) { + if (root.onMiddleClickCmd !== "") root.bar.run(root.onMiddleClickCmd) + } else { + if (root.onClickCmd !== "") root.bar.run(root.onClickCmd) + } + } + } +} diff --git a/omarchy/.config/omarchy/plugins/timmo.stream-command/manifest.json b/omarchy/.config/omarchy/plugins/timmo.stream-command/manifest.json new file mode 100644 index 00000000..aba43324 --- /dev/null +++ b/omarchy/.config/omarchy/plugins/timmo.stream-command/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "timmo.stream-command", + "name": "Stream command", + "version": "1.0.0", + "author": "Aidan Timson", + "description": "Runs a long-running command that streams status-bar JSON lines and renders the latest line", + "kinds": ["bar-widget"], + "entryPoints": { "barWidget": "Widget.qml" }, + "barWidget": { + "displayName": "Stream command", + "description": "Streaming command module for long-running watchers (e.g. ha-watch-singleton)", + "category": "Custom", + "allowMultiple": true + } +} diff --git a/omarchy/.config/omarchy/plugins/timmo.workspaces/Widget.qml b/omarchy/.config/omarchy/plugins/timmo.workspaces/Widget.qml new file mode 100644 index 00000000..9a611b44 --- /dev/null +++ b/omarchy/.config/omarchy/plugins/timmo.workspaces/Widget.qml @@ -0,0 +1,101 @@ +// timmo.workspaces — dynamic Hyprland workspace indicators. +// +// A fork of the stock omarchy.workspaces widget with three differences: +// * No persistent workspaces. The stock widget always seeds 1-5; this one +// shows only workspaces that currently exist (the occupied ones plus the +// focused one), matching the old Waybar behaviour. +// * Every workspace renders as its number. The stock widget swaps the +// focused workspace for a glyph icon; here the focused workspace keeps +// its number. +// * The focused workspace sits at full opacity and the rest are dimmed, +// so the active workspace reads as "number + full opacity". +// +// Per-instance settings (inline on the shell.json bar layout entry): +// activeOpacity Opacity of the focused workspace (default 1.0) +// inactiveOpacity Opacity of the other existing workspaces (default 0.5) +import QtQuick +import QtQuick.Layouts +import Quickshell.Hyprland +import qs.Commons +import qs.Ui + +BarWidget { + id: root + moduleName: "timmo.workspaces" + + readonly property real activeOpacity: setting("activeOpacity", 1.0) + readonly property real inactiveOpacity: setting("inactiveOpacity", 0.5) + + function workspaceById(id) { + var values = Hyprland.workspaces.values + for (var i = 0; i < values.length; i++) { + if (values[i].id === id) return values[i] + } + + return null + } + + // Only workspaces that currently exist: every workspace Hyprland reports + // (occupied ones, and the focused one which always exists) within the 1-10 + // range. No persistent seeding, so empty workspaces collapse away. + function workspaceIds() { + var ids = [] + var values = Hyprland.workspaces.values + + for (var i = 0; i < values.length; i++) { + var id = values[i].id + if (id > 0 && id <= 10 && ids.indexOf(id) === -1) ids.push(id) + } + + // Defensive: the focused workspace always exists in Hyprland, but make + // sure it is present even if the model has not caught up yet. + var focused = Hyprland.focusedWorkspace + if (focused !== null && focused.id > 0 && focused.id <= 10 && ids.indexOf(focused.id) === -1) + ids.push(focused.id) + + ids.sort(function(left, right) { return left - right }) + return ids + } + + function focusWorkspace(id) { + if (!root.bar) return + root.bar.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })")) + } + + // Gap before the first workspace, to separate it from the menu icon on its + // left. Tunable via the `leadingGap` setting (in Style space units). + readonly property real leadingGap: root.vertical ? 0 : Style.spaceReal(setting("leadingGap", 6)) + readonly property real trailingGap: root.vertical ? 0 : Style.spaceReal(1.5) + + implicitWidth: grid.implicitWidth + leadingGap + trailingGap + implicitHeight: grid.implicitHeight + + GridLayout { + id: grid + anchors.fill: parent + anchors.leftMargin: root.leadingGap + anchors.rightMargin: root.trailingGap + columns: root.vertical ? 1 : Math.max(1, root.workspaceIds().length) + columnSpacing: root.vertical ? 0 : Style.space(1) + rowSpacing: root.vertical ? Style.space(2) : 0 + + Repeater { + model: root.workspaceIds() + + WidgetButton { + required property int modelData + + readonly property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData + + bar: root.bar + text: modelData === 10 ? "0" : String(modelData) + opacity: focused ? root.activeOpacity : root.inactiveOpacity + horizontalMargin: 6 + verticalPadding: 6 + fixedWidth: root.vertical ? root.barSize : Style.space(20) + fixedHeight: root.barSize + onPressed: function() { root.focusWorkspace(modelData) } + } + } + } +} diff --git a/omarchy/.config/omarchy/plugins/timmo.workspaces/manifest.json b/omarchy/.config/omarchy/plugins/timmo.workspaces/manifest.json new file mode 100644 index 00000000..0c76f742 --- /dev/null +++ b/omarchy/.config/omarchy/plugins/timmo.workspaces/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "timmo.workspaces", + "name": "Workspaces", + "version": "1.0.0", + "author": "Aidan Timson", + "description": "Workspace number indicators without persistent workspaces; the focused workspace is shown at full opacity and the rest are dimmed", + "kinds": ["bar-widget"], + "entryPoints": { "barWidget": "Widget.qml" }, + "barWidget": { + "displayName": "Workspaces (Timmo)", + "description": "Dynamic workspace numbers: only existing workspaces, focused at full opacity, others dimmed", + "category": "Compositor", + "allowMultiple": false + } +} diff --git a/opencode.json b/opencode.json index 453ff97e..a1f875ba 100644 --- a/opencode.json +++ b/opencode.json @@ -81,8 +81,6 @@ "~/.config/hunk/**": "allow", "~/.config/opencode/**": "allow", "~/.config/hypr/**": "allow", - "~/.config/hypr-desktop/**": "allow", - "~/.config/hypr-laptop/**": "allow", "~/.config/mise/**": "allow", "~/.config/nvim/**": "allow", "~/.config/ripgrep/**": "allow", @@ -90,7 +88,6 @@ "~/.config/systemd/user/**": "allow", "~/.config/tmux/**": "allow", "~/.config/topgrade/**": "allow", - "~/.config/waybar/**": "allow", "~/.config/yazi/**": "allow", "~/.agents/skills/**": "allow", "~/.local/*": "allow", @@ -103,6 +100,7 @@ "~/.local/share/omarchy/*": "allow", "~/.local/share/omarchy/bin/**": "allow", "~/.local/share/opencode/repos/github.com/anomalyco/opencode/**": "allow", + "~/.local/share/omarchy/**": "allow", "~/.local/share/workspace-relayout/**": "allow", "~/.local/share/zsh/*": "allow", "~/.local/share/zsh/site-functions/**": "allow", @@ -125,7 +123,7 @@ }, "go-automate": { "repository": "timmo001/go-automate", - "description": "Use for the go-automate CLI and HA bridge invoked by Waybar, Hypr binds, and dotfiles scripts" + "description": "Use for the go-automate CLI and HA bridge invoked by the Omarchy shell, Hypr binds, and dotfiles scripts" }, "notes": { "repository": "timmo001/notes", @@ -133,8 +131,8 @@ }, "omarchy": { "repository": "basecamp/omarchy", - "branch": "master", - "description": "Use for Omarchy desktop, Hyprland, and Waybar customisation details" + "branch": "quattro", + "description": "Use for Omarchy desktop, Hyprland, and Quickshell shell customisation details" }, "opencode": { "repository": "anomalyco/opencode", @@ -153,6 +151,10 @@ "repository": "anomalyco/opentui", "description": "Use for OpenTUI core, renderables, and keyboard handling when working in dot/" }, + "quickshell": { + "repository": "quickshell-mirror/quickshell", + "description": "Use for upstream Quickshell QML types, WlrLayershell/layer-shell, IpcHandler, reloadable config, and the qs CLI when customising the Omarchy shell bar widgets" + }, "system-bridge": { "repository": "timmo001/system-bridge", "description": "Use for the system-bridge backend autostarted via Hypr and its API/WebSocket behaviour" diff --git a/renovate.json b/renovate.json index c25c01b8..9aee7acd 100644 --- a/renovate.json +++ b/renovate.json @@ -24,6 +24,15 @@ "datasourceTemplate": "github-tags", "versioningTemplate": "semver" }, + { + "description": "Keep the QML syntax proxy in sync with Arch's stable quickshell package", + "customType": "regex", + "managerFilePatterns": ["/^\\.github/workflows/quickshell-lint\\.ya?ml$/"], + "matchStrings": ["QUICKSHELL_VERSION: \"(?[^\"]+)\""], + "depNameTemplate": "arch/quickshell", + "datasourceTemplate": "repology", + "versioningTemplate": "loose" + }, { "customType": "regex", "description": "Update Herdr plugin commits.", @@ -61,10 +70,19 @@ "minimumReleaseAge": null }, { - "description": "Group OpenCode updates with mise's shorter release delay.", - "groupName": "OpenCode", - "matchDepNames": ["aqua:anomalyco/opencode", "anomalyco/opencode"], + "description": "Group OpenCode and Pi updates with mise's shorter release delay.", + "groupName": "OpenCode and Pi", + "matchDepNames": [ + "aqua:anomalyco/opencode", + "anomalyco/opencode", + "aqua:earendil-works/pi" + ], "minimumReleaseAge": "2 hours" + }, + { + "description": "Group the Quickshell lint version updates.", + "groupName": "quickshell lint version", + "matchDepNames": ["arch/quickshell"] } ] } diff --git a/scripts/.local/bin/close-active-window b/scripts/.local/bin/close-active-window deleted file mode 100755 index 0d639db9..00000000 --- a/scripts/.local/bin/close-active-window +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -active_class="$(hyprctl activewindow -j | jq -r '.class // ""')" - -if [[ "$active_class" == "com.mitchellh.ghostty" ]]; then - exec hyprctl dispatch sendshortcut SUPER, W, activewindow -fi - -exec hyprctl dispatch killactive diff --git a/scripts/.local/bin/git-diff-bar b/scripts/.local/bin/git-diff-bar index 2d0913ef..7d2cc732 100755 --- a/scripts/.local/bin/git-diff-bar +++ b/scripts/.local/bin/git-diff-bar @@ -3,20 +3,19 @@ set -euo pipefail DOT_BIN="${DOT_BIN:-$HOME/.config/dotfiles/scripts/.local/bin/dot}" -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/waybar" -CACHE_FILE="$CACHE_DIR/git-diff-waybar.json" -LOCK_DIR="$CACHE_DIR/git-diff-waybar.lock" -REFRESH_SIGNAL="${WAYBAR_GIT_DIFF_SIGNAL:-11}" -REFRESH_TIMEOUT="${WAYBAR_GIT_DIFF_TIMEOUT:-20}" +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/status-bar" +CACHE_FILE="$CACHE_DIR/git-diff.json" +LOCK_DIR="$CACHE_DIR/git-diff.lock" +REFRESH_TIMEOUT="${BAR_GIT_DIFF_TIMEOUT:-20}" loading_json='{"text":" ..","tooltip":"git diff: loading","class":"dots-unknown"}' error_json='{"text":" ?","tooltip":"git diff: unavailable","class":"dots-unknown"}' mkdir -p "$CACHE_DIR" -if [[ "${WAYBAR_GIT_DIFF_REFRESH_DETACHED:-0}" != "1" ]]; then +if [[ "${BAR_GIT_DIFF_REFRESH_DETACHED:-0}" != "1" ]]; then if mkdir "$LOCK_DIR" 2>/dev/null; then - export WAYBAR_GIT_DIFF_REFRESH_DETACHED=1 + export BAR_GIT_DIFF_REFRESH_DETACHED=1 setsid "$0" "$@" >/dev/null 2>&1 & fi @@ -43,7 +42,6 @@ refresh_cache() { tmp_file="$CACHE_FILE.tmp" printf '%s\n' "$rendered_json" >"$tmp_file" mv "$tmp_file" "$CACHE_FILE" - pkill -RTMIN+"$REFRESH_SIGNAL" -x waybar >/dev/null 2>&1 || true } trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT diff --git a/scripts/.local/bin/git-notifications-bar b/scripts/.local/bin/git-notifications-bar index 2e86411e..4f35f1f8 100755 --- a/scripts/.local/bin/git-notifications-bar +++ b/scripts/.local/bin/git-notifications-bar @@ -3,22 +3,17 @@ set -euo pipefail DOT_BIN="${DOT_BIN:-$HOME/.config/dotfiles/scripts/.local/bin/dot}" -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/waybar" -CACHE_FILE="$CACHE_DIR/git-notifications-waybar.json" -LOCK_DIR="$CACHE_DIR/git-notifications-waybar.lock" -REFRESH_SIGNAL="${WAYBAR_GIT_NOTIFICATIONS_SIGNAL:-13}" -REFRESH_TIMEOUT="${WAYBAR_GIT_NOTIFICATIONS_TIMEOUT:-30}" -REFRESH_MIN_AGE="${WAYBAR_GIT_NOTIFICATIONS_MIN_REFRESH_AGE:-30}" +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/status-bar" +CACHE_FILE="$CACHE_DIR/git-notifications.json" +LOCK_DIR="$CACHE_DIR/git-notifications.lock" +REFRESH_TIMEOUT="${BAR_GIT_NOTIFICATIONS_TIMEOUT:-30}" +REFRESH_MIN_AGE="${BAR_GIT_NOTIFICATIONS_MIN_REFRESH_AGE:-30}" loading_json='{"text":" ..","tooltip":"GitHub notifications: loading","class":"notifications-unknown"}' error_json='{"text":" ?","tooltip":"GitHub notifications: unavailable","class":"notifications-unknown"}' mkdir -p "$CACHE_DIR" -signal_waybar_refresh() { - pkill -RTMIN+"$REFRESH_SIGNAL" -x waybar >/dev/null 2>&1 || true -} - open_notifications() { uwsm app -- xdg-terminal-exec --app-id=TUI.float -e "$DOT_BIN" git-notifications --bar-filter >/dev/null 2>&1 & } @@ -45,7 +40,6 @@ refresh_cache() { tmp_file="$CACHE_FILE.tmp" printf '%s\n' "$rendered_json" >"$tmp_file" mv "$tmp_file" "$CACHE_FILE" - signal_waybar_refresh } case "${1:-status}" in @@ -56,9 +50,9 @@ refresh) refresh_cache ;; status) - if [[ "${WAYBAR_GIT_NOTIFICATIONS_REFRESH_DETACHED:-0}" != "1" ]]; then + if [[ "${BAR_GIT_NOTIFICATIONS_REFRESH_DETACHED:-0}" != "1" ]]; then if cache_needs_refresh && mkdir "$LOCK_DIR" 2>/dev/null; then - export WAYBAR_GIT_NOTIFICATIONS_REFRESH_DETACHED=1 + export BAR_GIT_NOTIFICATIONS_REFRESH_DETACHED=1 setsid "$0" >/dev/null 2>&1 & fi diff --git a/scripts/.local/bin/git-workflows-bar b/scripts/.local/bin/git-workflows-bar deleted file mode 100755 index bb801518..00000000 --- a/scripts/.local/bin/git-workflows-bar +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -DOT_BIN="${DOT_BIN:-$HOME/.config/dotfiles/scripts/.local/bin/dot}" -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/waybar" -CACHE_FILE="$CACHE_DIR/git-workflows-waybar.json" -LOCK_DIR="$CACHE_DIR/git-workflows-waybar.lock" -REFRESH_SIGNAL="${WAYBAR_GIT_WORKFLOWS_SIGNAL:-12}" -REFRESH_TIMEOUT="${WAYBAR_GIT_WORKFLOWS_TIMEOUT:-45}" - -loading_json='{"text":"● ..","tooltip":"GitHub workflows: loading","class":"workflows-unknown"}' -error_json='{"text":" ?","tooltip":"GitHub workflows: unavailable","class":"workflows-unknown"}' - -mkdir -p "$CACHE_DIR" - -since_last_hour() { - date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ -} - -signal_waybar_refresh() { - pkill -RTMIN+"$REFRESH_SIGNAL" -x waybar >/dev/null 2>&1 || true -} - -open_workflows() { - local since - since="$(since_last_hour)" - uwsm app -- xdg-terminal-exec --app-id=TUI.float -e "$DOT_BIN" git-workflows --since "$since" >/dev/null 2>&1 & -} - -refresh_cache() { - local since status_json rendered_json tmp_file - since="$(since_last_hour)" - - status_json="$(timeout "$REFRESH_TIMEOUT" "$DOT_BIN" git-workflows --bar-json --since "$since" 2>/dev/null || true)" - if [[ -z "$status_json" ]]; then - rendered_json="$error_json" - else - rendered_json="$status_json" - fi - - tmp_file="$CACHE_FILE.tmp" - printf '%s\n' "$rendered_json" >"$tmp_file" - mv "$tmp_file" "$CACHE_FILE" - signal_waybar_refresh -} - -case "${1:-status}" in -open) - open_workflows - ;; -refresh) - refresh_cache - ;; -status) - if [[ "${WAYBAR_GIT_WORKFLOWS_REFRESH_DETACHED:-0}" != "1" ]]; then - if mkdir "$LOCK_DIR" 2>/dev/null; then - export WAYBAR_GIT_WORKFLOWS_REFRESH_DETACHED=1 - setsid "$0" refresh >/dev/null 2>&1 & - fi - - if [[ -s "$CACHE_FILE" ]]; then - cat "$CACHE_FILE" - else - printf '%s\n' "$loading_json" - fi - exit 0 - fi - trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT - refresh_cache - ;; -*) - printf 'Usage: %s [status|refresh|open]\n' "${0##*/}" >&2 - exit 1 - ;; -esac diff --git a/scripts/.local/bin/ha-module-bar b/scripts/.local/bin/ha-module-bar index e59a68c7..d8b2d475 100755 --- a/scripts/.local/bin/ha-module-bar +++ b/scripts/.local/bin/ha-module-bar @@ -24,8 +24,8 @@ TRIGGER_INITIAL=0 TRIGGER_COOLDOWN=0 TRIGGER_KEY="" -WAYBAR_PARENT_PID="" -WAYBAR_PARENT_STARTTIME="" +BAR_PARENT_PID="" +BAR_PARENT_STARTTIME="" LAST_OUTPUT="" usage() { @@ -348,11 +348,11 @@ emit_if_changed() { } trigger_state_file() { - printf '%s/ha-waybar-trigger-%s.state' "${XDG_RUNTIME_DIR:-/tmp}" "$(sanitize_key "$TRIGGER_KEY")" + printf '%s/ha-bar-trigger-%s.state' "${XDG_RUNTIME_DIR:-/tmp}" "$(sanitize_key "$TRIGGER_KEY")" } trigger_cooldown_file() { - printf '%s/ha-waybar-trigger-%s.last' "${XDG_RUNTIME_DIR:-/tmp}" "$(sanitize_key "$TRIGGER_KEY")" + printf '%s/ha-bar-trigger-%s.last' "${XDG_RUNTIME_DIR:-/tmp}" "$(sanitize_key "$TRIGGER_KEY")" } run_trigger_command() { @@ -418,7 +418,7 @@ maybe_run_trigger() { fi } -find_waybar_ancestor_pid() { +find_bar_ancestor_pid() { local pid="$PPID" local depth=0 local comm="" @@ -426,7 +426,7 @@ find_waybar_ancestor_pid() { while [[ "$pid" =~ ^[0-9]+$ ]] && ((pid > 1)) && ((depth < 8)); do comm="$(awk '{print $2}' "/proc/$pid/stat" 2>/dev/null || true)" - if [[ "$comm" == "(waybar)" ]]; then + if [[ "$comm" == "(quickshell)" || "$comm" == "(waybar)" ]]; then printf '%s' "$pid" return 0 fi @@ -443,12 +443,12 @@ find_waybar_ancestor_pid() { return 1 } -capture_waybar_parent_starttime() { - WAYBAR_PARENT_PID="$(find_waybar_ancestor_pid || printf '%s' "$PPID")" - WAYBAR_PARENT_STARTTIME="$(awk '{print $22}' "/proc/$WAYBAR_PARENT_PID/stat" 2>/dev/null || true)" +capture_bar_parent_starttime() { + BAR_PARENT_PID="$(find_bar_ancestor_pid || printf '%s' "$PPID")" + BAR_PARENT_STARTTIME="$(awk '{print $22}' "/proc/$BAR_PARENT_PID/stat" 2>/dev/null || true)" - if [[ -z "$WAYBAR_PARENT_STARTTIME" ]]; then - printf 'ha-module-bar: unable to read Waybar parent starttime\n' >&2 + if [[ -z "$BAR_PARENT_STARTTIME" ]]; then + printf 'ha-module-bar: unable to read bar host parent starttime\n' >&2 exit 1 fi } @@ -456,6 +456,7 @@ capture_waybar_parent_starttime() { run_temperature() { local line="" local state="" + local text="" line="$(read_entity_line "$ENTITY_ID")" if [[ -z "$line" ]]; then @@ -469,7 +470,13 @@ run_temperature() { return fi - printf '{"text":"%.1f","class":"temperature","tooltip":"%s (%s): %.1f °C"}\n' "$state" "$ENTITY_NAME" "$ENTITY_ID" "$state" + if [[ -n "$ICON" ]]; then + text="$(printf '%s %.1f' "$ICON" "$state")" + else + text="$(printf '%.1f' "$state")" + fi + + printf '{"text":"%s","class":"temperature","tooltip":"%s (%s): %.1f °C"}\n' "$text" "$ENTITY_NAME" "$ENTITY_ID" "$state" maybe_run_trigger "$state" } @@ -702,13 +709,13 @@ run_doorbell_stream() { fi } - capture_waybar_parent_starttime + capture_bar_parent_starttime coproc DOORBELL_STREAM { exec singleton-stream \ --key "$STREAM_KEY" \ - --parent-pid "$WAYBAR_PARENT_PID" \ - --parent-starttime "$WAYBAR_PARENT_STARTTIME" \ + --parent-pid "$BAR_PARENT_PID" \ + --parent-starttime "$BAR_PARENT_STARTTIME" \ -- go-automate ha bridge watch entity --bar-json --icon '' "$ENTITY_ID" } stream_pid="${DOORBELL_STREAM_PID:-}" diff --git a/scripts/.local/bin/ha-watch-singleton b/scripts/.local/bin/ha-watch-singleton index 03b6610c..2a44a983 100755 --- a/scripts/.local/bin/ha-watch-singleton +++ b/scripts/.local/bin/ha-watch-singleton @@ -99,7 +99,9 @@ parse_args() { fi } -find_waybar_ancestor_pid() { +# Walk up the process tree to find the bar host (Quickshell) so the singleton +# stream can be tied to the shell's lifetime. Falls back to the direct parent. +find_bar_ancestor_pid() { local pid="$PPID" local depth=0 local comm="" @@ -107,7 +109,7 @@ find_waybar_ancestor_pid() { while [[ "$pid" =~ ^[0-9]+$ ]] && (( pid > 1 )) && (( depth < 8 )); do comm="$(awk '{print $2}' "/proc/$pid/stat" 2>/dev/null || true)" - if [[ "$comm" == "(waybar)" ]]; then + if [[ "$comm" == "(quickshell)" ]]; then printf '%s' "$pid" return 0 fi @@ -156,8 +158,8 @@ build_watch_command() { } main() { - local waybar_pid="" - local waybar_starttime="" + local bar_pid="" + local bar_starttime="" local stream_key="" parse_args "$@" @@ -167,10 +169,10 @@ main() { exit 1 fi - waybar_pid="$(find_waybar_ancestor_pid || printf '%s' "$PPID")" - waybar_starttime="$(awk '{print $22}' "/proc/$waybar_pid/stat" 2>/dev/null || true)" - if [[ -z "$waybar_starttime" ]]; then - printf 'ha-watch-singleton: unable to read Waybar parent starttime\n' >&2 + bar_pid="$(find_bar_ancestor_pid || printf '%s' "$PPID")" + bar_starttime="$(awk '{print $22}' "/proc/$bar_pid/stat" 2>/dev/null || true)" + if [[ -z "$bar_starttime" ]]; then + printf 'ha-watch-singleton: unable to read bar host parent starttime\n' >&2 exit 1 fi @@ -179,8 +181,8 @@ main() { exec singleton-stream \ --key "$stream_key" \ - --parent-pid "$waybar_pid" \ - --parent-starttime "$waybar_starttime" \ + --parent-pid "$bar_pid" \ + --parent-starttime "$bar_starttime" \ -- "${WATCH_CMD[@]}" } diff --git a/scripts/.local/bin/mise b/scripts/.local/bin/mise new file mode 100755 index 00000000..d47e3757 --- /dev/null +++ b/scripts/.local/bin/mise @@ -0,0 +1,51 @@ +#!/bin/bash + +set -euo pipefail + +self=$(readlink -f "$0") +real_mise="" +while IFS= read -r candidate; do + if [[ $(readlink -f "$candidate") != "$self" ]]; then + real_mise=$candidate + break + fi +done < <(type -aP mise) + +if [[ -z $real_mise ]]; then + echo "mise: could not find the system mise binary" >&2 + exit 127 +fi + +if [[ ${1:-} == --write-global-config ]]; then + shift + exec "$real_mise" "$@" +fi + +redirect_global_write=false +case ${1:-} in + use | unuse | rm) + for arg in "${@:2}"; do + if [[ $arg == -g || $arg == --global ]]; then + redirect_global_write=true + break + fi + done + ;; + settings) + case ${2:-} in + add | set | unset | rm | remove | delete | del) + redirect_global_write=true + ;; + esac + if (( $# >= 3 )) || [[ ${2:-} == *=* ]]; then + redirect_global_write=true + fi + ;; +esac + +if [[ $redirect_global_write == true ]]; then + export MISE_GLOBAL_CONFIG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/mise/omarchy-config.toml" + mkdir -p "$(dirname "$MISE_GLOBAL_CONFIG_FILE")" +fi + +exec "$real_mise" "$@" diff --git a/scripts/.local/bin/on-resume b/scripts/.local/bin/on-resume index 48207cb3..5cc6fd59 100755 --- a/scripts/.local/bin/on-resume +++ b/scripts/.local/bin/on-resume @@ -1,22 +1,22 @@ #!/bin/bash -# Called by hypridle after_sleep_cmd on system resume from sleep. +# Called after system resume from sleep. # Restarts services that don't recover well after suspend. LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/on-resume.log" -CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" -WAYBAR_CACHE_DIR="$CACHE_HOME/waybar" -DOT_FETCH_CACHE_DIR="$CACHE_HOME/dot/fetch-upstream" log() { mkdir -p "$(dirname "$LOG_FILE")" echo "[$(date '+%H:%M:%S')] $1" >> "$LOG_FILE" } -clear_git_caches() { - rm -f "$WAYBAR_CACHE_DIR"/git-*-waybar.json "$WAYBAR_CACHE_DIR"/git-*-waybar.json.tmp - rmdir "$WAYBAR_CACHE_DIR"/git-*-waybar.lock 2>/dev/null || true - rm -rf "$DOT_FETCH_CACHE_DIR" +refresh_shell_indicators() { + omarchy-shell -q omarchy.indicators refresh || true +} + +refresh_git_modules() { + omarchy-shell -q timmo.git-diff refresh || true + omarchy-shell -q timmo.git-notifications refresh || true } if [[ "${ON_RESUME_DETACHED:-0}" != "1" ]]; then @@ -28,6 +28,13 @@ fi : > "$LOG_FILE" log "Resume started" +# Re-arm the laptop keyboard backlight, which the EC can leave at 0 after +# suspend. This is a no-op on machines without a keyboard backlight. +if command -v kbd-backlight-rearm >/dev/null 2>&1; then + kbd-backlight-rearm || true + log "Re-armed keyboard backlight" +fi + # Restart twitch-notifications, auto-opening configured live channels after a real resume. twitch_args=(--restart) launch_args=() @@ -43,14 +50,12 @@ if ! twitch-notifications "${twitch_args[@]}" >/dev/null 2>&1; then fi log "twitch-notifications restart requested" -# Clear git caches and restart waybar so modules refresh after network resume -clear_git_caches -pkill -x waybar >/dev/null 2>&1 || true -sleep 0.2 -pkill -9 -x waybar >/dev/null 2>&1 || true -log "Killed waybar, cleared git caches" +# Ask the shell to repaint state that may have changed while suspended. +refresh_shell_indicators +log "Requested Omarchy shell indicator refresh" -setsid uwsm-app -- waybar >/dev/null 2>&1 & -log "Waybar restarted" +# Reload the polling git status modules so they show fresh state after wake. +refresh_git_modules +log "Requested git status module refresh" log "Resume complete" diff --git a/scripts/.local/bin/on-resume-monitor b/scripts/.local/bin/on-resume-monitor new file mode 100755 index 00000000..af322cb0 --- /dev/null +++ b/scripts/.local/bin/on-resume-monitor @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euo pipefail + +sleep_signal="type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'" +saw_sleep=0 + +while IFS= read -r line; do + case "$line" in + *"boolean true"*) + saw_sleep=1 + ;; + *"boolean false"*) + if (( saw_sleep )); then + sleep 1 + "$HOME/.local/bin/on-resume" + fi + saw_sleep=0 + ;; + esac +done < <(dbus-monitor --system "$sleep_signal") + +# Restart the user service if dbus-monitor exits unexpectedly. +exit 1 diff --git a/scripts/.local/bin/package-updates-bar b/scripts/.local/bin/package-updates-bar index 311aa718..3d573eb8 100755 --- a/scripts/.local/bin/package-updates-bar +++ b/scripts/.local/bin/package-updates-bar @@ -2,20 +2,17 @@ set -euo pipefail -PACKAGE_FILE="${WAYBAR_PACKAGE_UPDATES_FILE:-$HOME/.config/dotfiles/.dot-public-packages}" -CACHE_DIR="${WAYBAR_PACKAGE_UPDATES_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/waybar}" -CACHE_FILE="$CACHE_DIR/package-updates-waybar.json" -LOCK_DIR="$CACHE_DIR/package-updates-waybar.lock" -BACKOFF_FILE="$CACHE_DIR/package-updates-waybar.backoff" -YAY_BIN="${WAYBAR_PACKAGE_UPDATES_YAY_BIN:-yay}" -PACMAN_BIN="${WAYBAR_PACKAGE_UPDATES_PACMAN_BIN:-pacman}" -SETSID_BIN="${WAYBAR_PACKAGE_UPDATES_SETSID_BIN:-setsid}" -PKILL_BIN="${WAYBAR_PACKAGE_UPDATES_PKILL_BIN:-pkill}" -REFRESH_SIGNAL="${WAYBAR_PACKAGE_UPDATES_SIGNAL:-12}" -REFRESH_TIMEOUT="${WAYBAR_PACKAGE_UPDATES_TIMEOUT:-120}" -CACHE_MAX_AGE="${WAYBAR_PACKAGE_UPDATES_CACHE_MAX_AGE:-900}" -BACKOFF_BASE="${WAYBAR_PACKAGE_UPDATES_BACKOFF_BASE:-1800}" -BACKOFF_MAX="${WAYBAR_PACKAGE_UPDATES_BACKOFF_MAX:-21600}" +PACKAGE_FILE="${PACKAGE_UPDATES_FILE:-$HOME/.config/dotfiles/.dot-public-packages}" +CACHE_DIR="${PACKAGE_UPDATES_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/status-bar}" +CACHE_FILE="$CACHE_DIR/package-updates.json" +LOCK_DIR="$CACHE_DIR/package-updates.lock" +BACKOFF_FILE="$CACHE_DIR/package-updates.backoff" +YAY_BIN="${PACKAGE_UPDATES_YAY_BIN:-yay}" +PACMAN_BIN="${PACKAGE_UPDATES_PACMAN_BIN:-pacman}" +REFRESH_TIMEOUT="${PACKAGE_UPDATES_TIMEOUT:-120}" +CACHE_MAX_AGE="${PACKAGE_UPDATES_CACHE_MAX_AGE:-900}" +BACKOFF_BASE="${PACKAGE_UPDATES_BACKOFF_BASE:-1800}" +BACKOFF_MAX="${PACKAGE_UPDATES_BACKOFF_MAX:-21600}" loading_json='{"text":"󰏗 ..","tooltip":"Watched package updates: loading","class":"package-updates-unknown"}' hidden_json='{"text":"","tooltip":"Watched packages are up to date","class":"hidden"}' @@ -23,10 +20,6 @@ error_json='{"text":" ?","tooltip":"Watched package updates unavailable","cla mkdir -p "$CACHE_DIR" -signal_waybar_refresh() { - "$PKILL_BIN" -RTMIN+"$REFRESH_SIGNAL" -x waybar >/dev/null 2>&1 || true -} - refresh_cache() { local repo_output_file aur_output_file aur_error_file aur_status aur_available output rendered_json tmp_file local now backoff_failures=0 backoff_until=0 backoff_seconds @@ -65,7 +58,7 @@ refresh_cache() { if [[ -r "$BACKOFF_FILE" ]]; then read -r backoff_failures backoff_until <"$BACKOFF_FILE" || true fi - if ((now < backoff_until)) && [[ "${WAYBAR_PACKAGE_UPDATES_LOCKED:-0}" == "1" ]]; then + if ((now < backoff_until)) && [[ "${PACKAGE_UPDATES_LOCKED:-0}" == "1" ]]; then aur_status=75 printf 'AUR request backed off until %s\n' "$backoff_until" >"$aur_error_file" else @@ -76,7 +69,7 @@ refresh_cache() { if { ((aur_status == 0 || aur_status == 1)) && [[ ! -s "$aur_error_file" ]]; }; then rm -f "$BACKOFF_FILE" - elif grep -Eiq '(status|HTTP([^0-9]|/[0-9.])*)[^0-9]*(4|5)[0-9]{2}([^0-9]|$)' "$aur_error_file"; then + elif grep -Eiq '(status|HTTP([^0-9]|/[0-9.]*)*)[^0-9]*(4|5)[0-9]{2}([^0-9]|$)' "$aur_error_file"; then ((backoff_failures < 5)) && ((backoff_failures += 1)) backoff_seconds=$((BACKOFF_BASE * (1 << (backoff_failures - 1)))) ((backoff_seconds > BACKOFF_MAX)) && backoff_seconds="$BACKOFF_MAX" @@ -113,12 +106,12 @@ refresh_cache() { tmp_file="$CACHE_FILE.tmp" printf '%s\n' "$rendered_json" >"$tmp_file" mv "$tmp_file" "$CACHE_FILE" - signal_waybar_refresh + omarchy-shell -q timmo.package-updates refresh >/dev/null 2>&1 || true } case "${1:-status}" in refresh) - if [[ "${WAYBAR_PACKAGE_UPDATES_LOCKED:-0}" != "1" ]]; then + if [[ "${PACKAGE_UPDATES_LOCKED:-0}" != "1" ]]; then mkdir "$LOCK_DIR" 2>/dev/null || exit 0 fi trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT @@ -127,7 +120,7 @@ refresh) status) cache_age=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || printf '0'))) if ((cache_age >= CACHE_MAX_AGE)) && mkdir "$LOCK_DIR" 2>/dev/null; then - "$SETSID_BIN" env WAYBAR_PACKAGE_UPDATES_LOCKED=1 "$0" refresh >/dev/null 2>&1 & + setsid env PACKAGE_UPDATES_LOCKED=1 "$0" refresh >/dev/null 2>&1 & fi if [[ -s "$CACHE_FILE" ]]; then diff --git a/scripts/.local/bin/power-profile-menu b/scripts/.local/bin/power-profile-menu index 7aba7934..5b0f7753 100755 --- a/scripts/.local/bin/power-profile-menu +++ b/scripts/.local/bin/power-profile-menu @@ -1,52 +1,40 @@ #!/bin/bash -# Walker power-profile picker that shows the current profile in the title and -# sets the chosen one. Replaces "omarchy menu power", which only sets. +# Power-profile picker: shows the current profile in the prompt and sets the +# chosen one via omarchy-menu-select (Omarchy 4). Replaces "omarchy menu power", +# which only sets and never surfaces the current profile. set -uo pipefail pretty() { case "$1" in - performance) echo "Performance" ;; - balanced) echo "Balanced" ;; - power-saver) echo "Power Saver" ;; + performance) echo 'Performance' ;; + balanced) echo 'Balanced' ;; + power-saver) echo 'Power Saver' ;; *) echo "$1" ;; esac } ugly() { case "$1" in - Performance) echo "performance" ;; - Balanced) echo "balanced" ;; - "Power Saver") echo "power-saver" ;; + Performance) echo 'performance' ;; + Balanced) echo 'balanced' ;; + 'Power Saver') echo 'power-saver' ;; *) echo "$1" ;; esac } -current=$(powerprofilesctl get 2>/dev/null || echo "") -title="Current power profile: $(pretty "$current")" - -# Adaptive width so the placeholder title fits without much excess. -width=$((${#title} * 14 + 40)) -((width < 260)) && width=260 +current="$(powerprofilesctl get 2>/dev/null || echo '')" # Available profiles, newest-first to match Omarchy (power-saver, balanced, performance). mapfile -t ids < <(omarchy-powerprofiles-list 2>/dev/null) [[ ${#ids[@]} -eq 0 ]] && exit 0 -options="" -current_index="" -i=0 +options=() for id in "${ids[@]}"; do - i=$((i + 1)) - options+="$(pretty "$id")"$'\n' - [[ $id == "$current" ]] && current_index=$i + options+=("$(pretty "$id")") done -options=${options%$'\n'} - -args=(--dmenu --width "$width" --minheight 1 --maxheight 630 -p "$title") -[[ -n $current_index ]] && args+=(-c "$current_index") -choice=$(printf '%s' "$options" | omarchy-launch-walker "${args[@]}" 2>/dev/null) || exit 0 +choice="$(omarchy-menu-select "Power profile (now: $(pretty "$current"))" "${options[@]}" -- --width 300 --maxheight 300 2>/dev/null || true)" [[ -z $choice ]] && exit 0 powerprofilesctl set "$(ugly "$choice")" 2>/dev/null || true diff --git a/scripts/.local/bin/twitch-menu b/scripts/.local/bin/twitch-menu index d7e24c2f..98fcb6c5 100755 --- a/scripts/.local/bin/twitch-menu +++ b/scripts/.local/bin/twitch-menu @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -euo pipefail +menu_select() { + local prompt="$1" + local width="$2" + local maxheight="$3" + shift 3 + + omarchy-menu-select "$prompt" "$@" -- --width "$width" --maxheight "$maxheight" 2>/dev/null || true +} + open_url() { local url="${1:?}" local host="${OMARCHY_HOST:-}" @@ -56,7 +65,7 @@ channels_menu() { fi done - choice="$(printf '%s\n' "${options[@]}" | omarchy-launch-walker --dmenu --width 360 --minheight 1 --maxheight 630 -p 'Twitch channels…' 2>/dev/null || true)" + choice="$(menu_select 'Twitch channels' 360 630 "${options[@]}")" choice_label="${choice#* }" [[ $choice_label == 'Open all live autolaunch' ]] && exec twitch-notifications --recheck --open selected="${choice_label% \[offline\]}" @@ -75,7 +84,7 @@ if [[ "${1:-}" == 'channels' ]]; then fi while true; do - choice="$(printf '%s\n' ' Recheck notifications' '󰜉 Restart notifications' ' Open following' ' Open following live' ' Open channel(s)..' | omarchy-launch-walker --dmenu --width 295 --minheight 1 --maxheight 630 -p 'Twitch…' 2>/dev/null || true)" + choice="$(menu_select 'Twitch' 295 630 ' Recheck notifications' '󰜉 Restart notifications' ' Open following' ' Open following live' ' Open channel(s)..')" choice="${choice#* }" case "$choice" in diff --git a/scripts/.local/bin/update b/scripts/.local/bin/update index 9f723293..cd17dadf 100755 --- a/scripts/.local/bin/update +++ b/scripts/.local/bin/update @@ -115,7 +115,10 @@ fi if (( omarchy_selected )); then section 'Omarchy' - omarchy update -y || exit + mise_state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/mise" + mkdir -p "$mise_state_dir" + MISE_GLOBAL_CONFIG_FILE="$mise_state_dir/omarchy-config.toml" omarchy update -y || exit + dot stow --public || exit fi run_topgrade diff --git a/scripts/.local/bin/waybar b/scripts/.local/bin/waybar deleted file mode 100755 index 4095a5c0..00000000 --- a/scripts/.local/bin/waybar +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/waybar" -cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/waybar" -host="${OMARCHY_HOST:-}" -config_path="" -selected_config_path="" - -prepare_selected_config_path() { - mkdir -p "$cache_dir" - selected_config_path="$cache_dir/config.selected-output.jsonc" -} - -reset_git_waybar_caches() { - rm -f "$cache_dir"/git-*-waybar.json "$cache_dir"/git-*-waybar.json.tmp - rmdir "$cache_dir"/git-*-waybar.lock 2>/dev/null || true -} - -resolve_active_outputs() { - local outputs - - if ! command -v hyprctl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then - return 1 - fi - - outputs="$(hyprctl -j monitors | jq -r '[.[] | select(.disabled != true)] | sort_by(.x, .y) | .[].name // empty')" - [[ -n "$outputs" ]] || return 1 - - printf '%s\n' "$outputs" -} - -resolve_focused_output() { - local output - - if ! command -v hyprctl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then - return 1 - fi - - output="$(hyprctl -j monitors | jq -r '[.[] | select(.disabled != true and .focused == true)] | first | .name // empty')" - [[ -n "$output" ]] || return 1 - - printf '%s' "$output" -} - -output_in_list() { - local needle="$1" - shift - local item - - for item in "$@"; do - if [[ "$item" == "$needle" ]]; then - return 0 - fi - done - - return 1 -} - -resolve_primary_output() { - local preferred_output="" - local focused_output="" - - case "$host" in - desktop) - preferred_output="HDMI-A-2" - ;; - laptop) - preferred_output="eDP-1" - ;; - esac - - if [[ -n "$preferred_output" ]] && output_in_list "$preferred_output" "$@"; then - printf '%s' "$preferred_output" - return 0 - fi - - focused_output="$(resolve_focused_output || true)" - if [[ -n "$focused_output" ]] && output_in_list "$focused_output" "$@"; then - printf '%s' "$focused_output" - return 0 - fi - - if (( $# > 0 )); then - printf '%s' "$1" - return 0 - fi - - return 1 -} - -build_single_output_config() { - local base_config="$1" - local output="$2" - - prepare_selected_config_path - - cat > "$selected_config_path" < 0 )); then - secondary_outputs_json="$(printf '%s\n' "$@" | jq -R . | jq -s .)" - fi - - jq -n \ - --arg full_config "$full_config" \ - --arg minimal_config "$minimal_config" \ - --arg primary_output "$primary_output" \ - --argjson secondary_outputs "$secondary_outputs_json" \ - '[ - { - "include": [$full_config], - "output": [$primary_output] - }, - { - "include": [$minimal_config], - "output": $secondary_outputs - } - ]' > "$selected_config_path" -} - -real_waybar_bin="$(PATH=/usr/local/bin:/usr/bin:/bin command -v waybar 2>/dev/null || true)" -if [[ -z "$real_waybar_bin" ]]; then - printf 'waybar wrapper: could not find real waybar binary\n' >&2 - exit 1 -fi - -if [[ -n "$host" ]]; then - host_config="$config_dir/config.${host}.jsonc" - if [[ -f "$host_config" ]]; then - config_path="$host_config" - fi -fi - -for arg in "$@"; do - case "$arg" in - -c|--config|--config=*|-o|--output|--output=*) - reset_git_waybar_caches - exec "$real_waybar_bin" "$@" - ;; - esac -done - -reset_git_waybar_caches - -full_config_path="$config_path" -if [[ -z "$full_config_path" ]]; then - default_config="$config_dir/config.jsonc" - if [[ -f "$default_config" ]]; then - full_config_path="$default_config" - fi -fi - -minimal_config_path="" -if [[ -n "$host" ]]; then - host_minimal_config="$config_dir/config.minimal.${host}.jsonc" - if [[ -f "$host_minimal_config" ]]; then - minimal_config_path="$host_minimal_config" - fi -fi - -active_outputs=() -if mapfile -t active_outputs < <(resolve_active_outputs); then - : -fi - -if [[ -n "$full_config_path" ]] && (( ${#active_outputs[@]} > 0 )); then - primary_output="$(resolve_primary_output "${active_outputs[@]}" || true)" - - if [[ -n "$primary_output" ]]; then - if [[ -n "$minimal_config_path" ]] && (( ${#active_outputs[@]} > 1 )); then - secondary_outputs=() - for output in "${active_outputs[@]}"; do - if [[ "$output" != "$primary_output" ]]; then - secondary_outputs+=("$output") - fi - done - - if (( ${#secondary_outputs[@]} > 0 )); then - build_full_and_minimal_output_config \ - "$full_config_path" \ - "$minimal_config_path" \ - "$primary_output" \ - "${secondary_outputs[@]}" - - exec "$real_waybar_bin" -c "$selected_config_path" "$@" - fi - fi - - build_single_output_config "$full_config_path" "$primary_output" - exec "$real_waybar_bin" -c "$selected_config_path" "$@" - fi -fi - -if [[ -n "$full_config_path" ]]; then - exec "$real_waybar_bin" -c "$full_config_path" "$@" -fi - -exec "$real_waybar_bin" "$@" diff --git a/scripts/.local/bin/workspace-menu b/scripts/.local/bin/workspace-menu index 6eb90a8c..2c8a738b 100755 --- a/scripts/.local/bin/workspace-menu +++ b/scripts/.local/bin/workspace-menu @@ -2,6 +2,15 @@ set -euo pipefail +menu_select() { + local prompt="$1" + local width="$2" + local maxheight="$3" + shift 3 + + omarchy-menu-select "$prompt" "$@" -- --width "$width" --maxheight "$maxheight" 2>/dev/null || true +} + while true; do options=( '󰙀 Relayout current workspace' @@ -15,7 +24,7 @@ while true; do options=('󰣇 Setup workspace' "${options[@]}") fi - choice="$(printf '%s\n' "${options[@]}" | omarchy-launch-walker --dmenu --width 360 --minheight 1 --maxheight 420 -p 'Workspace…' 2>/dev/null || true)" + choice="$(menu_select 'Workspace' 360 420 "${options[@]}")" choice="${choice#* }" case "$choice" in diff --git a/scripts/.local/bin/workspace-relayout b/scripts/.local/bin/workspace-relayout index 742c6b3b..6cb54a0d 100755 --- a/scripts/.local/bin/workspace-relayout +++ b/scripts/.local/bin/workspace-relayout @@ -297,22 +297,32 @@ apply_tree() { # Menu + presets # --------------------------------------------------------------------------- -# Show a walker dmenu of the given options and echo the chosen line. -walker_pick() { +# Show an option menu and echo the chosen line via omarchy-menu-select +# (Omarchy 4 shell-IPC selector). Prompts omit a trailing ellipsis; +# omarchy-menu-select appends its own. +menu_select() { local prompt="$1" - shift - printf '%s\n' "$@" | omarchy-launch-walker --dmenu --width 460 --minheight 1 --maxheight 360 -p "$prompt" 2>/dev/null || true + local width="$2" + local maxheight="$3" + shift 3 + + omarchy-menu-select "$prompt" "$@" -- --width "$width" --maxheight "$maxheight" 2>/dev/null || true +} + +menu_selector_available() { + command -v omarchy-menu-select >/dev/null 2>&1 } -# Free-text prompt for a layout name, prefilled (via placeholder) with $1. -# Walker has no editable-prefill, so the default shows as the placeholder and +# Free-text prompt for a layout name, defaulting to $1. Uses the Omarchy 4 +# native menu input (omarchy-menu-input, which summons omarchy.menu). The native +# menu has no editable prefill, so the default is shown in the prompt label and # an empty submit keeps it; typing overrides it. prompt_name() { local default="$1" local out="" - if command -v omarchy-launch-walker >/dev/null 2>&1; then - out="$(omarchy-launch-walker --dmenu --inputonly --width 460 --minheight 1 --maxheight 1 -p "$default" 2>/dev/null || true)" + if command -v omarchy-menu-input >/dev/null 2>&1; then + out="$(omarchy-menu-input "Layout name ($default)" --width 460 2>/dev/null || true)" fi if [ -n "$out" ]; then @@ -414,11 +424,11 @@ apply_menu() { exit 0 fi - group="$(walker_pick "$count window layout family..." "${group_name[@]}")" + group="$(menu_select "$count window layout family" 460 360 "${group_name[@]}")" [ -n "$group" ] || exit 0 filter_presets_for_group "$group" - choice="$(walker_pick "$group layout..." "${sel_label[@]}")" + choice="$(menu_select "$group layout" 460 360 "${sel_label[@]}")" [ -n "$choice" ] || exit 0 idx="$(index_of "$choice" "${sel_label[@]}")" @@ -464,7 +474,7 @@ edit_menu() { load_presets_for_count "$count" - group="$(walker_pick "Edit $count window layout family..." "${group_name[@]}" "$add_group_label")" + group="$(menu_select "Edit $count window layout family" 460 360 "${group_name[@]}" "$add_group_label")" [ -n "$group" ] || exit 0 if [ "$group" = "$add_group_label" ]; then @@ -474,7 +484,7 @@ edit_menu() { fi filter_presets_for_group "$group" - choice="$(walker_pick "Edit $count window layouts..." "${sel_label[@]}" "$add_label")" + choice="$(menu_select "Edit $count window layouts" 460 360 "${sel_label[@]}" "$add_label")" [ -n "$choice" ] || exit 0 if [ "$choice" = "$add_label" ]; then @@ -506,8 +516,8 @@ main() { require_dependencies - if ! command -v omarchy-launch-walker >/dev/null 2>&1; then - notify "omarchy-launch-walker is not available" + if ! menu_selector_available; then + notify "No Omarchy menu selector is available" exit 1 fi diff --git a/scripts/.local/share/omarchy/bin/waybar b/scripts/.local/share/omarchy/bin/waybar deleted file mode 100755 index cc71c3d7..00000000 --- a/scripts/.local/share/omarchy/bin/waybar +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -wrapper="${HOME}/.local/bin/waybar" - -if [[ -x "$wrapper" ]]; then - exec "$wrapper" "$@" -fi - -exec /usr/bin/waybar "$@" diff --git a/systemd/.config/systemd/user/dot-on-resume-monitor.service b/systemd/.config/systemd/user/dot-on-resume-monitor.service new file mode 100644 index 00000000..8fa886bc --- /dev/null +++ b/systemd/.config/systemd/user/dot-on-resume-monitor.service @@ -0,0 +1,13 @@ +[Unit] +Description=Run dot resume recovery after system resume +After=dbus.socket +Requires=dbus.socket + +[Service] +Type=simple +ExecStart=%h/.local/bin/on-resume-monitor +Restart=always +RestartSec=2 + +[Install] +WantedBy=graphical-session.target diff --git a/tests/scripts/mise.test.sh b/tests/scripts/mise.test.sh new file mode 100755 index 00000000..65c2b61a --- /dev/null +++ b/tests/scripts/mise.test.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +mise_wrapper="$repo_root/scripts/.local/bin/mise" +test_root=$(mktemp -d) +mock_bin="$test_root/bin" +mock_home="$test_root/home" +mkdir -p "$mock_bin" "$mock_home" +trap 'rm -rf "$test_root"' EXIT + +cat >"$mock_bin/mise" <<'EOF' +#!/bin/bash +printf 'args=%s\n' "$*" +printf 'global=%s\n' "${MISE_GLOBAL_CONFIG_FILE:-}" +EOF +chmod +x "$mock_bin/mise" + +run_mise() { + HOME="$mock_home" XDG_STATE_HOME='' PATH="$mock_bin:/usr/bin" "$mise_wrapper" "$@" +} + +global_write=$(run_mise use -g gh) +[[ $global_write == *"args=use -g gh"* ]] +[[ $global_write == *"global=$mock_home/.local/state/mise/omarchy-config.toml"* ]] + +long_global_write=$(run_mise use --global node@latest) +[[ $long_global_write == *"global=$mock_home/.local/state/mise/omarchy-config.toml"* ]] + +settings_write=$(run_mise settings add idiomatic_version_file_enable_tools ruby) +[[ $settings_write == *"global=$mock_home/.local/state/mise/omarchy-config.toml"* ]] + +global_remove=$(run_mise rm -g node) +[[ $global_remove == *"global=$mock_home/.local/state/mise/omarchy-config.toml"* ]] + +global_unuse=$(run_mise unuse --global node) +[[ $global_unuse == *"global=$mock_home/.local/state/mise/omarchy-config.toml"* ]] + +local_write=$(run_mise use node@22) +[[ $local_write == *"args=use node@22"* ]] +[[ $local_write == $'args=use node@22\nglobal=' ]] + +read_command=$(run_mise current) +[[ $read_command == $'args=current\nglobal=' ]] + +explicit_write=$(run_mise --write-global-config use -g gh@2.97.0) +[[ $explicit_write == *"args=use -g gh@2.97.0"* ]] +[[ $explicit_write == $'args=use -g gh@2.97.0\nglobal=' ]] + +printf 'mise global config guard tests passed\n' diff --git a/tests/scripts/package-updates-bar.test.sh b/tests/scripts/package-updates-bar.test.sh deleted file mode 100755 index cdfc10c6..00000000 --- a/tests/scripts/package-updates-bar.test.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) -module="$repo_root/scripts/.local/bin/package-updates-bar" -test_dir=$(mktemp -d) -package_file="$test_dir/packages" -cache_dir="$test_dir/cache" -fake_yay="$test_dir/yay" -fake_pacman="$test_dir/pacman" -fake_pkill="$test_dir/pkill" - -trap 'rm -rf "$test_dir"' EXIT -mkdir -p "$cache_dir" - -cat >"$fake_yay" <<'EOF' -#!/usr/bin/env bash -printf 'called\n' >>"$FAKE_YAY_CALLS_FILE" -case "$FAKE_YAY_RESULT" in - none) exit 1 ;; - http-error) - printf 'status 429: Rate limit reached\n' >&2 - exit 1 - ;; -esac -EOF - -cat >"$fake_pacman" <<'EOF' -#!/usr/bin/env bash -case "$1:$2:${3:-}" in - -Qmq:--:example-bin) exit 0 ;; -esac -exit 1 -EOF - -printf '#!/usr/bin/env bash\nexit 0\n' >"$fake_pkill" -chmod +x "$fake_yay" "$fake_pacman" "$fake_pkill" - -printf 'example-bin\n' >"$package_file" - -export WAYBAR_PACKAGE_UPDATES_FILE="$package_file" -export WAYBAR_PACKAGE_UPDATES_CACHE_DIR="$cache_dir" -export WAYBAR_PACKAGE_UPDATES_YAY_BIN="$fake_yay" -export WAYBAR_PACKAGE_UPDATES_PACMAN_BIN="$fake_pacman" -export WAYBAR_PACKAGE_UPDATES_PKILL_BIN="$fake_pkill" -export FAKE_YAY_CALLS_FILE="$test_dir/yay-calls" - -FAKE_YAY_RESULT=http-error "$module" refresh -[[ -s "$cache_dir/package-updates-waybar.backoff" ]] - -calls_before=$(wc -l <"$FAKE_YAY_CALLS_FILE") -rm -rf "$cache_dir/package-updates-waybar.lock" -WAYBAR_PACKAGE_UPDATES_LOCKED=1 FAKE_YAY_RESULT=none "$module" refresh -calls_after=$(wc -l <"$FAKE_YAY_CALLS_FILE") -[[ "$calls_after" == "$calls_before" ]] - -rm -rf "$cache_dir/package-updates-waybar.lock" -FAKE_YAY_RESULT=none "$module" refresh -[[ ! -e "$cache_dir/package-updates-waybar.backoff" ]] -jq -e '.text == "" and .class == "hidden"' "$cache_dir/package-updates-waybar.json" >/dev/null - -printf 'package update refresh tests passed\n' diff --git a/tests/scripts/update.test.sh b/tests/scripts/update.test.sh index 37e6f733..93858d7a 100644 --- a/tests/scripts/update.test.sh +++ b/tests/scripts/update.test.sh @@ -66,27 +66,35 @@ chmod +x "$mock_bin/gum" cat >"$mock_bin/omarchy" <<'EOF' #!/bin/bash printf 'omarchy args: %s\n' "$*" +printf 'omarchy mise config: %s\n' "${MISE_GLOBAL_CONFIG_FILE:-}" command -v sudo +[[ ${UPDATE_TEST_OMARCHY_FAIL:-0} != 1 ]] EOF chmod +x "$mock_bin/omarchy" -headless_output=$(HOME="$mock_home" PATH="$mock_bin:$PATH" "$update_script" -y) +headless_output=$(HOME="$mock_home" XDG_STATE_HOME='' PATH="$mock_bin:$PATH" "$update_script" -y) [[ $headless_output == *"/sudo"* || $headless_output == *"/sudo" ]] [[ $headless_output != *"/usr/bin/sudo"* ]] [[ $headless_output == *"topgrade args: -y"* ]] interactive_output=$(script --quiet --return --command \ - "HOME='$mock_home' PATH='$mock_bin:$PATH' UPDATE_TEST_SELECTION=omarchy '$update_script'" /dev/null) + "HOME='$mock_home' XDG_STATE_HOME='' PATH='$mock_bin:$PATH' UPDATE_TEST_SELECTION=omarchy '$update_script'" /dev/null) [[ $interactive_output == *"/usr/bin/sudo"* ]] default_output=$(script --quiet --return --command \ - "HOME='$mock_home' PATH='$mock_bin:$PATH' UPDATE_TEST_SELECTION=default '$update_script'" /dev/null) + "HOME='$mock_home' XDG_STATE_HOME='' PATH='$mock_bin:$PATH' UPDATE_TEST_SELECTION=default '$update_script'" /dev/null) [[ $default_output == *"topgrade args: --only mise github_cli_extensions yazi"* ]] -[[ $default_output == *"dot args: update"*"omarchy args: update -y"*"topgrade args:"* ]] +[[ $default_output == *"dot args: update"*"omarchy args: update -y"*"dot args: stow --public"*"topgrade args:"* ]] +[[ $default_output == *"omarchy mise config: $mock_home/.local/state/mise/omarchy-config.toml"* ]] all_output=$(script --quiet --return --command \ - "HOME='$mock_home' PATH='$mock_bin:$PATH' UPDATE_TEST_SELECTION=all '$update_script'" /dev/null) + "HOME='$mock_home' XDG_STATE_HOME='' PATH='$mock_bin:$PATH' UPDATE_TEST_SELECTION=all '$update_script'" /dev/null) [[ $all_output == *"topgrade args: "* ]] [[ $all_output != *"topgrade args: --only"* ]] +if failed_output=$(HOME="$mock_home" XDG_STATE_HOME='' PATH="$mock_bin:$PATH" UPDATE_TEST_OMARCHY_FAIL=1 "$update_script" -y 2>&1); then + exit 1 +fi +[[ $failed_output != *"dot args: stow --public"* ]] + printf 'update privilege routing tests passed\n' diff --git a/tests/scripts/workspace-relayout.test.sh b/tests/scripts/workspace-relayout.test.sh index efa3d096..b0d9063b 100644 --- a/tests/scripts/workspace-relayout.test.sh +++ b/tests/scripts/workspace-relayout.test.sh @@ -29,7 +29,7 @@ case "$*" in esac EOF -cat >"$temp_dir/bin/omarchy-launch-walker" <<'EOF' +cat >"$temp_dir/bin/omarchy-menu-select" <<'EOF' #!/usr/bin/env bash count=0 if [[ -f "$WALKER_COUNT_FILE" ]]; then @@ -37,7 +37,11 @@ if [[ -f "$WALKER_COUNT_FILE" ]]; then fi count=$((count + 1)) printf '%s' "$count" >"$WALKER_COUNT_FILE" -cat >"$WALKER_LOG_DIR/menu-$count" +shift +for option in "$@"; do + [[ "$option" == -- ]] && break + printf '%s\n' "$option" +done >"$WALKER_LOG_DIR/menu-$count" sed -n "${count}p" "$WALKER_RESPONSES" EOF @@ -46,7 +50,7 @@ cat >"$temp_dir/bin/omarchy" <<'EOF' printf '%s\n' "$*" >>"$NOTIFICATION_LOG" EOF -chmod +x "$temp_dir/bin/hyprctl" "$temp_dir/bin/omarchy-launch-walker" "$temp_dir/bin/omarchy" +chmod +x "$temp_dir/bin/hyprctl" "$temp_dir/bin/omarchy-menu-select" "$temp_dir/bin/omarchy" run_relayout() { local case_dir="$1" diff --git a/tmux/.config/tmux/tmux.conf b/tmux/.config/tmux/tmux.conf index 2f4ac105..f600d044 100644 --- a/tmux/.config/tmux/tmux.conf +++ b/tmux/.config/tmux/tmux.conf @@ -4,7 +4,8 @@ set -g prefix2 C-b bind C-Space send-prefix # Reload config -bind q source-file ~/.config/tmux/tmux.conf +bind q source-file ~/.config/tmux/tmux.conf \; display "Configuration reloaded" +bind ? display-popup -E -w 80% -h 70% -T "Tmux keybindings" "omarchy-menu-tmux-keybindings --print | less -R" # Vi mode for copy setw -g mode-keys vi @@ -40,6 +41,7 @@ bind -n M-6 select-window -t 6 bind -n M-7 select-window -t 7 bind -n M-8 select-window -t 8 bind -n M-9 select-window -t 9 + bind -n M-Left select-window -t -1 bind -n M-Right select-window -t +1 bind -n M-S-Left swap-window -t -1 \; select-window -t -1 @@ -51,6 +53,7 @@ bind C new-session -c "#{pane_current_path}" bind K kill-session bind P switch-client -p bind N switch-client -n + bind -n M-Up switch-client -p bind -n M-Down switch-client -n @@ -72,6 +75,7 @@ setw -g aggressive-resize on set -g detach-on-destroy off set -g extended-keys on set -g extended-keys-format csi-u +set -g terminal-features[3] "xterm-kitty:extkeys" set -sg escape-time 10 # Status bar @@ -83,11 +87,10 @@ set -g window-status-separator "" set -gw automatic-rename on set -gw automatic-rename-format '#{b:pane_current_path}' - # Theme set -g status-style "bg=default,fg=default" -set -g status-left "" -set -g status-right "#[fg=blue]#{?pane_in_mode,COPY ,}#{?client_prefix,PREFIX ,}#[fg=brightblack]#h " +set -g status-left "#[fg=black,bg=blue,bold] #S #[bg=default] " +set -g status-right "#[fg=blue]#{?pane_in_mode,COPY ,}#{?client_prefix,PREFIX ,}#{?window_zoomed_flag,ZOOM ,}#[fg=brightblack]#h " set -g window-status-format "#[fg=brightblack] #I:#W " set -g window-status-current-format "#[fg=blue,bold] #I:#W " set -g pane-border-style "fg=brightblack" @@ -96,3 +99,10 @@ set -g message-style "bg=default,fg=blue" set -g message-command-style "bg=default,fg=blue" set -g mode-style "bg=blue,fg=black" setw -g clock-mode-colour blue + +bind -n M-Enter split-window -v -c "#{pane_current_path}" +bind -n M-S-Enter split-window -h -c "#{pane_current_path}" +bind -n M-Escape kill-pane + +# Enable OSC 52 clipboard forwarding for remote Neovim yanks. +set -as terminal-features ",*:clipboard" diff --git a/uwsm/.config/uwsm/env.d/90-dotfiles b/uwsm/.config/uwsm/env.d/90-dotfiles new file mode 100644 index 00000000..0ca36fa7 --- /dev/null +++ b/uwsm/.config/uwsm/env.d/90-dotfiles @@ -0,0 +1,8 @@ +# Changes require a relaunch of Hyprland to take effect. + +# Keep custom Hypr helpers available to uwsm apps. +export PATH="$HOME/.config/hypr/bin:$PATH" + +# Enable OpenCode's LSP tool and long-running background subtasks. +export OPENCODE_EXPERIMENTAL_LSP_TOOL=true +export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true diff --git a/zsh/.local/share/zsh/site-functions/_dot b/zsh/.local/share/zsh/site-functions/_dot index 76f6905f..6607a157 100644 --- a/zsh/.local/share/zsh/site-functions/_dot +++ b/zsh/.local/share/zsh/site-functions/_dot @@ -47,7 +47,6 @@ _dot_cmd_init() { '--force[Re-run init even if the machine looks initialised]' \ '--host[Hypr host to link before stow (default: OMARCHY_HOST or desktop)]:name:' \ '--log[Init log path (default: ~/.local/state/dot/init.log)]:path:_files' \ - '--branch[Branch override for Omarchy repos]:name:' \ '(-h --help)-h[Show this help message]' \ '(-h --help)--help[Show this help message]' \ '*::arg:->args' diff --git a/zsh/.zshrc b/zsh/.zshrc index 442a374d..67bc5fb0 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -246,7 +246,11 @@ export PATH="/home/aidan/.cache/.bun/bin:$PATH" # ------------------------------ export XDG_CURRENT_DESKTOP=Hyprland export XDG_SESSION_TYPE=wayland -export QT_QPA_PLATFORM=xcb +# Prefer the Wayland Qt platform (so layer-shell apps like the Omarchy +# Quickshell bar attach correctly), falling back to XCB only if Wayland init +# fails. A plain `xcb` here makes `omarchy restart shell` launch the bar under +# XWayland, where it cannot attach and renders as a floating window. +export QT_QPA_PLATFORM="wayland;xcb" export QT_WAYLAND_DISABLE_WINDOWDECORATION=1 export ELECTRON_OZONE_PLATFORM_HINT=wayland @@ -847,8 +851,6 @@ source ~/.local/share/omarchy/default/bash/envs # Omarchy extras # ------------------------------ timmo-update-extras() { - git-update ~/.config/waybar - git-update ~/.config/uwsm git-update ~/.config/dotfiles git-update ~/.config/dotfiles-private }