38 Commits

Author SHA1 Message Date
f6026b5cac hyprland: add workspace history plugin 2026-04-30 01:33:08 -07:00
34906469b9 hyprland: match Element scratchpad Wayland class 2026-04-30 00:47:59 -07:00
3cb0301f9a Add per-monitor workspace history cycling 2026-04-30 00:37:49 -07:00
6f489d14ab Pin taffybar Hyprland recovery fix 2026-04-29 22:59:22 -07:00
acae19d9c5 hyprland: move hyprexpo to alt-tab 2026-04-29 22:58:14 -07:00
c30a67facf Fix Hyprland workspace tab grouping 2026-04-29 21:28:48 -07:00
d48edc9bb8 Remove direct fullscreen WM bindings 2026-04-29 14:26:33 -07:00
af570360d3 taffybar: respect desktop shell selector 2026-04-29 14:23:15 -07:00
34fd60e8f2 Remove Chrome-backed scratchpads 2026-04-29 14:19:15 -07:00
f826c6ae75 xmonad: respect desktop shell UI selector 2026-04-29 14:12:04 -07:00
1a2b75adcb Fix KDE MIME defaults in xmonad session 2026-04-29 14:08:19 -07:00
4e52e81a50 Fix desktop shell UI systemd condition 2026-04-29 14:07:14 -07:00
df0b7b6db4 Split local Codex config from dotfiles 2026-04-29 13:57:19 -07:00
a7769545f1 hyprland: pin custom plugin forks 2026-04-29 13:35:03 -07:00
bb32668387 hyprland: run screensaver as layer overlay 2026-04-29 13:34:04 -07:00
8ccf5fb7de hyprland: add noctalia shell module 2026-04-29 13:33:39 -07:00
52861430da hyprland: route shell actions through wrapper 2026-04-29 13:31:48 -07:00
d9ebb812c5 desktop: add shell ui selector 2026-04-29 13:30:40 -07:00
5cf2eda008 hyprland: animate ghostty dropdown 2026-04-29 13:29:42 -07:00
6299ad2c7d hyprland: expand animation leaf config 2026-04-29 13:29:24 -07:00
672cc14713 hyprland: style grouped window tabs 2026-04-29 13:28:38 -07:00
64c45e1060 hyprland: add tabbed workspace grouping 2026-04-29 13:27:47 -07:00
a5413331d9 hyprland: route grouped directional controls 2026-04-29 13:24:19 -07:00
1044565bf7 hyprland: dispatch windows by address selector 2026-04-29 13:23:33 -07:00
d684f6fbc5 hyprland: import runtime dir into session environment 2026-04-29 13:22:37 -07:00
71deb64ed0 taffybar: fix local flake warning build 2026-04-29 12:18:00 -07:00
bb909849bd Add reusable remote Hyprland module 2026-04-29 11:17:02 -07:00
a37e83fb23 Install Hyprland rofi window picker 2026-04-29 07:55:41 -07:00
53d8a69a31 Restore rofi window picker for Hyprland 2026-04-29 07:34:45 -07:00
87fd1681e2 Restore Hyprland reload binding 2026-04-29 07:26:42 -07:00
8933f8e545 Remove Hyprland shell script bindings 2026-04-29 07:21:27 -07:00
ed90130233 Merge branch 'hyprland-lua-squashed' 2026-04-29 03:00:36 -07:00
aa1fbf9699 Show AI usage remaining percentages 2026-04-29 01:45:30 -07:00
3e05939ce3 Add random Hyprland screensaver rotation 2026-04-29 01:45:30 -07:00
8e2128b8d4 Add Roborock vacuum control
Add a python-roborock based CLI wrapper and package it for the NixOS system profile.
2026-04-29 01:45:30 -07:00
1696845579 Add KEF speaker control CLI 2026-04-29 01:45:30 -07:00
5522b8bacd Add logos to AI usage widgets 2026-04-29 01:45:30 -07:00
9c9af9f856 Implement Hyprland Lua migration 2026-04-28 21:10:34 -07:00
67 changed files with 5130 additions and 3065 deletions

View File

@@ -0,0 +1,218 @@
# Hyprland Lua Migration Checklist
This checklist tracks the migration described in `docs/tiling-wm-experience.md`.
Guiding rule for shelling out:
- Prefer Lua for compositor/window/workspace state changes.
- Avoid `hyprctl` for window manipulation unless there is no usable Lua API.
- `hyprctl` remains acceptable for non-window-control escape hatches such as
`hyprctl reload`.
- External utilities remain acceptable where they are the real tool being
launched, for example rofi, cliphist, grim/slurp/swappy, playerctl, hyprlock,
and systemd commands.
## 0. Version And Build Base
- [x] Update/confirm Hyprland Lua input at latest usable upstream target.
- [x] Keep stable Hyprland path intact until Lua path is proven.
- [x] Keep hy3 out of the Lua branch.
- [x] Keep hyprNStack following the Lua Hyprland input.
- [x] Rebuild hyprNStack against the Lua Hyprland branch.
- [x] Add a forked hyprexpo input for the Lua Hyprland branch.
- [x] Keep a cheap Lua check: parse config, execute against stub, reject
`hyprctl` in the Lua config's window/workspace manipulation path.
- [x] Add a real Hyprland Lua verifier check for the config parser path.
Current upstream note: latest Hyprland release observed during this migration is
`v0.54.3`; the Lua config input tracks PR 13817 and was already at the current
PR head `c35a8a5` dated 2026-04-26. The non-Lua fallback remains pinned to the older
hy3/hyprexpo-compatible stack; the Lua branch uses forked hyprexpo branch
`colonelpanic8/hyprland-plugins:hyprexpo-lua-hyprland`.
## 1. Core Layout
- [x] Primary layout is equal-width columns.
- [x] No scrolling layout.
- [x] No hy3 in Lua path.
- [x] Dynamic redistribution on open/close via Lua-managed nStack count.
- [x] Monocle/tabbed-style layout available.
- [x] Direct jump to columns layout.
- [x] Direct jump to monocle layout.
- [x] Directional focus cycles in monocle.
- [x] Visual indication of hidden monocle windows, currently notification.
- [x] Make layout state per workspace instead of one global current layout.
- [x] Preserve one-window smart gaps in the live config path.
- [x] Use a persistent monocle indicator instead of a transient notification.
Smart-gaps note: nStack uses `no_gaps_when_only = true`; Hyprland workspace
rules are still applied at runtime for broader parity, but skipped during
`--verify-config` because the current Lua PR segfaults when rule bindings run in
verifier mode.
## 2. Workspace Behavior
- [x] `Super+1..9` focuses bounded workspaces.
- [x] `Super+Shift+1..9` sends window without following.
- [x] `Super+Ctrl+1..9` sends and follows.
- [x] Previous workspace per monitor uses Lua-tracked history.
- [x] Implement next empty workspace focus in Lua.
- [x] Implement move focused window to next empty workspace without following.
- [x] Implement move focused window to next empty workspace and follow.
- [x] Implement bounded workspace cycling `1..9` in Lua, replacing
`workspace-scroll.sh`.
- [x] Implement workspace swap or decide whether native dispatcher is enough.
- [x] Track current monitor workspace history explicitly, with native
`previous_per_monitor` as fallback.
## 3. Directional Navigation
- [x] `Super+w/a/s/d` focuses windows.
- [x] `Super+Shift+w/a/s/d` swaps windows.
- [x] `Hyper+w/a/s/d` focuses monitors.
- [x] `Hyper+Shift+w/a/s/d` moves windows to monitors.
- [x] `Super+z` next monitor.
- [x] `Super+Shift+z` move to next monitor.
- [x] Replace any old cursor-follow/move scripts fully.
- [x] Add required `Super+Ctrl+w/a/s/d` move-to-monitor behavior preserving
useful focus.
- [x] Add "move to empty workspace on monitor in direction" without requiring
`Hyper+Ctrl`.
- [x] Route directional focus in monocle through deterministic Lua cycling.
- [ ] Live-verify directional focus in monocle behaves predictably.
## 4. Script Elimination Priority
- [x] Core layout switching no longer uses scripts.
- [x] Core column count logic no longer uses scripts or `hyprctl`.
- [x] Replace `find-empty-workspace.sh`.
- [x] Replace `workspace-goto-empty.sh`.
- [x] Replace `workspace-move-to-empty.sh`.
- [x] Replace `workspace-scroll.sh`.
- [x] Replace `cycle-layout.sh`.
- [x] Replace `movewindow-follow-cursor.sh`.
- [x] Replace `gather-class.sh`.
- [x] Replace `focus-next-class.sh`.
- [x] Replace `raise-or-run.sh`.
- [x] Replace minimize scripts if Lua can maintain hidden workspace state.
- [x] Replace `swap-workspaces.sh`.
- [x] Decide whether rofi-backed pickers remain scripts or become
Lua-generated command pipes. Rofi itself remains external.
## 5. Overview And Window Discovery
- [x] Restore visual hyprexpo for `Super+Tab` overview.
- [x] Restore visual hyprexpo `bring` mode for `Super+Shift+Tab`.
- [x] Keep first-pass Lua numbered window picker on secondary bindings.
- [x] Implement first-pass Lua-native go-to-window picker.
- [x] Implement first-pass Lua-native bring-window picker.
- [x] Implement first-pass Lua-native replace-window picker.
- [ ] Picker entries include icons.
- [x] Picker entries include title/workspace.
- [x] Hide scratchpad/minimized/internal windows from normal pickers.
- [x] Decide whether picker data generation can be Lua-native with rofi as only
external process.
Picker decision: current Lua API can query and manipulate windows directly, but
does not expose a synchronous way to run rofi and consume its selected output.
The first pass therefore uses Lua-native numbered submaps and notifications.
A final rofi/icon picker would need either a small IPC bridge or an upstream Lua
process-output/callback primitive.
Hyprexpo decision: hyprexpo is kept as the visual overview. The forked Lua
branch exposes `hl.plugin.hyprexpo.expo(...)`, so the Lua config can invoke
`toggle` and `bring` directly without shelling out to `hyprctl`.
## 6. Scratchpads
- [x] Preserve named scratchpads: element, htop, slack,
spotify, transmission, volume.
- [x] Preserve dropdown terminal scratchpad.
- [x] Scratchpads near-fullscreen and centered.
- [x] Scratchpads hidden from normal listings/status bar.
- [x] Toggling scratchpad exits fullscreen/monocle state first.
- [x] Decide hyprscratch daemon is not needed in the Lua branch.
- [x] Replace `hyprscratch toggle` with Lua-managed scratchpad toggles.
- [x] Disable hyprscratch service on the Lua branch.
- [x] Handle delayed class/title assignment with window class/title event adoption.
- [x] Handle already-running app.
- [x] Handle minimized app.
- [x] Handle app on another workspace.
## 7. Minimization
- [x] Implement minimize active window.
- [x] Implement restore last minimized window.
- [x] Exclude minimized windows from layout.
- [x] Exclude minimized windows from normal go/bring lists.
- [x] Implement minimized picker.
- [x] Implement restore all minimized.
- [x] Implement minimize other windows of current workspace class.
- [x] Implement restore windows of focused class.
- [x] Decide hidden workspace naming/state model for minimized windows.
- [x] Hydrate minimized-window state from the hidden workspace on restore/picker
paths.
## 8. Class-Aware Workflows
- [x] Gather all windows of focused class onto current workspace.
- [x] Focus next window of different/same class as desired parity.
- [x] Browser raise-or-spawn.
- [x] Window info command exposes class/title/workspace/address/pid.
- [ ] Window menus expose real window icons.
- [x] Prefer Lua window queries over `hyprctl clients`.
## 9. Status Bar Contract
- [ ] Confirm taffybar can still list normal workspaces.
- [ ] Confirm special scratchpad/minimize workspaces are filtered.
- [ ] Confirm active workspace per monitor remains visible.
- [ ] Confirm class/title/active/minimized/urgent metadata is available.
- [x] Expose layout name/state if practical.
- [ ] Confirm workspace/window positioning remains enough for icon strips.
Layout state note: Lua writes `$XDG_RUNTIME_DIR/hyprland-layout-state` with the
active workspace, active layout, and per-workspace layout map. Taffybar still
needs a live readback check.
## 10. Session And Utilities
- [x] Terminal binding preserved.
- [x] Launcher/run menu preserved.
- [x] Media keys preserved.
- [x] Clipboard history binding preserved.
- [x] Screenshot binding preserved.
- [x] Lock binding preserved.
- [x] Session startup target integration preserved.
- [x] `hyprctl reload` may remain available as a non-window-manipulation escape
hatch.
- [x] Resolve `Hyper+w` conflict: monitor focus must win; wallpaper picker
needs another key.
- [x] Keep rofi utility commands as external commands unless there is a
meaningful Lua replacement.
- [x] Decide which shell utilities are acceptable because they are not Hyprland
control scripts.
## 11. Validation
- [x] Lua syntax check.
- [x] Lua stub execution check.
- [x] `hyprctl` rejection in Lua config for window/workspace manipulation.
- [x] Real `Hyprland --verify-config` check.
- [x] hyprNStack flake build check.
- [x] hyprexpo Lua-branch flake build check.
- [x] `ryzen-shine` system dry-run.
- [x] `just switch` activates successfully and deploys branch-owned
`~/.config/hypr/hyprland.lua`.
- [x] Re-run checks after Hyprland/Lua input confirmation.
- [ ] Try live compositor smoke test again after version bump.
- [x] Document `--verify-config` caveats for Lua rule/plugin-specific config.
- [x] Eventually run `just switch` only when the branch is coherent enough for a
live test.
Live-smoke note: this Hyprland binary exposes `--verify-config` but no
`--headless` CLI flag. `just switch` now installs the Lua branch binary and
deploys `hyprland.lua`, but the currently running compositor remains the old
0.53 process until the Hyprland session is restarted. A true compositor smoke
test still needs a session restart or a nested Wayland session that avoids
startup side effects.

View File

@@ -43,13 +43,13 @@ Required behavior:
- Moving the focused window to the next empty workspace and following it is a
first-class operation.
- Normal workspaces are bounded to `1..9`.
- Workspace history is tracked per monitor.
- Last-workspace toggle uses the current monitor's workspace history.
- Workspace cycling works on the current monitor within the bounded workspace
set.
Important behavior:
- Workspace history is tracked per monitor.
- Last-workspace toggle uses the current monitor's workspace history.
- Workspace history cycling works on the current monitor within the bounded
workspace set.
- Swapping the current workspace contents with another workspace is available.
- Moving a window to an empty workspace on another monitor is available.
- Moving the focused window to another monitor without following keeps keyboard
@@ -62,6 +62,30 @@ Important behavior:
- Hidden/special workspaces are excluded from the status bar's normal workspace
list.
### Workspace History Cycling
Important behavior:
- The model is most-recently-used workspace switching, scoped to the monitor
where the action starts.
- Each monitor has its own ordered workspace history. The focused monitor's
history is not shared with other monitors.
- Only ordinary bounded workspaces are candidates. Special, scratchpad,
minimized, hidden, and out-of-range workspaces are excluded.
- Starting a cycle freezes the candidate list for that cycle. Previewing
workspaces while the cycle is active must not rewrite the history order.
- Starting a cycle previews the previous workspace for the current monitor.
- Repeating the forward cycle action continues farther back through that
monitor's frozen history.
- A reverse cycle action moves through the same frozen history in the opposite
direction.
- Releasing the initiating modifier key commits the currently previewed
workspace and updates history exactly once.
- A cancel path may return to the workspace where the cycle started.
This behavior is important for workflow continuity, but it is not a hard
requirement for a minimal daily-driver window manager.
## Directional Navigation
Required behavior:
@@ -74,6 +98,14 @@ Required behavior:
- Moving the focused window to an empty workspace on the monitor in a direction
remains required behavior, but it should not require an extra `Hyper`
modifier beyond `Shift`.
- `Super+w/a/s/d` focuses windows directionally.
- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally.
- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that
direction while preserving useful focus.
- `Super+Ctrl+Shift+w/a/s/d` moves the focused window to an empty workspace on
the monitor in that direction.
- `Hyper+w/a/s/d` focuses monitors directionally.
- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally.
- Directional focus in tabbed/fullscreen mode should cycle predictably through
windows even though their screen geometry overlaps.
@@ -88,6 +120,7 @@ Required behavior:
- Tiling is dynamic.
- Primary layout is equal-width vertical columns.
- Scrolling layouts are not acceptable.
- All ordinary splits are vertical.
- Adding windows dynamically redistributes all tiled windows evenly.
- Removing windows dynamically redistributes all tiled windows evenly.
@@ -100,6 +133,9 @@ Required behavior:
- Dialogs are centered.
- There is a command to jump directly to the columns layout and one to jump
directly to the tabbed/fullscreen layout.
- `Super+Ctrl+Space` jumps directly to the tabbed/fullscreen layout.
- Direct fullscreen or floating-fullscreen behavior should not have a
keybinding.
- Layout state is per workspace when the compositor supports it.
Important behavior:
@@ -109,7 +145,6 @@ Important behavior:
Nice behavior:
- Gaps can be toggled.
- Fullscreen can be toggled.
- Smart borders can be toggled.
- Layout-related modifiers remain available for experiments.
- Inactive windows are slightly dimmed when supported.
@@ -144,9 +179,7 @@ Important behavior:
Required behavior:
- A named scratchpad exists for element.
- A named scratchpad exists for gmail.
- A named scratchpad exists for htop.
- A named scratchpad exists for messages.
- A named scratchpad exists for slack.
- A named scratchpad exists for spotify.
- A named scratchpad exists for transmission.
@@ -235,6 +268,8 @@ Important behavior:
Nice behavior:
- Wallpaper behavior remains consistent.
- Wallpaper selection uses `Hyper+comma`; `Hyper+w/a/s/d` are reserved for
directional monitor focus.
- Idle behavior remains consistent.
- Lock behavior remains consistent.
- Clipboard history behavior remains consistent.
@@ -263,12 +298,14 @@ Required behavior:
- `Super+p` opens the application launcher.
- `Super+Shift+p` opens the run menu.
- `Super+Shift+Return` opens a terminal.
- `Super+q` reloads the window manager config.
- `Super+Shift+c` closes the focused window.
- `Super+Shift+q` exits the window manager session.
- `Super+Tab` opens the overview.
- `Super+Shift+Tab` opens the overview in bring-window mode when supported.
- `Super+g` opens the go-to-window picker.
- `Super+b` opens the bring-window picker.
- `Super+Shift+b` opens the replace-window picker.
- `Super+\` toggles to the previous workspace on the current monitor.
- `Super+Shift+e` moves the focused window to the next empty workspace and
follows it. This is the target replacement for the older `Super+Shift+h`
binding.
@@ -276,6 +313,13 @@ Required behavior:
- `Hyper+5` swaps the current workspace with a selected workspace.
- `Hyper+g` gathers windows of the focused class onto the current workspace.
Important behavior:
- `Super+\` starts or advances current-monitor workspace history cycling.
- `Super+/` reverses current-monitor workspace history cycling while the
initiating `Super` key is held.
- Releasing the initiating `Super` key commits the workspace history cycle.
### Directional Navigation Bindings
Required behavior:
@@ -306,9 +350,7 @@ Required behavior:
Required behavior:
- `Super+Alt+e` toggles the element scratchpad.
- `Super+Alt+g` toggles the gmail scratchpad.
- `Super+Alt+h` toggles the htop scratchpad.
- `Super+Alt+m` toggles the messages scratchpad.
- `Super+Alt+k` toggles the slack scratchpad.
- `Super+Alt+s` toggles the spotify scratchpad.
- `Super+Alt+t` toggles the transmission scratchpad.

View File

@@ -3,3 +3,5 @@
!AGENTS.md
!config.toml
!skills
# Generated/local Codex state, including config.local.toml, stays ignored.

View File

@@ -2,147 +2,8 @@ model = "gpt-5.5"
model_reasoning_effort = "high"
personality = "pragmatic"
notify = ["/Users/kat/dotfiles/dotfiles/codex/plugins/cache/openai-bundled/computer-use/1.0.755/Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient", "turn-ended"]
[projects."/home/imalison/Projects/nixpkgs"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles"]
trust_level = "trusted"
[projects."/home/imalison/Projects/railbird"]
trust_level = "trusted"
[projects."/home/imalison/Projects/subtr-actor"]
trust_level = "trusted"
[projects."/home/imalison/Projects/google-messages-api"]
trust_level = "trusted"
[projects."/home/imalison"]
trust_level = "trusted"
[projects."/home/imalison/Projects/scrobble-scrubber"]
trust_level = "trusted"
[projects."/home/imalison/temp"]
trust_level = "trusted"
[projects."/home/imalison/Projects/org-agenda-api"]
trust_level = "untrusted"
[projects."/home/imalison/org"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/.git/modules/dotfiles/config/taffybar"]
trust_level = "trusted"
[projects."/home/imalison/Projects/notifications-tray-icon"]
trust_level = "trusted"
[projects."/home/imalison/Projects/hyprland"]
trust_level = "trusted"
[projects."/home/imalison/Projects/git-sync-rs"]
trust_level = "trusted"
[projects."/home/imalison/Projects/keepbook"]
trust_level = "trusted"
[projects."/home/imalison/Projects/boxcars"]
trust_level = "trusted"
[projects."/home/imalison/Projects/rumno"]
trust_level = "trusted"
[projects."/home/imalison/Projects/git-blame-rank"]
trust_level = "trusted"
[projects."/home/imalison/Projects/hatchet"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/dotfiles/emacs.d/elpaca/sources/org-project-capture"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar/packages"]
trust_level = "trusted"
[projects."/home/imalison/Projects/scrobble-tools"]
trust_level = "trusted"
[projects."/home/imalison/.password-store"]
trust_level = "trusted"
[projects."/home/imalison/Projects/subtr-actor-mechanics"]
trust_level = "trusted"
[projects."/home/imalison/Projects/lastfm-edit"]
trust_level = "trusted"
[projects."/home/imalison/Projects/mova"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"]
trust_level = "trusted"
[projects."/home/imalison/Projects"]
trust_level = "trusted"
[projects."/home/imalison/Projects/rofi-systemd"]
trust_level = "trusted"
[projects."/home/imalison/Projects/map-quiz"]
trust_level = "trusted"
[projects."/run/media/imalison/NETDEBUGUSB"]
trust_level = "trusted"
[projects."/home/imalison/Projects/coqui-tts-streamer"]
trust_level = "trusted"
[projects."/home/imalison/Downloads"]
trust_level = "trusted"
[projects."/home/imalison/keysmith_generated"]
trust_level = "trusted"
[projects."/run/media/imalison/NIXOS_SD"]
trust_level = "trusted"
[projects."/Users/kat/dotfiles"]
trust_level = "trusted"
[projects."/Users/kat"]
trust_level = "trusted"
[projects."/Users/kat/org"]
trust_level = "trusted"
[projects."/Users/kat/Documents/Codex/2026-04-25/do-you-see-the-sandisk-external"]
trust_level = "trusted"
[projects."/Volumes/Extreme SSD/Projects/keepbook"]
trust_level = "trusted"
[projects."/Users/kat/Documents/Codex/2026-04-25/it-seems-like-maybe-we-dont"]
trust_level = "trusted"
[projects."/Users/kat/Documents/Codex/2026-04-25/what-is-the-state-of-tiling"]
trust_level = "trusted"
[projects."/home/imalison/Pictures/ai/2026/celeb"]
trust_level = "trusted"
[projects."/home/imalison/.local/share/keepbook"]
trust_level = "trusted"
[notice]
hide_gpt5_1_migration_prompt = true
"hide_gpt-5.1-codex-max_migration_prompt" = true
[notice.model_migrations]
"gpt-5.2" = "gpt-5.2-codex"
# Portable Codex defaults. Machine-local additions are appended from
# dotfiles/codex/config.local.toml by Home Manager.
[mcp_servers.chrome-devtools]
command = "npx"
@@ -160,16 +21,6 @@ unified_exec = true
apps = true
steer = true
[marketplaces.openai-bundled]
last_updated = "2026-04-21T17:43:57Z"
source_type = "local"
source = "/Users/kat/.codex/.tmp/bundled-marketplaces/openai-bundled"
[marketplaces.openai-primary-runtime]
last_updated = "2026-04-25T23:49:36Z"
source_type = "local"
source = "/Users/kat/.cache/codex-runtimes/codex-primary-runtime/plugins/openai-primary-runtime"
[plugins."google-calendar@openai-curated"]
enabled = true
@@ -196,6 +47,3 @@ enabled = true
[plugins."browser-use@openai-bundled"]
enabled = true
[tui.model_availability_nux]
"gpt-5.5" = 4

View File

@@ -1,11 +1,10 @@
general {
lock_cmd = pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch dpms on
}
listener {
timeout = 900
on-timeout = hypr-screensaver stop && hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on
timeout = 300
on-timeout = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver start
on-resume = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop
}

View File

@@ -1,562 +0,0 @@
# Hyprland Configuration
# XMonad-like dynamic tiling using hy3 plugin
# Based on XMonad configuration from xmonad.hs
# =============================================================================
# PLUGINS (Hyprland pinned to 0.53.0 to match hy3)
# =============================================================================
# Load the plugin before parsing keybinds/layouts that depend on it
plugin = /run/current-system/sw/lib/libhy3.so
plugin = /run/current-system/sw/lib/libhyprexpo.so
# =============================================================================
# MONITORS
# =============================================================================
monitor=,preferred,auto,1
# =============================================================================
# PROGRAMS
# =============================================================================
$terminal = ghostty --gtk-single-instance=false
$fileManager = dolphin
$menu = rofi -show drun -show-icons
$runMenu = rofi -show run
# =============================================================================
# ENVIRONMENT VARIABLES
# =============================================================================
env = XCURSOR_SIZE,24
env = QT_QPA_PLATFORMTHEME,qt5ct
# Used by ~/.config/hypr/scripts/* to keep workspace IDs bounded.
env = HYPR_MAX_WORKSPACE,9
# =============================================================================
# INPUT CONFIGURATION
# =============================================================================
input {
kb_layout = us
kb_variant =
kb_model =
kb_options =
kb_rules =
follow_mouse = 1
touchpad {
natural_scroll = no
}
sensitivity = 0
}
# Cursor warping behavior
cursor {
persistent_warps = true
}
# =============================================================================
# GENERAL SETTINGS
# =============================================================================
general {
gaps_in = 5
gaps_out = 10
border_size = 0
col.active_border = rgba(edb443ee) rgba(33ccffee) 45deg
col.inactive_border = rgba(595959aa)
# Use hy3 layout for XMonad-like dynamic tiling
layout = hy3
allow_tearing = false
}
# =============================================================================
# DECORATION
# =============================================================================
decoration {
rounding = 5
blur {
enabled = true
size = 3
passes = 1
}
# Fade inactive windows (like XMonad's fadeInactive)
active_opacity = 1.0
inactive_opacity = 0.9
}
# =============================================================================
# ANIMATIONS
# =============================================================================
animations {
enabled = yes
# Hyprland supports bezier curves, not true spring physics.
# Use a mild overshoot plus GNOME-like window animation style.
bezier = overshoot, 0.05, 0.9, 0.1, 1.1
bezier = smoothOut, 0.36, 1, 0.3, 1
bezier = smoothInOut, 0.42, 0, 0.58, 1
bezier = linear, 0, 0, 1, 1
# SPEED is in deciseconds (e.g. 6 == 600ms).
animation = windows, 1, 6, overshoot, gnomed
animation = windowsIn, 1, 6, overshoot, gnomed
animation = windowsOut, 1, 5, smoothInOut, gnomed
animation = windowsMove, 1, 6, smoothOut
animation = border, 0
animation = borderangle, 0
animation = fade, 1, 5, smoothOut
animation = workspaces, 1, 6, smoothOut, slidefade 15%
animation = specialWorkspace, 1, 6, smoothOut, slidevert
}
# =============================================================================
# MASTER LAYOUT CONFIGURATION
# =============================================================================
master {
new_status = slave
mfact = 0.5
orientation = left
}
# Dwindle layout (alternative - binary tree like i3)
dwindle {
pseudotile = yes
preserve_split = yes
}
# =============================================================================
# WORKSPACE RULES (SMART GAPS)
# =============================================================================
# Replace no_gaps_when_only (removed in newer Hyprland)
# Remove gaps when there's only one visible tiled window (ignore special workspaces)
workspace = w[tv1]s[false], gapsout:0, gapsin:0
workspace = f[1]s[false], gapsout:0, gapsin:0
# Group/tabbed window configuration (built-in alternative to hy3 tabs)
group {
col.border_active = rgba(edb443ff)
col.border_inactive = rgba(091f2eff)
groupbar {
enabled = true
font_size = 12
height = 22
col.active = rgba(edb443ff)
col.inactive = rgba(091f2eff)
text_color = rgba(091f2eff)
}
}
# =============================================================================
# HY3/HYPREXPO PLUGIN CONFIG
# =============================================================================
plugin {
hy3 {
# Disable autotile to get XMonad-like manual control
autotile {
enable = false
}
# Tab configuration
tabs {
height = 22
padding = 6
render_text = true
text_font = "Sans"
text_height = 10
text_padding = 3
col.active = rgba(edb443ff)
col.inactive = rgba(091f2eff)
col.urgent = rgba(ff0000ff)
col.text.active = rgba(091f2eff)
col.text.inactive = rgba(ffffffff)
col.text.urgent = rgba(ffffffff)
}
}
hyprexpo {
# Always include workspace 1 in the overview grid
workspace_method = first 1
# Only show workspaces with windows
skip_empty = true
# Show numeric workspace labels in the expo grid
show_workspace_numbers = true
# 3 columns -> 3x3 grid when 9 workspaces are visible
columns = 3
}
}
# =============================================================================
# MISC
# =============================================================================
misc {
force_default_wallpaper = 0
disable_hyprland_logo = true
}
# =============================================================================
# BINDS OPTIONS
# =============================================================================
binds {
# Keep workspace history so "previous" can toggle back reliably.
allow_workspace_cycles = true
workspace_back_and_forth = true
}
# =============================================================================
# WINDOW RULES
# =============================================================================
# Float dialogs
windowrule = match:class ^()$, match:title ^()$, float on
windowrule = match:title ^(Picture-in-Picture)$, float on
windowrule = match:title ^(Open File)$, float on
windowrule = match:title ^(Save File)$, float on
windowrule = match:title ^(Confirm)$, float on
# Rumno OSD/notifications: treat as an overlay, not a "real" managed window.
# (Matches both class and title because rumno may set either depending on backend.)
windowrule = match:class ^(.*[Rr]umno.*)$, float on
windowrule = match:class ^(.*[Rr]umno.*)$, pin on
windowrule = match:class ^(.*[Rr]umno.*)$, center on
windowrule = match:class ^(.*[Rr]umno.*)$, decorate off
windowrule = match:class ^(.*[Rr]umno.*)$, no_shadow on
windowrule = match:title ^(.*[Rr]umno.*)$, float on
windowrule = match:title ^(.*[Rr]umno.*)$, pin on
windowrule = match:title ^(.*[Rr]umno.*)$, center on
windowrule = match:title ^(.*[Rr]umno.*)$, decorate off
windowrule = match:title ^(.*[Rr]umno.*)$, no_shadow on
# Scratchpad sizing handled by hyprscratch exec rules (see hyprland.nix)
# Using hyprscratch rules instead of windowrule to avoid affecting child windows (e.g. Slack meets)
# =============================================================================
# KEY BINDINGS
# =============================================================================
# Modifier keys
$mainMod = SUPER
$modAlt = SUPER ALT
$hyper = SUPER CTRL ALT
# -----------------------------------------------------------------------------
# Program Launching
# -----------------------------------------------------------------------------
bind = $mainMod, P, exec, $menu
bind = $mainMod SHIFT, P, exec, $runMenu
bind = $mainMod SHIFT, Return, exec, $terminal
# -----------------------------------------------------------------------------
# Overview (Hyprexpo)
# -----------------------------------------------------------------------------
bind = $mainMod, TAB, hyprexpo:expo, toggle
bind = $mainMod SHIFT, TAB, hyprexpo:expo, bring
bind = $mainMod, Q, killactive,
bind = $mainMod SHIFT, C, killactive,
bind = $mainMod SHIFT, Q, exit,
# Emacs-everywhere (like XMonad's emacs-everywhere)
bind = $mainMod, E, exec, emacsclient --eval '(emacs-everywhere)'
bind = $mainMod, V, exec, wl-paste | xdotool type --file -
# Chrome/Browser (raise or spawn like XMonad's bindBringAndRaise)
bind = $modAlt, C, exec, ~/.config/hypr/scripts/raise-or-run.sh google-chrome google-chrome-stable
# -----------------------------------------------------------------------------
# SCRATCHPADS (managed by hyprscratch daemon with auto-dismiss)
# -----------------------------------------------------------------------------
bind = $modAlt, E, exec, hyprscratch toggle element
bind = $modAlt, G, exec, hyprscratch toggle gmail
bind = $modAlt, H, exec, hyprscratch toggle htop
bind = $modAlt, M, exec, hyprscratch toggle messages
bind = $modAlt, K, exec, hyprscratch toggle slack
bind = $modAlt, S, exec, hyprscratch toggle spotify
bind = $modAlt, T, exec, hyprscratch toggle transmission
bind = $modAlt, V, exec, hyprscratch toggle volume
bind = $modAlt, grave, exec, hyprscratch toggle dropdown
# Hidden workspace (like XMonad's NSP)
bind = $mainMod, X, movetoworkspace, special:NSP
bind = $mainMod SHIFT, X, togglespecialworkspace, NSP
# -----------------------------------------------------------------------------
# DIRECTIONAL NAVIGATION (WASD - like XMonad Navigation2D)
# Using hy3 dispatchers for proper tree-based navigation
# -----------------------------------------------------------------------------
# Focus movement (Mod + WASD) - hy3:movefocus navigates the tree
bind = $mainMod, W, hy3:movefocus, u
bind = $mainMod, S, hy3:movefocus, d
bind = $mainMod, A, hy3:movefocus, l
bind = $mainMod, D, hy3:movefocus, r
# Move windows (Mod + Shift + WASD) - hy3:movewindow with once=true for swapping
bind = $mainMod SHIFT, W, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh u once
bind = $mainMod SHIFT, S, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh d once
bind = $mainMod SHIFT, A, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh l once
bind = $mainMod SHIFT, D, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh r once
# Resize windows (Mod + Ctrl + WASD)
binde = $mainMod CTRL, W, resizeactive, 0 -50
binde = $mainMod CTRL, S, resizeactive, 0 50
binde = $mainMod CTRL, A, resizeactive, -50 0
binde = $mainMod CTRL, D, resizeactive, 50 0
# Screen/Monitor focus (Hyper + WASD)
bind = $hyper, W, focusmonitor, u
bind = $hyper, S, focusmonitor, d
bind = $hyper, A, focusmonitor, l
bind = $hyper, D, focusmonitor, r
# Move window to monitor and follow (Hyper + Shift + WASD)
bind = $hyper SHIFT, W, movewindow, mon:u
bind = $hyper SHIFT, S, movewindow, mon:d
bind = $hyper SHIFT, A, movewindow, mon:l
bind = $hyper SHIFT, D, movewindow, mon:r
# Shift to empty workspace on screen direction (Hyper + Ctrl + WASD)
# Like XMonad's shiftToEmptyOnScreen
bind = $hyper CTRL, W, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh u
bind = $hyper CTRL, S, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh d
bind = $hyper CTRL, A, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh l
bind = $hyper CTRL, D, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh r
# -----------------------------------------------------------------------------
# LAYOUT CONTROL (XMonad-like with hy3)
# -----------------------------------------------------------------------------
# Create groups with different orientations (like XMonad layouts)
# hy3:makegroup creates a split/tab group from focused window
bind = $mainMod, Space, hy3:changegroup, toggletab
bind = $mainMod SHIFT, Space, hy3:changegroup, opposite
# Create specific group types
bind = $mainMod, H, hy3:makegroup, h
bind = $mainMod SHIFT, V, hy3:makegroup, v
# Mod+Ctrl+Space mirrors Mod+Space (tabs instead of fullscreen)
bind = $mainMod CTRL, Space, hy3:changegroup, toggletab
# Change group type (cycle h -> v -> tab)
bind = $mainMod, slash, hy3:changegroup, h
bind = $mainMod SHIFT, slash, hy3:changegroup, v
# Tab navigation (like XMonad's focus next/prev in tabbed)
bind = $mainMod, bracketright, hy3:focustab, r, wrap
bind = $mainMod, bracketleft, hy3:focustab, l, wrap
# Move window within tab group (hy3 has no movetab dispatcher)
bind = $mainMod SHIFT, bracketright, hy3:movewindow, r, visible
bind = $mainMod SHIFT, bracketleft, hy3:movewindow, l, visible
# Expand focus to parent group (like XMonad's focus parent)
bind = $mainMod, grave, hy3:expand, expand
bind = $mainMod SHIFT, grave, hy3:expand, base
# Fullscreen (like XMonad's NBFULL toggle)
bind = $mainMod, F, fullscreen, 0
bind = $mainMod SHIFT, F, fullscreen, 1
# Toggle floating
bind = $mainMod, T, togglefloating,
# Resize split ratio (hy3 uses resizeactive for splits)
binde = $mainMod, comma, resizeactive, -50 0
binde = $mainMod, period, resizeactive, 50 0
# Equalize window sizes on workspace (hy3)
bind = $mainMod SHIFT, equal, hy3:equalize, workspace
# Kill group - removes the focused window from its group
bind = $mainMod, N, hy3:killactive
# hy3:setswallow - set a window to swallow newly spawned windows
bind = $mainMod CTRL, M, hy3:setswallow, toggle
# Minimize/unminimize (via special workspace)
bind = $mainMod, M, exec, ~/.config/hypr/scripts/minimize-active.sh minimized
bind = $mainMod SHIFT, M, exec, ~/.config/hypr/scripts/unminimize-last.sh minimized
# Minimized "picker" mode:
# Open the minimized special workspace, focus a window, press Enter to restore it.
bind = $modAlt, Return, exec, ~/.config/hypr/scripts/minimized-mode.sh minimized
submap = minimized
bind = , Return, exec, ~/.config/hypr/scripts/unminimize-last.sh minimized; hyprctl dispatch submap reset
bind = , Escape, exec, ~/.config/hypr/scripts/minimized-cancel.sh minimized
bind = $modAlt, Return, exec, ~/.config/hypr/scripts/minimized-cancel.sh minimized
# Optional: basic focus navigation inside the picker.
bind = , H, movefocus, l
bind = , J, movefocus, d
bind = , K, movefocus, u
bind = , L, movefocus, r
bind = , left, movefocus, l
bind = , down, movefocus, d
bind = , up, movefocus, u
bind = , right, movefocus, r
submap = reset
# -----------------------------------------------------------------------------
# WORKSPACE CONTROL
# -----------------------------------------------------------------------------
# Switch workspaces (1-9 only) on the currently focused monitor.
bind = $mainMod, 1, focusworkspaceoncurrentmonitor, 1
bind = $mainMod, 2, focusworkspaceoncurrentmonitor, 2
bind = $mainMod, 3, focusworkspaceoncurrentmonitor, 3
bind = $mainMod, 4, focusworkspaceoncurrentmonitor, 4
bind = $mainMod, 5, focusworkspaceoncurrentmonitor, 5
bind = $mainMod, 6, focusworkspaceoncurrentmonitor, 6
bind = $mainMod, 7, focusworkspaceoncurrentmonitor, 7
bind = $mainMod, 8, focusworkspaceoncurrentmonitor, 8
bind = $mainMod, 9, focusworkspaceoncurrentmonitor, 9
# Move window to workspace
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
# Move and follow to workspace (like XMonad's shiftThenView)
bind = $mainMod CTRL, 1, movetoworkspacesilent, 1
bind = $mainMod CTRL, 1, focusworkspaceoncurrentmonitor, 1
bind = $mainMod CTRL, 2, movetoworkspacesilent, 2
bind = $mainMod CTRL, 2, focusworkspaceoncurrentmonitor, 2
bind = $mainMod CTRL, 3, movetoworkspacesilent, 3
bind = $mainMod CTRL, 3, focusworkspaceoncurrentmonitor, 3
bind = $mainMod CTRL, 4, movetoworkspacesilent, 4
bind = $mainMod CTRL, 4, focusworkspaceoncurrentmonitor, 4
bind = $mainMod CTRL, 5, movetoworkspacesilent, 5
bind = $mainMod CTRL, 5, focusworkspaceoncurrentmonitor, 5
bind = $mainMod CTRL, 6, movetoworkspacesilent, 6
bind = $mainMod CTRL, 6, focusworkspaceoncurrentmonitor, 6
bind = $mainMod CTRL, 7, movetoworkspacesilent, 7
bind = $mainMod CTRL, 7, focusworkspaceoncurrentmonitor, 7
bind = $mainMod CTRL, 8, movetoworkspacesilent, 8
bind = $mainMod CTRL, 8, focusworkspaceoncurrentmonitor, 8
bind = $mainMod CTRL, 9, movetoworkspacesilent, 9
bind = $mainMod CTRL, 9, focusworkspaceoncurrentmonitor, 9
# Toggle to the previous workspace on the current monitor using Hyprland's
# built-in per-monitor workspace history.
bind = $mainMod, backslash, workspace, previous_per_monitor
# Swap current workspace with another (like XMonad's swapWithCurrent)
bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh
# Go to next empty workspace (like XMonad's moveTo Next emptyWS)
bind = $hyper, E, exec, ~/.config/hypr/scripts/workspace-goto-empty.sh
# Move to next screen (like XMonad's shiftToNextScreenX)
bind = $mainMod, Z, focusmonitor, +1
bind = $mainMod SHIFT, Z, movewindow, mon:+1
# Shift to empty workspace and view (like XMonad's shiftToEmptyAndView)
bind = $mainMod SHIFT, H, exec, ~/.config/hypr/scripts/workspace-move-to-empty.sh
# -----------------------------------------------------------------------------
# WINDOW MANAGEMENT
# -----------------------------------------------------------------------------
# Go to window (rofi window switcher with icons)
bind = $mainMod, G, exec, ~/.config/hypr/scripts/go-to-window.sh
# Bring window (move to current workspace)
bind = $mainMod, B, exec, ~/.config/hypr/scripts/bring-window.sh
# Replace window (swap focused with selected - like XMonad's myReplaceWindow)
bind = $mainMod SHIFT, B, exec, ~/.config/hypr/scripts/replace-window.sh
# Gather windows of same class (like XMonad's gatherThisClass)
bind = $hyper, G, exec, ~/.config/hypr/scripts/gather-class.sh
# Focus next window of different class (like XMonad's focusNextClass)
bind = $mainMod, apostrophe, exec, ~/.config/hypr/scripts/focus-next-class.sh
# -----------------------------------------------------------------------------
# MEDIA KEYS
# -----------------------------------------------------------------------------
# Volume control (matching XMonad: Mod+I=up, Mod+K=down, Mod+U=mute)
binde = , XF86AudioRaiseVolume, exec, set_volume --unmute --change-volume +5
binde = , XF86AudioLowerVolume, exec, set_volume --unmute --change-volume -5
bind = , XF86AudioMute, exec, set_volume --toggle-mute
binde = $mainMod, I, exec, set_volume --unmute --change-volume +5
binde = $mainMod, K, exec, set_volume --unmute --change-volume -5
bind = $mainMod, U, exec, set_volume --toggle-mute
# Media player controls (matching XMonad: Mod+;=play, Mod+L=next, Mod+J=prev)
bind = $mainMod, semicolon, exec, playerctl play-pause
bind = , XF86AudioPlay, exec, playerctl play-pause
bind = , XF86AudioPause, exec, playerctl play-pause
bind = $mainMod, L, exec, playerctl next
bind = , XF86AudioNext, exec, playerctl next
bind = $mainMod, J, exec, playerctl previous
bind = , XF86AudioPrev, exec, playerctl previous
# Mute current window (like XMonad's toggle_mute_current_window)
bind = $hyper SHIFT, Q, exec, toggle_mute_current_window.sh
bind = $hyper CTRL, Q, exec, toggle_mute_current_window.sh only
# Brightness control
binde = , XF86MonBrightnessUp, exec, brightness.sh up
binde = , XF86MonBrightnessDown, exec, brightness.sh down
# -----------------------------------------------------------------------------
# UTILITY BINDINGS
# -----------------------------------------------------------------------------
bind = $hyper, V, exec, cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy
bind = $hyper, P, exec, rofi-pass
bind = $hyper, H, exec, grim -g "$(slurp)" - | swappy -f -
bind = $hyper, C, exec, shell_command.sh
bind = $hyper, X, exec, rofi_command.sh
bind = $hyper SHIFT, L, exec, hyprlock
bind = $hyper, K, exec, rofi_kill_process.sh
bind = $hyper SHIFT, K, exec, rofi_kill_all.sh
bind = $hyper, R, exec, rofi-systemd
bind = $hyper, slash, exec, toggle_taffybar
bind = $hyper, 9, exec, start_synergy.sh
bind = $hyper, I, exec, rofi_select_input.hs
bind = $hyper, backslash, exec, /home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle
bind = $hyper, O, exec, rofi_paswitch
bind = $hyper, W, exec, rofi_wallpaper.sh
bind = $hyper, Y, exec, rofi_agentic_skill
# Reload config
bind = $mainMod, R, exec, hyprctl reload
# -----------------------------------------------------------------------------
# MOUSE BINDINGS
# -----------------------------------------------------------------------------
bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
# Scroll through workspaces
bind = $mainMod, mouse_down, exec, ~/.config/hypr/scripts/workspace-scroll.sh +1
bind = $mainMod, mouse_up, exec, ~/.config/hypr/scripts/workspace-scroll.sh -1
# =============================================================================
# AUTOSTART
# =============================================================================
# Wire Hyprland into Home Manager's standard user-session targets.
# `graphical-session.target` pulls in most tray/SNI applets (which in turn pull in `tray.target`).
# Keep the systemd user manager in sync with the current Hyprland session before
# starting any session-bound units. Separate `exec-once` commands race.
exec-once = sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'
# Force a fresh daemon after compositor restarts so hyprscratch doesn't keep a stale socket.
exec-once = systemctl --user restart hyprscratch.service
exec-once = hypridle
# Clipboard history daemon
exec-once = wl-paste --type text --watch cliphist store
exec-once = wl-paste --type image --watch cliphist store

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Bring window to current workspace (like XMonad's bringWindow)
# Uses rofi with icons to select a window, then moves it here.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/window-icon-map.sh"
CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id')
# Get windows on OTHER workspaces as TSV
WINDOW_DATA=$(hyprctl clients -j | jq -r --argjson cws "$CURRENT_WS" '
.[] | select(.workspace.id >= 0 and .workspace.id != $cws)
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
| @tsv')
if [ -z "$WINDOW_DATA" ]; then
notify-send "Bring Window" "No windows on other workspaces"
exit 0
fi
addresses=()
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
while IFS=$'\t' read -r address class title ws_id; do
icon=$(icon_for_class "$class")
addresses+=("$address")
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
"$class" "$title" "$ws_id" "$icon"
done <<< "$WINDOW_DATA" > "$TMPFILE"
INDEX=$(rofi -dmenu -i -show-icons -p "Bring window" -format i < "$TMPFILE") || exit 0
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
ADDRESS="${addresses[$INDEX]}"
hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDRESS"
hyprctl dispatch focuswindow "address:$ADDRESS"
fi

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
# Cycle between master and dwindle layouts
# Like XMonad's NextLayout
set -euo pipefail
CURRENT=$(hyprctl getoption general:layout -j | jq -r '.str')
if [ "$CURRENT" = "master" ]; then
hyprctl keyword general:layout dwindle
notify-send "Layout" "Switched to Dwindle (binary tree)"
else
hyprctl keyword general:layout master
notify-send "Layout" "Switched to Master (XMonad-like)"
fi

View File

@@ -1,72 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Print an "empty" workspace id within 1..$HYPR_MAX_WORKSPACE (default 9).
#
# Preference order (lowest id wins within each tier):
# 1. Workspace exists on the target monitor and has 0 windows
# 2. Workspace id does not exist at all (will be created on dispatch)
# 3. Workspace exists (elsewhere) and has 0 windows
#
# Usage:
# find-empty-workspace.sh [monitor] [exclude_id]
max_ws="${HYPR_MAX_WORKSPACE:-9}"
monitor="${1:-}"
exclude_id="${2:-}"
if [[ -z "${monitor}" ]]; then
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
fi
if [[ -z "${monitor}" || "${monitor}" == "null" ]]; then
exit 1
fi
workspaces_json="$(hyprctl workspaces -j 2>/dev/null || echo '[]')"
unused_candidate=""
elsewhere_empty_candidate=""
for i in $(seq 1 "${max_ws}"); do
if [[ -n "${exclude_id}" && "${i}" == "${exclude_id}" ]]; then
continue
fi
exists="$(jq -r --argjson id "${i}" '[.[] | select(.id == $id)] | length' <<<"${workspaces_json}")"
if [[ "${exists}" == "0" ]]; then
if [[ -z "${unused_candidate}" ]]; then
unused_candidate="${i}"
fi
continue
fi
windows="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .windows] | .[0]) // 0' <<<"${workspaces_json}")"
if [[ "${windows}" != "0" ]]; then
continue
fi
ws_monitor="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .monitor] | .[0]) // ""' <<<"${workspaces_json}")"
if [[ "${ws_monitor}" == "${monitor}" ]]; then
printf '%s\n' "${i}"
exit 0
fi
if [[ -z "${elsewhere_empty_candidate}" ]]; then
elsewhere_empty_candidate="${i}"
fi
done
if [[ -n "${unused_candidate}" ]]; then
printf '%s\n' "${unused_candidate}"
exit 0
fi
if [[ -n "${elsewhere_empty_candidate}" ]]; then
printf '%s\n' "${elsewhere_empty_candidate}"
exit 0
fi
exit 1

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
# Focus next window of a different class (like XMonad's focusNextClass)
set -euo pipefail
# Get focused window class
FOCUSED_CLASS=$(hyprctl activewindow -j | jq -r '.class')
FOCUSED_ADDR=$(hyprctl activewindow -j | jq -r '.address')
if [ "$FOCUSED_CLASS" = "null" ] || [ -z "$FOCUSED_CLASS" ]; then
# No focused window, just focus any window
hyprctl dispatch cyclenext
exit 0
fi
# Get all unique classes
ALL_CLASSES=$(hyprctl clients -j | jq -r '[.[] | select(.workspace.id >= 0) | .class] | unique | .[]')
# Get sorted list of classes
CLASSES_ARRAY=()
while IFS= read -r class; do
CLASSES_ARRAY+=("$class")
done <<< "$ALL_CLASSES"
# Find current class index and get next class
CURRENT_INDEX=-1
for i in "${!CLASSES_ARRAY[@]}"; do
if [ "${CLASSES_ARRAY[$i]}" = "$FOCUSED_CLASS" ]; then
CURRENT_INDEX=$i
break
fi
done
if [ $CURRENT_INDEX -eq -1 ] || [ ${#CLASSES_ARRAY[@]} -le 1 ]; then
# Only one class or class not found
exit 0
fi
# Get next class (wrapping around)
NEXT_INDEX=$(( (CURRENT_INDEX + 1) % ${#CLASSES_ARRAY[@]} ))
NEXT_CLASS="${CLASSES_ARRAY[$NEXT_INDEX]}"
# Find first window of next class
NEXT_WINDOW=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$NEXT_CLASS\" and .workspace.id >= 0) | .address" | head -1)
if [ -n "$NEXT_WINDOW" ]; then
hyprctl dispatch focuswindow "address:$NEXT_WINDOW"
fi

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# Gather all windows of the same class as focused window (like XMonad's gatherThisClass)
set -euo pipefail
# Get focused window class
FOCUSED_CLASS=$(hyprctl activewindow -j | jq -r '.class')
CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id')
if [ "$FOCUSED_CLASS" = "null" ] || [ -z "$FOCUSED_CLASS" ]; then
notify-send "Gather Class" "No focused window"
exit 0
fi
# Find all windows with same class on other workspaces
WINDOWS=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$FOCUSED_CLASS\" and .workspace.id != $CURRENT_WS and .workspace.id >= 0) | .address")
if [ -z "$WINDOWS" ]; then
notify-send "Gather Class" "No other windows of class '$FOCUSED_CLASS'"
exit 0
fi
# Move each window to current workspace
COUNT=0
for ADDR in $WINDOWS; do
hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDR"
COUNT=$((COUNT + 1))
done
notify-send "Gather Class" "Gathered $COUNT windows of class '$FOCUSED_CLASS'"

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
# Go to a window selected via rofi (with icons from desktop entries).
# Replaces "rofi -show window" which doesn't work well on Wayland.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/window-icon-map.sh"
# Get all windows on regular workspaces as TSV
WINDOW_DATA=$(hyprctl clients -j | jq -r '
.[] | select(.workspace.id >= 0)
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
| @tsv')
[ -n "$WINDOW_DATA" ] || exit 0
addresses=()
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
while IFS=$'\t' read -r address class title ws_id; do
icon=$(icon_for_class "$class")
addresses+=("$address")
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
"$class" "$title" "$ws_id" "$icon"
done <<< "$WINDOW_DATA" > "$TMPFILE"
INDEX=$(rofi -dmenu -i -show-icons -p "Go to window" -format i < "$TMPFILE") || exit 0
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
hyprctl dispatch focuswindow "address:${addresses[$INDEX]}"
fi

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Minimize the active window by moving it to a special workspace without
# toggling that special workspace open.
#
# Usage: minimize-active.sh <name>
# Example: minimize-active.sh minimized
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
# We could parse plain output, but jq should exist in this setup; if it
# doesn't, fail soft.
exit 0
fi
ACTIVE_JSON="$(hyprctl -j activewindow 2>/dev/null || true)"
ADDR="$(printf '%s' "$ACTIVE_JSON" | jq -r '.address // empty')"
if [ -z "$ADDR" ] || [ "$ADDR" = "null" ]; then
exit 0
fi
# If the minimized special workspace is currently visible, closing it after the
# move keeps the window hidden (what "minimize" usually means).
MONITOR_ID="$(printf '%s' "$ACTIVE_JSON" | jq -r '.monitor // empty')"
SPECIAL_OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --arg n "special:$NAME" --argjson mid "${MONITOR_ID:-0}" '
.[]
| select(.id == $mid)
| (.specialWorkspace.name // "")
| select(. == $n)
' \
| head -n 1 \
|| true
)"
hyprctl dispatch movetoworkspacesilent "special:${NAME},address:${ADDR}" >/dev/null 2>&1 || true
if [ -n "$SPECIAL_OPEN" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
exit 0

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env bash
# Exit minimized picker mode:
# - Hide the minimized special workspace on the active monitor (if visible)
# - Reset the submap
#
# Usage: minimized-cancel.sh <name>
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
SPECIAL_WS="special:${NAME}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
MONITOR_ID="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.monitorID // empty' || true)"
if [ -z "$MONITOR_ID" ] || [ "$MONITOR_ID" = "null" ]; then
MONITOR_ID=0
fi
OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --argjson mid "$MONITOR_ID" '.[] | select(.id == $mid) | (.specialWorkspace.name // "")' \
| head -n 1 \
|| true
)"
if [ "$OPEN" = "$SPECIAL_WS" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
hyprctl dispatch submap reset >/dev/null 2>&1 || true
exit 0

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Enter a "picker" mode for minimized windows:
# - Ensure the minimized special workspace is visible on the active monitor
# - Switch Hyprland into a submap so Enter restores and Escape cancels
#
# Usage: minimized-mode.sh <name>
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
SPECIAL_WS="special:${NAME}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
MONITOR_ID="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.monitorID // empty' || true)"
if [ -z "$MONITOR_ID" ] || [ "$MONITOR_ID" = "null" ]; then
MONITOR_ID=0
fi
OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --argjson mid "$MONITOR_ID" '.[] | select(.id == $mid) | (.specialWorkspace.name // "")' \
| head -n 1 \
|| true
)"
# Ensure it's visible (but don't toggle it off if already open).
if [ "$OPEN" != "$SPECIAL_WS" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
hyprctl dispatch submap minimized >/dev/null 2>&1 || true
exit 0

View File

@@ -1,83 +0,0 @@
#!/usr/bin/env bash
# Move the active window in a direction and warp the cursor to keep its
# relative position inside the moved window.
set -euo pipefail
export PATH="/run/current-system/sw/bin:${PATH}"
if [[ $# -lt 1 ]]; then
echo "usage: $0 <dir> [mode]" >&2
exit 1
fi
dir="$1"
mode="${2:-}"
if ! command -v hyprctl >/dev/null; then
exit 0
fi
move_window() {
if [[ -n "$mode" ]]; then
hyprctl dispatch hy3:movewindow "$dir, $mode" >/dev/null 2>&1 || true
else
hyprctl dispatch hy3:movewindow "$dir" >/dev/null 2>&1 || true
fi
}
win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
cur_json="$(hyprctl -j cursorpos 2>/dev/null || true)"
if [[ -z "$win_json" || "$win_json" == "null" || -z "$cur_json" || "$cur_json" == "null" ]]; then
move_window
exit 0
fi
win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
cur_x="$(jq -er '.x' <<<"$cur_json" 2>/dev/null || true)"
cur_y="$(jq -er '.y' <<<"$cur_json" 2>/dev/null || true)"
if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ || ! "$cur_x" =~ ^-?[0-9]+$ || ! "$cur_y" =~ ^-?[0-9]+$ ]]; then
move_window
exit 0
fi
rel_x=$((cur_x - win_x))
rel_y=$((cur_y - win_y))
move_window
win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
if [[ -z "$win_json" || "$win_json" == "null" ]]; then
exit 0
fi
win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ ]]; then
exit 0
fi
if ((rel_x < 0)); then
rel_x=0
elif ((rel_x > win_w)); then
rel_x=$win_w
fi
if ((rel_y < 0)); then
rel_y=0
elif ((rel_y > win_h)); then
rel_y=$win_h
fi
new_x=$((win_x + rel_x))
new_y=$((win_y + rel_y))
hyprctl dispatch movecursor "$new_x" "$new_y" >/dev/null 2>&1 || true

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# Raise existing window or run command (like XMonad's raiseNextMaybe)
# Usage: raise-or-run.sh <class-pattern> <command>
set -euo pipefail
CLASS_PATTERN="$1"
COMMAND="$2"
# Find windows matching the class pattern
MATCHING=$(hyprctl clients -j | jq -r ".[] | select(.class | test(\"$CLASS_PATTERN\"; \"i\")) | .address" | head -1)
if [ -n "$MATCHING" ]; then
# Window exists, focus it
hyprctl dispatch focuswindow "address:$MATCHING"
else
# No matching window, run the command
exec $COMMAND
fi

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env bash
# Replace focused window with selected window (like XMonad's myReplaceWindow)
# Swaps the positions of focused window and selected window
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/window-icon-map.sh"
FOCUSED=$(hyprctl activewindow -j | jq -r '.address')
if [ "$FOCUSED" = "null" ] || [ -z "$FOCUSED" ]; then
notify-send "Replace Window" "No focused window"
exit 0
fi
# Get all windows except focused as TSV
WINDOW_DATA=$(hyprctl clients -j | jq -r --arg focused "$FOCUSED" '
.[] | select(.workspace.id >= 0 and .address != $focused)
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
| @tsv')
if [ -z "$WINDOW_DATA" ]; then
notify-send "Replace Window" "No other windows available"
exit 0
fi
addresses=()
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
while IFS=$'\t' read -r address class title ws_id; do
icon=$(icon_for_class "$class")
addresses+=("$address")
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
"$class" "$title" "$ws_id" "$icon"
done <<< "$WINDOW_DATA" > "$TMPFILE"
INDEX=$(rofi -dmenu -i -show-icons -p "Replace with" -format i < "$TMPFILE") || exit 0
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
hyprctl dispatch hy3:movewindow "address:${addresses[$INDEX]}"
fi

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env bash
# Shift window to empty workspace on screen in given direction
# Like XMonad's shiftToEmptyOnScreen
# Usage: shift-to-empty-on-screen.sh <direction: u|d|l|r>
set -euo pipefail
DIRECTION="$1"
max_ws="${HYPR_MAX_WORKSPACE:-9}"
# Track the current monitor so we can return
ORIG_MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
# Move focus to the screen in that direction
hyprctl dispatch focusmonitor "$DIRECTION"
# Get the monitor we're now on (target monitor)
MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
# If there is no monitor in that direction, bail
if [ "$MONITOR" = "$ORIG_MONITOR" ]; then
exit 0
fi
# Find an empty workspace within 1..$HYPR_MAX_WORKSPACE.
EMPTY_WS="$(~/.config/hypr/scripts/find-empty-workspace.sh "${MONITOR}" 2>/dev/null || true)"
if [[ -z "${EMPTY_WS}" ]]; then
# No empty workspace available within the cap; restore focus and bail.
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
exit 0
fi
if (( EMPTY_WS < 1 || EMPTY_WS > max_ws )); then
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
exit 0
fi
# Ensure the workspace exists on the target monitor
hyprctl dispatch workspace "$EMPTY_WS"
# Go back to original monitor and move the window (without following)
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
hyprctl dispatch movetoworkspacesilent "$EMPTY_WS"

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Swap the contents of the current workspace with another workspace.
# Intended to mirror XMonad's swapWithCurrent behavior.
set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
CURRENT_WS="$(hyprctl activeworkspace -j | jq -r '.id')"
if [[ -z "${CURRENT_WS}" || "${CURRENT_WS}" == "null" ]]; then
exit 0
fi
TARGET_WS="${1:-}"
if [[ -z "${TARGET_WS}" ]]; then
WS_LIST="$({
seq 1 "${max_ws}"
hyprctl workspaces -j | jq -r '.[].id' 2>/dev/null || true
} | awk 'NF {print $1}' | awk '!seen[$0]++' | sort -n)"
TARGET_WS="$(printf "%s\n" "${WS_LIST}" | rofi -dmenu -p "Swap with workspace")"
fi
if [[ -z "${TARGET_WS}" || "${TARGET_WS}" == "null" ]]; then
exit 0
fi
if [[ "${TARGET_WS}" == "${CURRENT_WS}" ]]; then
exit 0
fi
if ! [[ "${TARGET_WS}" =~ ^-?[0-9]+$ ]]; then
notify-send "Swap Workspace" "Invalid workspace: ${TARGET_WS}"
exit 1
fi
if (( TARGET_WS < 1 || TARGET_WS > max_ws )); then
notify-send "Swap Workspace" "Workspace out of range (1-${max_ws}): ${TARGET_WS}"
exit 1
fi
WINDOWS_CURRENT="$(hyprctl clients -j | jq -r --arg ws "${CURRENT_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
WINDOWS_TARGET="$(hyprctl clients -j | jq -r --arg ws "${TARGET_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
for ADDR in ${WINDOWS_CURRENT}; do
hyprctl dispatch movetoworkspace "${TARGET_WS},address:${ADDR}"
done
for ADDR in ${WINDOWS_TARGET}; do
hyprctl dispatch movetoworkspace "${CURRENT_WS},address:${ADDR}"
done

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Toggle a named Hyprland scratchpad, spawning it if needed.
# Usage: toggle-scratchpad.sh <name> <class_regex|-> <title_regex|-> <command...>
set -euo pipefail
if [ "$#" -lt 4 ]; then
echo "usage: $0 <name> <class_regex|-> <title_regex|-> <command...>" >&2
exit 1
fi
NAME="$1"
shift
CLASS_REGEX="$1"
shift
TITLE_REGEX="$1"
shift
COMMAND=("$@")
if [ "$CLASS_REGEX" = "-" ]; then
CLASS_REGEX=""
fi
if [ "$TITLE_REGEX" = "-" ]; then
TITLE_REGEX=""
fi
if [ -z "$CLASS_REGEX" ] && [ -z "$TITLE_REGEX" ]; then
echo "toggle-scratchpad: provide a class or title regex" >&2
exit 1
fi
MATCHING=$(hyprctl clients -j | jq -r --arg cre "$CLASS_REGEX" --arg tre "$TITLE_REGEX" '
.[]
| select(
(($cre == "") or (.class | test($cre; "i")))
and
(($tre == "") or (.title | test($tre; "i")))
)
| .address
')
if [ -z "$MATCHING" ]; then
"${COMMAND[@]}" &
else
while IFS= read -r ADDR; do
[ -n "$ADDR" ] || continue
hyprctl dispatch movetoworkspacesilent "special:$NAME,address:$ADDR"
done <<< "$MATCHING"
fi
hyprctl dispatch togglespecialworkspace "$NAME"

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env bash
# Restore a minimized window by moving it out of a special workspace.
#
# Usage: unminimize-last.sh <name>
# Example: unminimize-last.sh minimized
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
SPECIAL_WS="special:${NAME}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
ACTIVE_JSON="$(hyprctl -j activewindow 2>/dev/null || true)"
ACTIVE_ADDR="$(printf '%s' "$ACTIVE_JSON" | jq -r '.address // empty')"
ACTIVE_WS="$(printf '%s' "$ACTIVE_JSON" | jq -r '.workspace.name // empty')"
MONITOR_ID="$(printf '%s' "$ACTIVE_JSON" | jq -r '.monitor // empty')"
# Destination is the normal active workspace for the active monitor.
DEST_WS="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --argjson mid "${MONITOR_ID:-0}" '.[] | select(.id == $mid) | .activeWorkspace.name' \
| head -n 1 \
|| true
)"
if [ -z "$DEST_WS" ] || [ "$DEST_WS" = "null" ]; then
DEST_WS="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.name // empty' || true)"
fi
if [ -z "$DEST_WS" ] || [ "$DEST_WS" = "null" ]; then
exit 0
fi
# If we're focused on a minimized window already, restore that one.
ADDR=""
if [ "$ACTIVE_WS" = "$SPECIAL_WS" ] && [ -n "$ACTIVE_ADDR" ] && [ "$ACTIVE_ADDR" != "null" ]; then
ADDR="$ACTIVE_ADDR"
else
# Otherwise, restore the "most recent" minimized window we can find.
# focusHistoryID tends to have 0 as most recent; pick the smallest value.
ADDR="$(
hyprctl -j clients 2>/dev/null \
| jq -r --arg sw "$SPECIAL_WS" '
[ .[]
| select(.workspace.name == $sw)
| { addr: .address, fh: (.focusHistoryID // 999999999) }
]
| sort_by(.fh)
| (.[0].addr // empty)
' \
| head -n 1 \
|| true
)"
fi
if [ -z "$ADDR" ] || [ "$ADDR" = "null" ]; then
exit 0
fi
hyprctl dispatch movetoworkspacesilent "${DEST_WS},address:${ADDR}" >/dev/null 2>&1 || true
hyprctl dispatch focuswindow "address:${ADDR}" >/dev/null 2>&1 || true
# If the minimized special workspace is currently visible, close it so we don't
# leave things in a special state after a restore.
SPECIAL_OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --arg n "$SPECIAL_WS" --argjson mid "${MONITOR_ID:-0}" '
.[]
| select(.id == $mid)
| (.specialWorkspace.name // "")
| select(. == $n)
' \
| head -n 1 \
|| true
)"
if [ -n "$SPECIAL_OPEN" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
exit 0

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env bash
# Source this file to get icon_for_class function.
# Builds a mapping from window class → freedesktop icon name
# by scanning .desktop files for StartupWMClass and Icon fields.
#
# Usage:
# source "$(dirname "$0")/window-icon-map.sh"
# icon=$(icon_for_class "google-chrome")
declare -A _WINDOW_ICON_MAP
_build_window_icon_map() {
local IFS=':'
local -a search_dirs=()
local dir
for dir in ${XDG_DATA_DIRS:-/run/current-system/sw/share:/usr/share:/usr/local/share}; do
[ -d "$dir/applications" ] && search_dirs+=("$dir/applications")
done
[ -d "$HOME/.local/share/applications" ] && search_dirs+=("$HOME/.local/share/applications")
[ ${#search_dirs[@]} -eq 0 ] && return
# Expand globs per-directory so the pattern works correctly
local -a desktop_files=()
for dir in "${search_dirs[@]}"; do
desktop_files+=("$dir"/*.desktop)
done
[ ${#desktop_files[@]} -eq 0 ] && return
# Single grep pass across all desktop files
local -A file_icons file_wmclass
local filepath line
while IFS=: read -r filepath line; do
case "$line" in
Icon=*)
[ -z "${file_icons[$filepath]:-}" ] && file_icons["$filepath"]="${line#Icon=}"
;;
StartupWMClass=*)
[ -z "${file_wmclass[$filepath]:-}" ] && file_wmclass["$filepath"]="${line#StartupWMClass=}"
;;
esac
done < <(grep -H '^Icon=\|^StartupWMClass=' "${desktop_files[@]}" 2>/dev/null)
# Build class → icon map
local icon wm_class bn name
for filepath in "${!file_icons[@]}"; do
icon="${file_icons[$filepath]}"
[ -n "$icon" ] || continue
wm_class="${file_wmclass[$filepath]:-}"
if [ -n "$wm_class" ]; then
_WINDOW_ICON_MAP["${wm_class,,}"]="$icon"
fi
bn="${filepath##*/}"
name="${bn%.desktop}"
_WINDOW_ICON_MAP["${name,,}"]="$icon"
done
}
_build_window_icon_map
icon_for_class() {
local class_lower="${1,,}"
echo "${_WINDOW_ICON_MAP[$class_lower]:-$class_lower}"
}

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
ws="$(
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
)"
if [[ -z "${ws}" ]]; then
exit 0
fi
hyprctl dispatch workspace "${ws}" >/dev/null 2>&1 || true

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
ws="$(
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
)"
if [[ -z "${ws}" ]]; then
exit 0
fi
hyprctl dispatch movetoworkspace "${ws}" >/dev/null 2>&1 || true

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
delta="${1:-}"
case "${delta}" in
+1|-1) ;;
next) delta="+1" ;;
prev) delta="-1" ;;
*)
exit 2
;;
esac
cur="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
if ! [[ "${cur}" =~ ^[0-9]+$ ]]; then
exit 0
fi
if (( cur < 1 )); then
cur=1
elif (( cur > max_ws )); then
cur="${max_ws}"
fi
if [[ "${delta}" == "+1" ]]; then
if (( cur >= max_ws )); then
nxt=1
else
nxt=$((cur + 1))
fi
else
if (( cur <= 1 )); then
nxt="${max_ws}"
else
nxt=$((cur - 1))
fi
fi
hyprctl dispatch workspace "${nxt}" >/dev/null 2>&1 || true

View File

@@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.27)
project(hypr-workspace-history
DESCRIPTION "Workspace history cycling plugin for Hyprland"
VERSION 0.1.0
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(hypr-workspace-history SHARED src/main.cpp)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET
hyprland
wayland-server
xkbcommon
)
target_link_libraries(hypr-workspace-history PRIVATE rt PkgConfig::deps)
install(TARGETS hypr-workspace-history)

View File

@@ -0,0 +1,406 @@
#define WLR_USE_UNSTABLE
#include <hyprland/src/Compositor.hpp>
#include <hyprland/src/debug/log/Logger.hpp>
#include <hyprland/src/desktop/Workspace.hpp>
#include <hyprland/src/desktop/state/FocusState.hpp>
#include <hyprland/src/devices/IKeyboard.hpp>
#include <hyprland/src/event/EventBus.hpp>
#include <hyprland/src/helpers/Monitor.hpp>
#include <hyprland/src/managers/KeybindManager.hpp>
#include <hyprland/src/plugins/PluginAPI.hpp>
#include <lua.hpp>
#include <algorithm>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <map>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
inline HANDLE PHANDLE = nullptr;
namespace {
constexpr int MAX_WORKSPACE = 9;
struct SCycleState {
std::string monitorKey;
int originalWorkspace = 0;
int previewWorkspace = 0;
std::vector<int> history;
size_t nextIndex = 0;
};
class CWorkspaceHistory {
public:
void seedActiveWorkspaces() {
if (!g_pCompositor)
return;
for (const auto& monitor : g_pCompositor->m_monitors) {
if (monitor && monitor->m_activeWorkspace)
remember(monitor->m_activeWorkspace);
}
writeDebug("seed");
}
void observe(PHLWORKSPACE workspace) {
const auto workspaceId = workspaceID(workspace);
if (!workspaceId) {
writeDebug("observe-skipped-non-normal-workspace");
return;
}
const auto key = monitorKey(workspace);
if (!m_cycle || m_cycle->monitorKey != key) {
remember(workspace);
return;
}
if (contains(m_cycle->history, *workspaceId)) {
m_cycle->previewWorkspace = *workspaceId;
writeDebug("cycle-observe-preview");
return;
}
m_cycle.reset();
remember(workspace);
writeDebug("remember-after-cycle-abandoned");
}
SDispatchResult cycle(int direction) {
auto* cycle = startCycle();
if (!cycle)
return {};
if (cycle->history.size() < 2) {
writeDebug("cycle-skipped-short-history");
return {};
}
const auto target = cycle->history[cycle->nextIndex];
cycle->previewWorkspace = target;
const auto result = focusWorkspace(target);
if (!result.success) {
writeDebug("cycle-focus-failed");
return result;
}
cycle->nextIndex = wrappedIndex(cycle->nextIndex, direction, cycle->history.size());
writeDebug(std::string("cycle-preview-") + std::to_string(target));
return {};
}
SDispatchResult commit() {
if (!m_cycle) {
writeDebug("commit-skipped-no-cycle");
return {};
}
auto cycle = *m_cycle;
m_cycle.reset();
m_histories[cycle.monitorKey] = promote(cycle.history, cycle.previewWorkspace);
writeDebug("commit");
return {};
}
SDispatchResult cancel() {
if (!m_cycle) {
writeDebug("cancel-skipped-no-cycle");
return {};
}
const auto original = m_cycle->originalWorkspace;
m_cycle.reset();
focusWorkspace(original);
writeDebug("cancel");
return {};
}
void onKey(IKeyboard::SKeyEvent event) {
if (!m_cycle || event.state != WL_KEYBOARD_KEY_STATE_RELEASED || !g_pKeybindManager)
return;
if (g_pKeybindManager->keycodeToModifier(event.keycode + 8) == HL_MODIFIER_META)
commit();
}
std::string snapshot(const std::string& reason) const {
std::stringstream out;
out << "reason=" << reason << "\n";
const auto monitor = Desktop::focusState() ? Desktop::focusState()->monitor() : nullptr;
const auto workspace = monitor ? monitor->m_activeWorkspace : nullptr;
out << "active_monitor=" << monitorKey(monitor) << "\n";
out << "active_workspace=" << (workspace ? std::to_string(workspace->m_id) : "?") << "\n";
for (const auto& [key, history] : m_histories) {
out << "history." << key << "=";
for (size_t i = 0; i < history.size(); ++i) {
if (i > 0)
out << ",";
out << history[i];
}
out << "\n";
}
if (m_cycle) {
out << "cycle.monitor=" << m_cycle->monitorKey << "\n";
out << "cycle.original=" << m_cycle->originalWorkspace << "\n";
out << "cycle.preview=" << m_cycle->previewWorkspace << "\n";
out << "cycle.next_index=" << (m_cycle->nextIndex + 1) << "\n";
out << "cycle.history=";
for (size_t i = 0; i < m_cycle->history.size(); ++i) {
if (i > 0)
out << ",";
out << m_cycle->history[i];
}
out << "\n";
} else {
out << "cycle=none\n";
}
return out.str();
}
void showDebug() const {
HyprlandAPI::addNotification(PHANDLE, snapshot("notification"), CHyprColor{0.4, 0.8, 1.0, 1.0}, 6000);
}
private:
std::map<std::string, std::vector<int>> m_histories;
std::optional<SCycleState> m_cycle;
static std::optional<int> workspaceID(PHLWORKSPACE workspace) {
if (!workspace || workspace->m_id < 1 || workspace->m_id > MAX_WORKSPACE)
return std::nullopt;
return workspace->m_id;
}
static std::string monitorKey(PHLMONITOR monitor) {
if (!monitor)
return "unknown";
if (!monitor->m_name.empty())
return monitor->m_name;
return std::to_string(monitor->m_id);
}
static std::string monitorKey(PHLWORKSPACE workspace) {
if (!workspace || !workspace->m_monitor)
return monitorKey(Desktop::focusState() ? Desktop::focusState()->monitor() : nullptr);
return monitorKey(workspace->m_monitor.lock());
}
static bool contains(const std::vector<int>& history, int workspace) {
return std::ranges::contains(history, workspace);
}
static std::vector<int> promote(std::vector<int> history, int workspace) {
std::erase(history, workspace);
history.insert(history.begin(), workspace);
return history;
}
static size_t wrappedIndex(size_t current, int direction, size_t size) {
const auto signedSize = static_cast<int>(size);
auto next = static_cast<int>(current) + direction;
next = ((next % signedSize) + signedSize) % signedSize;
return static_cast<size_t>(next);
}
PHLWORKSPACE activeWorkspace() const {
const auto monitor = Desktop::focusState() ? Desktop::focusState()->monitor() : nullptr;
return monitor ? monitor->m_activeWorkspace : nullptr;
}
void remember(PHLWORKSPACE workspace) {
const auto workspaceId = workspaceID(workspace);
if (!workspaceId) {
writeDebug("remember-skipped-non-normal-workspace");
return;
}
const auto key = monitorKey(workspace);
auto& history = m_histories[key];
const bool changed = history.empty() || history.front() != *workspaceId;
history = promote(history, *workspaceId);
if (changed)
writeDebug("remember");
}
SCycleState* startCycle() {
if (m_cycle)
return &*m_cycle;
const auto workspace = activeWorkspace();
remember(workspace);
const auto workspaceId = workspaceID(workspace);
if (!workspaceId) {
writeDebug("cycle-start-skipped-non-normal-workspace");
return nullptr;
}
const auto key = monitorKey(workspace);
auto history = promote(m_histories[key], *workspaceId);
if (history.size() < 2) {
writeDebug("cycle-start-skipped-short-history");
return nullptr;
}
m_cycle = SCycleState{
.monitorKey = key,
.originalWorkspace = *workspaceId,
.previewWorkspace = *workspaceId,
.history = history,
.nextIndex = 1,
};
writeDebug("cycle-start");
return &*m_cycle;
}
static SDispatchResult focusWorkspace(int workspace) {
if (!g_pKeybindManager)
return {.success = false, .error = "keybind manager is unavailable"};
const auto dispatcher = g_pKeybindManager->m_dispatchers.find("focusworkspaceoncurrentmonitor");
if (dispatcher == g_pKeybindManager->m_dispatchers.end())
return {.success = false, .error = "focusworkspaceoncurrentmonitor dispatcher is unavailable"};
return dispatcher->second(std::to_string(workspace));
}
static std::optional<std::string> runtimePath(const std::string& name) {
const auto runtimeDir = std::getenv("XDG_RUNTIME_DIR");
if (!runtimeDir)
return std::nullopt;
return std::string(runtimeDir) + "/" + name;
}
void writeDebug(const std::string& reason) const {
const auto statePath = runtimePath("hyprland-workspace-history-state");
const auto logPath = runtimePath("hyprland-workspace-history.log");
if (!statePath || !logPath)
return;
const auto body = snapshot(reason);
std::ofstream state(*statePath, std::ios::trunc);
if (state)
state << body;
std::ofstream log(*logPath, std::ios::app);
if (log) {
const auto now = std::time(nullptr);
log << "--- " << std::put_time(std::localtime(&now), "%Y-%m-%d %H:%M:%S") << " ---\n";
log << body;
}
}
};
CWorkspaceHistory g_workspaceHistory;
SDispatchResult dispatchCycle(std::string arg) {
int direction = 1;
if (arg == "-1" || arg == "previous" || arg == "prev" || arg == "reverse")
direction = -1;
return g_workspaceHistory.cycle(direction);
}
SDispatchResult dispatchCommit(std::string) {
return g_workspaceHistory.commit();
}
SDispatchResult dispatchCancel(std::string) {
return g_workspaceHistory.cancel();
}
SDispatchResult dispatchDebug(std::string) {
g_workspaceHistory.showDebug();
return {};
}
int luaCycle(lua_State* L) {
const auto result = g_workspaceHistory.cycle(static_cast<int>(luaL_optinteger(L, 1, 1)));
if (!result.success)
return luaL_error(L, "%s", result.error.c_str());
return 0;
}
int luaCommit(lua_State* L) {
const auto result = g_workspaceHistory.commit();
if (!result.success)
return luaL_error(L, "%s", result.error.c_str());
return 0;
}
int luaCancel(lua_State* L) {
const auto result = g_workspaceHistory.cancel();
if (!result.success)
return luaL_error(L, "%s", result.error.c_str());
return 0;
}
int luaDebug(lua_State*) {
g_workspaceHistory.showDebug();
return 0;
}
void failNotification(const std::string& reason) {
HyprlandAPI::addNotification(PHANDLE, "[workspace-history] " + reason, CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000);
}
}
APICALL EXPORT std::string PLUGIN_API_VERSION() {
return HYPRLAND_API_VERSION;
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
PHANDLE = handle;
const std::string hash = __hyprland_api_get_hash();
const std::string clientHash = __hyprland_api_get_client_hash();
if (hash != clientHash) {
failNotification("version mismatch between Hyprland headers and running Hyprland");
throw std::runtime_error("[workspace-history] version mismatch");
}
static auto workspaceHook = Event::bus()->m_events.workspace.active.listen([](PHLWORKSPACE workspace) { g_workspaceHistory.observe(workspace); });
static auto keyboardHook = Event::bus()->m_events.input.keyboard.key.listen([](IKeyboard::SKeyEvent event, Event::SCallbackInfo&) { g_workspaceHistory.onKey(event); });
static auto startHook = Event::bus()->m_events.start.listen([] { g_workspaceHistory.seedActiveWorkspaces(); });
static auto reloadHook = Event::bus()->m_events.config.reloaded.listen([] { g_workspaceHistory.seedActiveWorkspaces(); });
static auto monitorHook = Event::bus()->m_events.monitor.focused.listen([](PHLMONITOR monitor) {
if (monitor && monitor->m_activeWorkspace)
g_workspaceHistory.observe(monitor->m_activeWorkspace);
});
HyprlandAPI::addDispatcherV2(PHANDLE, "workspacehistory:cycle", ::dispatchCycle);
HyprlandAPI::addDispatcherV2(PHANDLE, "workspacehistory:commit", ::dispatchCommit);
HyprlandAPI::addDispatcherV2(PHANDLE, "workspacehistory:cancel", ::dispatchCancel);
HyprlandAPI::addDispatcherV2(PHANDLE, "workspacehistory:debug", ::dispatchDebug);
HyprlandAPI::addLuaFunction(PHANDLE, "workspacehistory", "cycle", ::luaCycle);
HyprlandAPI::addLuaFunction(PHANDLE, "workspacehistory", "commit", ::luaCommit);
HyprlandAPI::addLuaFunction(PHANDLE, "workspacehistory", "cancel", ::luaCancel);
HyprlandAPI::addLuaFunction(PHANDLE, "workspacehistory", "debug", ::luaDebug);
g_workspaceHistory.seedActiveWorkspaces();
HyprlandAPI::addNotification(PHANDLE, "[workspace-history] Initialized", CHyprColor{0.2, 1.0, 0.2, 1.0}, 3000);
return {"hypr-workspace-history", "Workspace history cycling with modifier-release commits", "Ivan Malison", "0.1.0"};
}
APICALL EXPORT void PLUGIN_EXIT() {
g_workspaceHistory.commit();
}

View File

@@ -136,11 +136,11 @@
"xmonad-contrib": "xmonad-contrib"
},
"locked": {
"lastModified": 1777319252,
"narHash": "sha256-mPft6i8ReJAvW2LdylFI6FF6NFGa1HMa3RNbisfAsbc=",
"ref": "refs/heads/codex/fix-gdk-backend-strut-detection",
"rev": "c2cee23fc57384cd322d589944129e6c31d4f0fd",
"revCount": 2288,
"lastModified": 1777525523,
"narHash": "sha256-/LGaCcX6BgXRYpWnRp9CNgAgy7lmbQsubi4RwHJgnTI=",
"ref": "refs/heads/master",
"rev": "23dbc827adca706b28df7404624ae3f5e800b04f",
"revCount": 2296,
"type": "git",
"url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"
},

View File

@@ -121,13 +121,6 @@
(oa: {
doHaddock = false;
doCheck = false;
# Legacy fix for older GHC (harmless on newer)
postPatch = (oa.postPatch or "") + ''
substituteInPlace src/System/Taffybar/DBus/Client/Util.hs \
--replace-fail "import Control.Monad (forM)" \
"import Control.Monad (forM)
import Control.Applicative (liftA2)"
'';
# Needed for gi-gtk-layer-shell (introspection data).
librarySystemDepends = (oa.librarySystemDepends or []) ++ [ pkgs.gtk-layer-shell ];
});

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174 148.18">
<path
fill="#d97757"
d="m 105.01,322.07 29.14,-16.35 0.49,-1.42 -0.49,-0.79 h -1.42 l -4.87,-0.3 -16.65,-0.45 -14.44,-0.6 -13.99,-0.75 -3.52,-0.75 -3.3,-4.35 0.34,-2.17 2.96,-1.99 4.24,0.37 9.37,0.64 14.06,0.97 10.2,0.6 15.11,1.57 h 2.4 l 0.34,-0.97 -0.82,-0.6 -0.64,-0.6 -14.55,-9.86 -15.75,-10.42 -8.25,-6 -4.46,-3.04 -2.25,-2.85 -0.97,-6.22 4.05,-4.46 5.44,0.37 1.39,0.37 5.51,4.24 11.77,9.11 15.37,11.32 2.25,1.87 0.9,-0.64 0.11,-0.45 -1.01,-1.69 -8.36,-15.11 -8.92,-15.37 -3.97,-6.37 -1.05,-3.82 c -0.37,-1.57 -0.64,-2.89 -0.64,-4.5 l 4.61,-6.26 2.55,-0.82 6.15,0.82 2.59,2.25 3.82,8.74 6.19,13.76 9.6,18.71 2.81,5.55 1.5,5.14 0.56,1.57 h 0.97 v -0.9 l 0.79,-10.54 1.46,-12.94 1.42,-16.65 0.49,-4.69 2.32,-5.62 4.61,-3.04 3.6,1.72 2.96,4.24 -0.41,2.74 -1.76,11.44 -3.45,17.92 -2.25,12 h 1.31 l 1.5,-1.5 6.07,-8.06 10.2,-12.75 4.5,-5.06 5.25,-5.59 3.37,-2.66 h 6.37 l 4.69,6.97 -2.1,7.2 -6.56,8.32 -5.44,7.05 -7.8,10.5 -4.87,8.4 0.45,0.67 1.16,-0.11 17.62,-3.75 9.52,-1.72 11.36,-1.95 5.14,2.4 0.56,2.44 -2.02,4.99 -12.15,3 -14.25,2.85 -21.22,5.02 -0.26,0.19 0.3,0.37 9.56,0.9 4.09,0.22 h 10.01 l 18.64,1.39 4.87,3.22 2.92,3.94 -0.49,3 -7.5,3.82 -10.12,-2.4 -23.62,-5.62 -8.1,-2.02 h -1.12 v 0.67 l 6.75,6.6 12.37,11.17 15.49,14.4 0.79,3.56 -1.99,2.81 -2.1,-0.3 -13.61,-10.24 -5.25,-4.61 -11.89,-10.01 h -0.79 v 1.05 l 2.74,4.01 14.47,21.75 0.75,6.67 -1.05,2.17 -3.75,1.31 -4.12,-0.75 -8.47,-11.89 -8.74,-13.39 -7.05,-12 -0.86,0.49 -4.16,44.81 -1.95,2.29 -4.5,1.72 -3.75,-2.85 -1.99,-4.61 1.99,-9.11 2.4,-11.89 1.95,-9.45 1.76,-11.74 1.05,-3.9 -0.07,-0.26 -0.86,0.11 -8.85,12.15 -13.46,18.19 -10.65,11.4 -2.55,1.01 -4.42,-2.29 0.41,-4.09 2.47,-3.64 14.74,-18.75 8.89,-11.62 5.74,-6.71 -0.04,-0.97 h -0.34 l -39.15,25.42 -6.97,0.9 -3,-2.81 0.37,-4.61 1.42,-1.5 11.77,-8.1 -0.04,0.04 z"
transform="translate(-75.96,-223.53)"
shape-rendering="optimizeQuality" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 158.7128 157.296">
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
<path fill="#e7e4ee" d="M60.8734,57.2556v-14.9432c0-1.2586.4722-2.2029,1.5728-2.8314l30.0443-17.3023c4.0899-2.3593,8.9662-3.4599,13.9988-3.4599,18.8759,0,30.8307,14.6289,30.8307,30.2006,0,1.1007,0,2.3593-.158,3.6178l-31.1446-18.2467c-1.8872-1.1006-3.7754-1.1006-5.6629,0l-39.4812,22.9651ZM131.0276,115.4561v-35.7074c0-2.2028-.9446-3.7756-2.8318-4.8763l-39.481-22.9651,12.8982-7.3934c1.1007-.6285,2.0453-.6285,3.1458,0l30.0441,17.3024c8.6523,5.0341,14.4708,15.7296,14.4708,26.1107,0,11.9539-7.0769,22.965-18.2461,27.527v.0021ZM51.593,83.9964l-12.8982-7.5497c-1.1007-.6285-1.5728-1.5728-1.5728-2.8314v-34.6048c0-16.8303,12.8982-29.5722,30.3585-29.5722,6.607,0,12.7403,2.2029,17.9324,6.1349l-30.987,17.9324c-1.8871,1.1007-2.8314,2.6735-2.8314,4.8764v45.6159l-.0014-.0015ZM79.3562,100.0403l-18.4829-10.3811v-22.0209l18.4829-10.3811,18.4812,10.3811v22.0209l-18.4812,10.3811ZM91.2319,147.8591c-6.607,0-12.7403-2.2031-17.9324-6.1344l30.9866-17.9333c1.8872-1.1005,2.8318-2.6728,2.8318-4.8759v-45.616l13.0564,7.5498c1.1005.6285,1.5723,1.5728,1.5723,2.8314v34.6051c0,16.8297-13.0564,29.5723-30.5147,29.5723v.001ZM53.9522,112.7822l-30.0443-17.3024c-8.652-5.0343-14.471-15.7296-14.471-26.1107,0-12.1119,7.2356-22.9652,18.403-27.5272v35.8634c0,2.2028.9443,3.7756,2.8314,4.8763l39.3248,22.8068-12.8982,7.3938c-1.1007.6287-2.045.6287-3.1456,0ZM52.2229,138.5791c-17.7745,0-30.8306-13.3713-30.8306-29.8871,0-1.2585.1578-2.5169.3143-3.7754l30.987,17.9323c1.8871,1.1005,3.7757,1.1005,5.6628,0l39.4811-22.807v14.9435c0,1.2585-.4721,2.2021-1.5728,2.8308l-30.0443,17.3025c-4.0898,2.359-8.9662,3.4605-13.9989,3.4605h.0014ZM91.2319,157.296c19.0327,0,34.9188-13.5272,38.5383-31.4594,17.6164-4.562,28.9425-21.0779,28.9425-37.908,0-11.0112-4.719-21.7066-13.2133-29.4143.7867-3.3035,1.2595-6.607,1.2595-9.909,0-22.4929-18.2471-39.3247-39.3251-39.3247-4.2461,0-8.3363.6285-12.4262,2.045-7.0792-6.9213-16.8318-11.3254-27.5271-11.3254-19.0331,0-34.9191,13.5268-38.5384,31.4591C11.3255,36.0212,0,52.5373,0,69.3675c0,11.0112,4.7184,21.7065,13.2125,29.4142-.7865,3.3035-1.2586,6.6067-1.2586,9.9092,0,22.4923,18.2466,39.3241,39.3248,39.3241,4.2462,0,8.3362-.6277,12.426-2.0441,7.0776,6.921,16.8302,11.3251,27.5271,11.3251Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -40,6 +40,16 @@
-GtkLabel-justify: left;
}
/* Compact logo column for stacked AI usage sections. */
.usage-section.icon-label > .icon {
min-width: 22px;
padding-right: 8px;
}
.usage-section.icon-label > .label {
padding-right: 0px;
}
/* Compact two-line RAM/SWAP widget: reduce icon padding a bit. */
.ram-swap .icon-label > .icon {
/* Different glyphs have different visual widths; fix the icon column width

View File

@@ -1,4 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
@@ -46,11 +47,21 @@ import System.Taffybar.SimpleConfig
import System.Taffybar.Util (getPixbufFromFilePath, maybeTCombine, postGUIASync, (<|||>))
import System.Taffybar.Widget
import qualified System.Taffybar.Widget.ASUS as ASUS
import System.Taffybar.Widget.AnthropicUsage (anthropicUsageStackNew)
import System.Taffybar.Widget.AnthropicUsage
( AnthropicUsageDisplayMode (AnthropicUsageDisplayRemaining),
AnthropicUsageStackConfig (..),
anthropicUsageStackNewWith,
defaultAnthropicUsageStackConfig,
)
import System.Taffybar.Widget.CPUMonitor (cpuMonitorNew)
import System.Taffybar.Widget.Generic.Graph (GraphConfig (..), GraphDirection (..), GraphStyle (..), defaultGraphConfig)
import qualified System.Taffybar.Widget.NetworkManager as NetworkManager
import System.Taffybar.Widget.OpenAIUsage (openAIUsageStackNew)
import System.Taffybar.Widget.OpenAIUsage
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
OpenAIUsageStackConfig (..),
defaultOpenAIUsageStackConfig,
openAIUsageStackNewWith,
)
import qualified System.Taffybar.Widget.PulseAudio as PulseAudio
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
import System.Taffybar.Widget.SNITray
@@ -65,7 +76,7 @@ import System.Taffybar.Widget.SNITray.PrioritizedCollapsible
sniTrayPrioritizedCollapsibleNewFromParams,
)
import qualified System.Taffybar.Widget.ScreenLock as ScreenLock
import System.Taffybar.Widget.Util (backgroundLoop, buildContentsBox, buildIconLabelBox, loadPixbufByName, widgetSetClassGI)
import System.Taffybar.Widget.Util (backgroundLoop, buildContentsBox, buildIconLabelBox, loadPixbufByName, pixbufNewFromFileAtScaleByHeight, widgetSetClassGI)
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import System.Taffybar.WindowIcon (pixBufFromColor)
@@ -507,10 +518,10 @@ simplifiedScreensaverWidget =
then return False
else case button of
1 -> do
void $ spawnCommand "hypr-screensaver toggle >/dev/null 2>&1"
void $ spawnCommand "/home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver toggle >/dev/null 2>&1"
return True
3 -> do
void $ spawnCommand "hypr-screensaver stop >/dev/null 2>&1"
void $ spawnCommand "/home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop >/dev/null 2>&1"
return True
_ -> return False
Gtk.widgetShowAll ebox
@@ -551,13 +562,40 @@ wakeupDebugWidget :: TaffyIO Gtk.Widget
wakeupDebugWidget =
decorateWithClassAndBoxM "wakeup-debug" wakeupDebugWidgetNew
usageLogoWidget :: FilePath -> Text -> IO Gtk.Widget
usageLogoWidget iconFile tooltip = do
iconPath <- getUserConfigFile "taffybar" ("icons/" <> iconFile)
iconWidget <-
pixbufNewFromFileAtScaleByHeight 18 iconPath >>= \case
Right pixbuf -> Gtk.toWidget =<< Gtk.imageNewFromPixbuf (Just pixbuf)
Left _ -> Gtk.toWidget =<< Gtk.labelNew (Just "?")
Gtk.widgetSetTooltipText iconWidget (Just tooltip)
widgetSetClassGI iconWidget "usage-logo"
usageSectionWidget :: Text -> FilePath -> Text -> TaffyIO Gtk.Widget -> TaffyIO Gtk.Widget
usageSectionWidget klass iconFile tooltip stackBuilder =
decorateWithClassAndBoxM klass $ do
stack <- stackBuilder
liftIO $ do
iconWidget <- usageLogoWidget iconFile tooltip
section <- buildIconLabelBox iconWidget stack
widgetSetClassGI section "usage-section"
openAIUsageWidget :: TaffyIO Gtk.Widget
openAIUsageWidget =
decorateWithClassAndBoxM "openai-usage" openAIUsageStackNew
usageSectionWidget "openai-usage" "openai-symbol.svg" "OpenAI usage" $
openAIUsageStackNewWith
defaultOpenAIUsageStackConfig
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
}
anthropicUsageWidget :: TaffyIO Gtk.Widget
anthropicUsageWidget =
decorateWithClassAndBoxM "anthropic-usage" anthropicUsageStackNew
usageSectionWidget "anthropic-usage" "claude-symbol.svg" "Anthropic usage" $
anthropicUsageStackNewWith
defaultAnthropicUsageStackConfig
{ anthropicUsageStackDefaultDisplayMode = AnthropicUsageDisplayRemaining
}
sniPriorityVisibilityThresholdDefault :: Int
sniPriorityVisibilityThresholdDefault = 0

View File

@@ -232,27 +232,19 @@ getWorkspaceDmenu = myDmenu (workspaces myConfig)
-- Selectors
isGmailTitle t = isInfixOf "@gmail.com" t && isInfixOf "Gmail" t
isMessagesTitle = isPrefixOf "Messages"
isChromeClass = isInfixOf "chrome"
noSpecialChromeTitles = helper <$> title
where helper t = not $ any ($ t) [isGmailTitle, isMessagesTitle]
chromeSelectorBase = isChromeClass <$> className
chromeSelector = chromeSelectorBase <&&> noSpecialChromeTitles
chromeSelector = chromeSelectorBase
elementSelector = className =? "Element"
emacsSelector = className =? "Emacs"
gmailSelector = chromeSelectorBase <&&> fmap isGmailTitle title
messagesSelector = chromeSelectorBase <&&> isMessagesTitle <$> title
slackSelector = className =? "Slack"
spotifySelector = className =? "Spotify"
transmissionSelector = fmap (isPrefixOf "Transmission") title
volumeSelector = className =? "Pavucontrol"
virtualClasses =
[ (gmailSelector, "Gmail")
, (messagesSelector, "Messages")
, (chromeSelector, "Chrome")
[ (chromeSelector, "Chrome")
, (transmissionSelector, "Transmission")
]
@@ -261,11 +253,7 @@ virtualClasses =
chromeCommand = "google-chrome-stable"
elementCommand = "element-desktop"
emacsCommand = "emacsclient -c"
gmailCommand =
"google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox"
htopCommand = "ghostty --title=htop -e htop"
messagesCommand =
"google-chrome-stable --new-window https://messages.google.com/web/conversations"
slackCommand = "slack"
spotifyCommand = "spotify"
transmissionCommand = "transmission-gtk"
@@ -813,9 +801,7 @@ nearFullFloat = customFloating $ W.RationalRect l t w h
scratchpads =
[ NS "element" elementCommand elementSelector nearFullFloat
, NS "gmail" gmailCommand gmailSelector nearFullFloat
, NS "htop" htopCommand (title =? "htop") nearFullFloat
, NS "messages" messagesCommand messagesSelector nearFullFloat
, NS "slack" slackCommand slackSelector nearFullFloat
, NS "spotify" spotifyCommand spotifySelector nearFullFloat
, NS "transmission" transmissionCommand transmissionSelector nearFullFloat
@@ -1012,11 +998,11 @@ addKeys conf@XConfig { modMask = modm } =
(modm .|. shiftMask) (`windowSwap` True) ++
buildDirectionalBindings
(modm .|. controlMask) (followingWindow . (`windowToScreen` True)) ++
buildDirectionalBindings
(modm .|. controlMask .|. shiftMask) shiftToEmptyOnScreen ++
buildDirectionalBindings hyper (`screenGo` True) ++
buildDirectionalBindings
(hyper .|. shiftMask) (followingWindow . (`screenSwap` True)) ++
buildDirectionalBindings
(hyper .|. controlMask) shiftToEmptyOnScreen ++
-- Specific program spawning
bindBringAndRaiseMany
@@ -1025,9 +1011,7 @@ addKeys conf@XConfig { modMask = modm } =
-- ScratchPads
[ ((modalt, xK_e), doScratchpad "element")
, ((modalt, xK_g), doScratchpad "gmail")
, ((modalt, xK_h), doScratchpad "htop")
, ((modalt, xK_m), doScratchpad "messages")
, ((modalt, xK_k), doScratchpad "slack")
, ((modalt, xK_s), doScratchpad "spotify")
, ((modalt, xK_t), doScratchpad "transmission")
@@ -1101,7 +1085,7 @@ addKeys conf@XConfig { modMask = modm } =
, ((hyper, xK_space), spawn "skippy-xd")
, ((hyper, xK_i), spawn "rofi_select_input.hs")
, ((hyper, xK_o), spawn "rofi_paswitch")
, ((hyper, xK_w), spawn "rofi_wallpaper.sh")
, ((hyper, xK_comma), spawn "rofi_wallpaper.sh")
, ((hyper, xK_y), spawn "rofi_agentic_skill")
, ((modm, xK_e), spawn "emacsclient --eval '(emacs-everywhere)'")

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
state_file="${IM_DESKTOP_SHELL_UI_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/imalison/desktop-shell-ui}"
default_shell_ui="${IM_HYPRLAND_SHELL_UI:-taffybar}"
normalize_shell_ui() {
case "${1:-}" in
noctalia)
printf '%s\n' "noctalia"
;;
taffybar|rofi)
printf '%s\n' "taffybar"
;;
*)
return 1
;;
esac
}
current_shell_ui() {
local configured=""
if [[ -r "$state_file" ]]; then
IFS= read -r configured < "$state_file" || true
fi
normalize_shell_ui "$configured" 2>/dev/null \
|| normalize_shell_ui "$default_shell_ui" 2>/dev/null \
|| printf '%s\n' "taffybar"
}
write_shell_ui() {
local shell_ui="$1"
mkdir -p "$(dirname "$state_file")"
printf '%s\n' "$shell_ui" > "$state_file"
}
apply_shell_ui() {
local shell_ui="$1"
export IM_HYPRLAND_SHELL_UI="$shell_ui"
systemctl --user import-environment IM_HYPRLAND_SHELL_UI 2>/dev/null || true
case "$shell_ui" in
noctalia)
systemctl --user stop taffybar.service 2>/dev/null || true
systemctl --user reset-failed taffybar.service 2>/dev/null || true
systemctl --user start noctalia-shell.service
;;
taffybar)
systemctl --user stop noctalia-shell.service 2>/dev/null || true
systemctl --user reset-failed noctalia-shell.service 2>/dev/null || true
systemctl --user start taffybar.service
;;
esac
}
set_shell_ui() {
local shell_ui
shell_ui="$(normalize_shell_ui "${1:-}")" || {
echo "usage: desktop_shell_ui set {taffybar|noctalia}" >&2
exit 2
}
write_shell_ui "$shell_ui"
apply_shell_ui "$shell_ui"
}
case "${1:-current}" in
current)
current_shell_ui
;;
set)
set_shell_ui "${2:-}"
;;
toggle)
case "$(current_shell_ui)" in
noctalia) set_shell_ui taffybar ;;
*) set_shell_ui noctalia ;;
esac
;;
apply)
apply_shell_ui "$(current_shell_ui)"
;;
exec-condition)
[[ "$(current_shell_ui)" == "$(normalize_shell_ui "${2:-}")" ]]
;;
*)
echo "usage: desktop_shell_ui {current|set|toggle|apply|exec-condition}" >&2
exit 2
;;
esac

View File

@@ -2,27 +2,48 @@
set -euo pipefail
script_path="$(readlink -f "${BASH_SOURCE[0]}")"
state_dir="${XDG_RUNTIME_DIR:-/tmp}/hypr-screensaver"
pid_file="$state_dir/mpvpaper.pid"
event_log="$state_dir/events.log"
mkdir -p "$state_dir"
title_prefix="hypr-screensaver:"
screensaver_dir="${HYPR_SCREENSAVER_DIR:-/var/lib/syncthing/sync/Screensaver}"
screensaver_use_dir="${HYPR_SCREENSAVER_USE_DIR:-$screensaver_dir/use}"
usage() {
cat <<'EOF'
Usage: hypr-screensaver <start|stop|toggle|status|session>
Commands:
start Launch the screensaver on every Hyprland monitor.
stop Stop any running screensaver windows.
start Launch the screensaver as a Wayland layer-shell overlay.
stop Stop any running screensaver overlay.
toggle Start if stopped, otherwise stop.
status Exit 0 if any screensaver window is running, otherwise exit 1.
session Run the configured screensaver payload for one monitor.
status Exit 0 if the screensaver overlay is running, otherwise exit 1.
session Compatibility alias for start.
The default payload is an mpv-rendered lavfi animation. You can override the
source with HYPR_SCREENSAVER_SOURCE, for example:
By default, start chooses a random media file from:
/var/lib/syncthing/sync/Screensaver/use
Populate that directory with symlinks to generated screensaver loops you want
in rotation. You can override the source with HYPR_SCREENSAVER_SOURCE, for
example:
HYPR_SCREENSAVER_SOURCE='/path/to/video.mp4'
HYPR_SCREENSAVER_SOURCE='av://lavfi:mandelbrot=s=2560x1440:r=60'
You can also override the rotation directory:
HYPR_SCREENSAVER_USE_DIR='/path/to/use'
Layer-shell/output overrides:
HYPR_SCREENSAVER_OUTPUT='ALL'
HYPR_SCREENSAVER_LAYER='overlay'
HDR handling defaults to matching Hyprland's monitor color-management preset.
Only monitors with preset "hdr" or "hdredid" get HDR colorspace hints. Override
with:
HYPR_SCREENSAVER_HDR_MODE=auto
HYPR_SCREENSAVER_HDR_MODE=sdr
HYPR_SCREENSAVER_HDR_MODE=hdr
EOF
}
@@ -30,19 +51,11 @@ monitors_json() {
hyprctl -j monitors
}
monitor_names() {
monitors_json | jq -r '.[].name'
log_event() {
printf '%s %s\n' "$(date --iso-8601=seconds)" "$*" >>"$event_log"
}
monitor_specs() {
monitors_json | jq -c '.[] | { name, width, height }'
}
focused_monitor() {
monitors_json | jq -r '.[] | select(.focused) | .name'
}
screensaver_window_pids() {
legacy_screensaver_window_pids() {
hyprctl -j clients 2>/dev/null | jq -r --arg prefix "$title_prefix" '
.[]
| select((.title // "") | startswith($prefix))
@@ -52,106 +65,170 @@ screensaver_window_pids() {
is_running() {
local pid
for pid in $(screensaver_window_pids); do
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
done
shopt -s nullglob
local pid_file
for pid_file in "$state_dir"/*.pid; do
if [ -f "$pid_file" ]; then
pid="$(<"$pid_file")"
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
done
rm -f "$pid_file"
fi
return 1
}
default_source() {
local width="$1"
local height="$2"
local size width height
size="$(
monitors_json 2>/dev/null \
| jq -r 'max_by((.width // 0) * (.height // 0)) | "\(.width // 1920)x\(.height // 1080)"' 2>/dev/null \
|| true
)"
size="${size:-1920x1080}"
width="${size%x*}"
height="${size#*x}"
printf 'av://lavfi:life=s=%sx%s:r=60:mold=10:ratio=0.065:death_color=#101414:life_color=#7dd3fc:mold_color=#1e3a5f,format=yuv420p' \
"$width" "$height"
}
random_source() {
[ -d "$screensaver_use_dir" ] || return 1
local -a candidates=()
local candidate
while IFS= read -r -d '' candidate; do
candidates+=("$candidate")
done < <(
find -L "$screensaver_use_dir" -maxdepth 1 -type f \
\( \
-iname '*.mp4' -o \
-iname '*.mkv' -o \
-iname '*.mov' -o \
-iname '*.webm' -o \
-iname '*.gif' -o \
-iname '*.png' -o \
-iname '*.jpg' -o \
-iname '*.jpeg' \
\) \
-print0
)
[ "${#candidates[@]}" -gt 0 ] || return 1
printf '%s\n' "${candidates[$((RANDOM % ${#candidates[@]}))]}"
}
screensaver_uses_hdr() {
local mode="${HYPR_SCREENSAVER_HDR_MODE:-auto}"
case "$mode" in
hdr)
return 0
;;
sdr)
return 1
;;
auto)
monitors_json 2>/dev/null \
| jq -e 'any(.[]; (.colorManagementPreset // "srgb") == "hdr" or (.colorManagementPreset // "srgb") == "hdredid")' >/dev/null 2>&1
;;
*)
printf 'Invalid HYPR_SCREENSAVER_HDR_MODE=%s; expected auto, sdr, or hdr\n' "$mode" >&2
return 1
;;
esac
}
mpv_color_options() {
if screensaver_uses_hdr; then
printf '%s ' \
target-colorspace-hint=yes \
target-colorspace-hint-mode=source
return
fi
printf '%s ' \
target-colorspace-hint=no \
target-prim=bt.709 \
target-trc=srgb \
target-gamut=bt.709 \
target-peak=80 \
inverse-tone-mapping=no
}
mpv_options() {
if [ -n "${HYPR_SCREENSAVER_MPV_OPTIONS:-}" ]; then
printf '%s\n' "$HYPR_SCREENSAVER_MPV_OPTIONS"
return
fi
printf '%s %s\n' \
"no-audio loop-file=inf osc=no osd-level=0 input-default-bindings=no terminal=no image-display-duration=inf keep-open=yes" \
"$(mpv_color_options)"
}
run_mpvpaper() {
if command -v mpvpaper >/dev/null 2>&1; then
exec mpvpaper "$@"
fi
exec nix shell nixpkgs#mpvpaper --command mpvpaper "$@"
}
start() {
local current_monitor spec monitor width height pid
local source output layer options pid
if is_running; then
log_event "start ignored: already running pid=$(<"$pid_file")"
exit 0
fi
current_monitor="$(focused_monitor || true)"
stop
while IFS= read -r spec; do
monitor="$(jq -r '.name' <<<"$spec")"
width="$(jq -r '.width' <<<"$spec")"
height="$(jq -r '.height' <<<"$spec")"
[ -n "$monitor" ] || continue
HYPR_SCREENSAVER_MONITOR="$monitor" \
HYPR_SCREENSAVER_WIDTH="$width" \
HYPR_SCREENSAVER_HEIGHT="$height" \
"$script_path" session >/dev/null 2>&1 &
pid=$!
printf '%s\n' "$pid" > "$state_dir/${monitor}.pid"
sleep 0.15
done < <(monitor_specs)
if [ -n "$current_monitor" ]; then
hyprctl dispatch focusmonitor "$current_monitor" >/dev/null 2>&1 || true
source="${HYPR_SCREENSAVER_SOURCE:-}"
if [ -z "$source" ]; then
source="$(random_source || true)"
fi
if [ -z "$source" ]; then
source="$(default_source)"
fi
output="${HYPR_SCREENSAVER_OUTPUT:-ALL}"
layer="${HYPR_SCREENSAVER_LAYER:-overlay}"
options="$(mpv_options)"
log_event "start output=$output layer=$layer source=$source"
(
exec </dev/null
run_mpvpaper --layer "$layer" --mpv-options "$options" "$output" "$source"
) >>"$state_dir/mpvpaper.log" 2>&1 &
pid=$!
printf '%s\n' "$pid" > "$pid_file"
sleep 0.2
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pid_file"
log_event "start failed: process exited early pid=$pid"
return 1
fi
log_event "start ok pid=$pid"
}
stop() {
local pid pid_file
local pid legacy_pid
for pid in $(screensaver_window_pids); do
kill "$pid" >/dev/null 2>&1 || true
done
shopt -s nullglob
for pid_file in "$state_dir"/*.pid; do
if [ -f "$pid_file" ]; then
pid="$(<"$pid_file")"
log_event "stop pid=$pid"
kill "$pid" >/dev/null 2>&1 || true
pkill -TERM -P "$pid" >/dev/null 2>&1 || true
rm -f "$pid_file"
done
}
session() {
local monitor="${HYPR_SCREENSAVER_MONITOR:?missing HYPR_SCREENSAVER_MONITOR}"
local width="${HYPR_SCREENSAVER_WIDTH:-1920}"
local height="${HYPR_SCREENSAVER_HEIGHT:-1080}"
local source="${HYPR_SCREENSAVER_SOURCE:-$(default_source "$width" "$height")}"
local -a mpv_args=(
--no-config
--really-quiet
--fullscreen
--fs-screen-name="$monitor"
--screen-name="$monitor"
--force-window=immediate
--border=no
--title-bar=no
--ontop
--keep-open=yes
--loop-file=inf
--audio=no
--osc=no
--osd-level=0
--input-default-bindings=no
--wayland-app-id=hypr-screensaver
--title="${title_prefix}${monitor}"
--image-display-duration=inf
"$source"
)
if command -v mpv >/dev/null 2>&1; then
exec mpv "${mpv_args[@]}"
else
log_event "stop with no pid file"
fi
exec nix shell nixpkgs#mpv --command mpv "${mpv_args[@]}"
for legacy_pid in $(legacy_screensaver_window_pids); do
log_event "stop legacy pid=$legacy_pid"
kill "$legacy_pid" >/dev/null 2>&1 || true
done
}
status() {
@@ -176,7 +253,7 @@ case "${1:-}" in
status
;;
session)
session
start
;;
""|-h|--help|help)
usage

254
dotfiles/lib/bin/hypr_rofi_window Executable file
View File

@@ -0,0 +1,254 @@
#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
PROMPTS = {
"go": "Go to window",
"bring": "Bring window",
"replace": "Replace with",
}
def ensure_hyprland_instance():
if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
return
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"))
hypr_dir = runtime / "hypr"
if not hypr_dir.is_dir():
return
sockets = []
for socket in hypr_dir.glob("*/.socket.sock"):
try:
sockets.append((socket.stat().st_mtime, socket.parent.name))
except OSError:
pass
if sockets:
os.environ["HYPRLAND_INSTANCE_SIGNATURE"] = max(sockets)[1]
def run_json(*args):
result = subprocess.run(args, check=False, text=True, capture_output=True)
if result.returncode != 0 or not result.stdout.strip():
return None
return json.loads(result.stdout)
def dispatch_lua(expression):
return subprocess.run(["hyprctl", "dispatch", expression], check=False).returncode == 0
def dispatch_legacy(*args):
return subprocess.run(["hyprctl", "dispatch", *args], check=False).returncode == 0
def normal_workspace(window):
workspace = window.get("workspace") or {}
workspace_id = workspace.get("id")
return isinstance(workspace_id, int) and workspace_id >= 0
def window_address(window):
address = window.get("address") or ""
return address if isinstance(address, str) and address else None
def clean_text(value):
return str(value or "").replace("\t", " ").replace("\n", " ").strip()
def app_dirs():
seen = set()
for base in os.environ.get("XDG_DATA_DIRS", "/run/current-system/sw/share:/usr/share:/usr/local/share").split(":"):
path = Path(base) / "applications"
if path.is_dir() and path not in seen:
seen.add(path)
yield path
local = Path.home() / ".local/share/applications"
if local.is_dir() and local not in seen:
yield local
def parse_desktop_file(path):
icon = None
wm_class = None
try:
with path.open(errors="ignore") as handle:
for raw_line in handle:
line = raw_line.strip()
if not icon and line.startswith("Icon="):
icon = line.removeprefix("Icon=").strip()
elif not wm_class and line.startswith("StartupWMClass="):
wm_class = line.removeprefix("StartupWMClass=").strip()
if icon and wm_class:
break
except OSError:
return None, None
return icon, wm_class
def icon_map():
mapping = {}
for directory in app_dirs():
for desktop_file in directory.glob("*.desktop"):
icon, wm_class = parse_desktop_file(desktop_file)
if not icon:
continue
mapping.setdefault(desktop_file.stem.lower(), icon)
if wm_class:
mapping.setdefault(wm_class.lower(), icon)
return mapping
def icon_for(mapping, class_name):
class_key = clean_text(class_name).lower()
return mapping.get(class_key, class_key or "application-x-executable")
def rofi_index(entries, prompt):
menu = b"".join(entry.encode("utf-8", errors="replace") for entry in entries)
result = subprocess.run(
["rofi", "-dmenu", "-i", "-show-icons", "-p", prompt, "-format", "i"],
input=menu,
check=False,
capture_output=True,
)
if result.returncode != 0:
return None
try:
return int(result.stdout.decode("utf-8").strip())
except ValueError:
return None
def candidates(mode, clients, active_workspace, active_window):
current_ws = (active_workspace or {}).get("id")
focused = window_address(active_window or {}) if active_window else None
filtered = []
for window in clients:
if not normal_workspace(window):
continue
address = window_address(window)
if not address:
continue
workspace_id = (window.get("workspace") or {}).get("id")
if mode == "bring" and workspace_id == current_ws:
continue
if mode == "replace" and address == focused:
continue
filtered.append(window)
filtered.sort(
key=lambda window: (
(window.get("workspace") or {}).get("id", 9999),
window.get("focusHistoryID", 999999),
clean_text(window.get("class")).lower(),
clean_text(window.get("title")).lower(),
)
)
return filtered
def menu_entry(window, icons):
class_name = clean_text(window.get("class"))
title = clean_text(window.get("title"))
workspace_id = (window.get("workspace") or {}).get("id", "?")
label = f"{class_name[:24]:<24} {title} WS:{workspace_id}"
icon = icon_for(icons, class_name)
return f"{label}\0icon\x1f{icon}\n"
def focus_window(address):
if dispatch_lua(f'hl.dsp.focus({{ window = "address:{address}" }})'):
return
dispatch_legacy("focuswindow", f"address:{address}")
def move_to_workspace(address, workspace_id):
workspace = str(workspace_id)
if dispatch_lua(f'hl.dsp.window.move({{ workspace = "{workspace}", window = "address:{address}" }})'):
return
dispatch_legacy("movetoworkspace", f"{workspace},address:{address}")
def swap_with_focused(target_address, focused_address):
if dispatch_lua(
f'hl.dsp.window.swap({{ target = "address:{target_address}", window = "address:{focused_address}" }})'
):
return
dispatch_legacy("hy3:movewindow", f"address:{target_address}")
def activate(mode, window, active_workspace, active_window):
address = window_address(window)
if not address:
return
if mode == "go":
focus_window(address)
return
if mode == "bring":
current_ws = (active_workspace or {}).get("id")
if current_ws is not None:
move_to_workspace(address, current_ws)
focus_window(address)
return
if mode == "replace":
focused = window_address(active_window or {})
if focused and focused != address:
swap_with_focused(address, focused)
focus_window(address)
def main():
parser = argparse.ArgumentParser(description="Rofi window go/bring/replace for Hyprland.")
parser.add_argument("mode", choices=sorted(PROMPTS))
parser.add_argument("--print-candidates", action="store_true")
parser.add_argument("--select-index", type=int)
args = parser.parse_args()
ensure_hyprland_instance()
clients = run_json("hyprctl", "clients", "-j") or []
active_workspace = run_json("hyprctl", "activeworkspace", "-j") or {}
active_window = run_json("hyprctl", "activewindow", "-j") or {}
windows = candidates(args.mode, clients, active_workspace, active_window)
if args.print_candidates:
print(json.dumps(windows, indent=2, sort_keys=True))
return 0
if not windows:
return 0
if args.select_index is None:
icons = icon_map()
entries = [menu_entry(window, icons) for window in windows]
index = rofi_index(entries, PROMPTS[args.mode])
else:
index = args.select_index
if index is None or index < 0 or index >= len(windows):
return 0
activate(args.mode, windows[index], active_workspace, active_window)
return 0
if __name__ == "__main__":
sys.exit(main())

78
dotfiles/lib/bin/hypr_shell_ui Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v desktop_shell_ui >/dev/null 2>&1; then
shell_ui="$(desktop_shell_ui current 2>/dev/null || true)"
else
shell_ui=""
fi
shell_ui="${shell_ui:-${IM_HYPRLAND_SHELL_UI:-taffybar}}"
run_noctalia() {
noctalia-shell ipc --any-display call "$@"
}
run_launcher() {
case "$shell_ui" in
noctalia)
run_noctalia launcher toggle
;;
taffybar|rofi)
exec rofi -show drun -show-icons
;;
*)
echo "unknown IM_HYPRLAND_SHELL_UI: $shell_ui" >&2
exit 2
;;
esac
}
run_window_picker() {
local mode="${1:-}"
case "$mode" in
go|bring|replace) ;;
*)
echo "usage: hypr_shell_ui window {go|bring|replace}" >&2
exit 2
;;
esac
if [[ "$shell_ui" == "noctalia" && "${IM_HYPRLAND_NOCTALIA_WINDOW_PICKER:-0}" == "1" ]]; then
# Future Noctalia launcher-provider hook. Until that plugin exists or if it
# fails to load, keep the existing rofi picker as the working path.
run_noctalia "plugin:hypr-window-picker" "$mode" 2>/dev/null && exit 0
fi
exec hypr_rofi_window "$mode"
}
case "${1:-}" in
launcher)
run_launcher
;;
run)
exec rofi -show run
;;
control-center)
if [[ "$shell_ui" == "noctalia" ]]; then
run_noctalia controlCenter toggle || true
fi
exit 0
;;
settings)
if [[ "$shell_ui" == "noctalia" ]]; then
run_noctalia settings toggle || true
fi
exit 0
;;
window)
shift
run_window_picker "$@"
;;
*)
echo "usage: hypr_shell_ui {launcher|run|control-center|settings|window}" >&2
exit 2
;;
esac

275
dotfiles/lib/bin/roborock-control Executable file
View File

@@ -0,0 +1,275 @@
#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
CONFIG_PATH = Path.home() / ".config" / "roborock-control" / "config.json"
PYTHON_ENV_EXPR = (
"with import <nixpkgs> {}; "
"python313.withPackages (ps: [ ps.python-roborock ps.pyyaml ps.pyshark ])"
)
COMMON_COMMANDS = {
"start": ("app_start", None),
"pause": ("app_pause", None),
"stop": ("app_stop", None),
"dock": ("app_charge", None),
"find": ("find_me", None),
"dust": ("app_start_collect_dust", None),
"stop-dust": ("app_stop_collect_dust", None),
"wash": ("app_start_wash", None),
"stop-wash": ("app_stop_wash", None),
}
def load_config():
if not CONFIG_PATH.exists():
return {}
return json.loads(CONFIG_PATH.read_text())
def save_config(config):
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(json.dumps(config, indent=2, sort_keys=True) + "\n")
def roborock_args(*args):
if os.getenv("ROBOROCK_CONTROL_RUNNER") == "direct":
return ["roborock", *args]
return [
"nix",
"shell",
"--impure",
"--expr",
PYTHON_ENV_EXPR,
"--command",
"roborock",
*args,
]
def run_roborock(*args, capture=False, check=True):
kwargs = {
"text": True,
"check": check,
}
if capture:
kwargs["stdout"] = subprocess.PIPE
kwargs["stderr"] = subprocess.PIPE
return subprocess.run(roborock_args(*args), **kwargs)
def configured_device_id(args, config):
return args.device_id or os.getenv("ROBOROCK_DEVICE_ID") or config.get("device_id")
def infer_single_device_id():
result = run_roborock("list-devices", capture=True)
devices = json.loads(result.stdout)
if len(devices) != 1:
names = ", ".join(sorted(devices)) or "none"
raise SystemExit(
"Could not infer a default device. "
f"Found {len(devices)} devices: {names}. "
"Pass --device-id or run 'roborock-control config --device-id <id>'."
)
return next(iter(devices.values()))
def get_device_id(args, config):
return configured_device_id(args, config) or infer_single_device_id()
def handle_config(args):
config = load_config()
changed = False
if args.clear:
config = {}
changed = True
if args.device_id is not None:
config["device_id"] = args.device_id
changed = True
if args.email is not None:
config["email"] = args.email
changed = True
if changed:
save_config(config)
print(json.dumps(config, indent=2, sort_keys=True))
def handle_login(args):
config = load_config()
email = args.email or os.getenv("ROBOROCK_EMAIL") or config.get("email")
if not email:
raise SystemExit(
"Email is required. Pass --email or run "
"'roborock-control config --email <address>'."
)
cli_args = ["login", "--email", email]
if args.reauth:
cli_args.append("--reauth")
if args.password_command:
password = subprocess.check_output(args.password_command, shell=True, text=True).strip()
cli_args.extend(["--password", password])
run_roborock(*cli_args)
def handle_devices(args):
if args.refresh:
run_roborock("discover")
run_roborock("list-devices")
def command_with_device(args, upstream_command):
config = load_config()
return [upstream_command, "--device_id", get_device_id(args, config)]
def handle_home(args):
cli_args = command_with_device(args, "home")
if args.refresh:
cli_args.append("--refresh")
run_roborock(*cli_args)
def handle_maps(args):
run_roborock(*command_with_device(args, "maps"))
def handle_rooms(args):
run_roborock(*command_with_device(args, "rooms"))
def handle_map_data(args):
cli_args = command_with_device(args, "map-data")
if args.include_path:
cli_args.append("--include_path")
run_roborock(*cli_args)
def handle_map_image(args):
run_roborock(
*command_with_device(args, "map-image"),
"--output-file",
args.output_file,
)
def handle_command(args):
config = load_config()
params = args.params
cli_args = [
"command",
"--device_id",
get_device_id(args, config),
"--cmd",
args.command_name,
]
if params is not None:
cli_args.extend(["--params", params])
run_roborock(*cli_args)
def handle_common_command(args):
command_name, params = COMMON_COMMANDS[args.action]
config = load_config()
cli_args = [
"command",
"--device_id",
get_device_id(args, config),
"--cmd",
command_name,
]
if params is not None:
cli_args.extend(["--params", params])
run_roborock(*cli_args)
def handle_status(args):
config = load_config()
run_roborock("status", "--device_id", get_device_id(args, config))
def handle_upstream(args):
run_roborock(*args.args)
def build_parser():
parser = argparse.ArgumentParser(
description="Control Roborock vacuums through python-roborock."
)
parser.add_argument(
"--device-id",
help="Roborock device id. Defaults to ROBOROCK_DEVICE_ID, saved config, or the only cached device.",
)
subparsers = parser.add_subparsers(dest="subcommand", required=True)
config_parser = subparsers.add_parser("config", help="Show or update saved defaults")
config_parser.add_argument("--device-id")
config_parser.add_argument("--email")
config_parser.add_argument("--clear", action="store_true")
config_parser.set_defaults(func=handle_config)
login_parser = subparsers.add_parser("login", help="Login with email code or password")
login_parser.add_argument("--email")
login_parser.add_argument("--reauth", action="store_true")
login_parser.add_argument(
"--password-command",
help="Shell command that prints the Roborock password, e.g. 'pass show path/to/login'.",
)
login_parser.set_defaults(func=handle_login)
devices_parser = subparsers.add_parser("devices", help="List cached devices")
devices_parser.add_argument("--refresh", action="store_true", help="Refresh discovery first")
devices_parser.set_defaults(func=handle_devices)
subparsers.add_parser("status", help="Show vacuum status").set_defaults(func=handle_status)
home_parser = subparsers.add_parser("home", help="Discover and cache home layout")
home_parser.add_argument("--refresh", action="store_true")
home_parser.set_defaults(func=handle_home)
subparsers.add_parser("maps", help="Show map metadata").set_defaults(func=handle_maps)
subparsers.add_parser("rooms", help="Show room metadata").set_defaults(func=handle_rooms)
map_data_parser = subparsers.add_parser("map-data", help="Show parsed map data as JSON")
map_data_parser.add_argument("--include-path", action="store_true")
map_data_parser.set_defaults(func=handle_map_data)
map_image_parser = subparsers.add_parser("map-image", help="Save the map image")
map_image_parser.add_argument("output_file")
map_image_parser.set_defaults(func=handle_map_image)
raw_parser = subparsers.add_parser("raw", help="Send a raw python-roborock command")
raw_parser.add_argument("command_name")
raw_parser.add_argument("params", nargs="?")
raw_parser.set_defaults(func=handle_command)
upstream_parser = subparsers.add_parser("cli", help="Pass arguments to the upstream CLI")
upstream_parser.add_argument("args", nargs=argparse.REMAINDER)
upstream_parser.set_defaults(func=handle_upstream)
for action in sorted(COMMON_COMMANDS):
subparsers.add_parser(action).set_defaults(func=handle_common_command, action=action)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
try:
args.func(args)
except subprocess.CalledProcessError as error:
if error.stdout:
print(error.stdout, end="")
if error.stderr:
print(error.stderr, end="", file=sys.stderr)
raise SystemExit(error.returncode)
if __name__ == "__main__":
main()

View File

@@ -69,6 +69,7 @@
multiplexerAliases = import ../../shared/multiplexer-aliases.nix;
excludedTopLevelEntries = [
"codex"
"config"
];

View File

@@ -5,9 +5,10 @@
...
}: let
cfg = config.myModules.codexGeneratedSkills;
oos = config.lib.file.mkOutOfStoreSymlink;
in {
options.myModules.codexGeneratedSkills = {
enable = lib.mkEnableOption "generated Codex skill setup";
enable = lib.mkEnableOption "Codex home setup";
codexHome = lib.mkOption {
type = lib.types.str;
@@ -15,6 +16,12 @@ in {
description = "Codex home directory.";
};
worktreeCodexDir = lib.mkOption {
type = lib.types.str;
default = "${config.home.homeDirectory}/dotfiles/dotfiles/codex";
description = "Codex dotfiles directory in the live worktree.";
};
skillsDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.codexHome}/skills";
@@ -29,6 +36,67 @@ in {
};
config = lib.mkIf cfg.enable {
home.file = {
".codex/.gitignore" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/.gitignore";
};
".codex/AGENTS.md" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/AGENTS.md";
};
".codex/skills" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/skills";
};
};
home.activation.prepareCodexDirectory = lib.hm.dag.entryBefore ["checkLinkTargets"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
worktree_codex=${lib.escapeShellArg cfg.worktreeCodexDir}
if [ -L "$codex_home" ] && [ "$(readlink "$codex_home")" = "$worktree_codex" ]; then
rm -f "$codex_home"
mkdir -p "$codex_home"
elif [ ! -e "$codex_home" ]; then
mkdir -p "$codex_home"
elif [ ! -d "$codex_home" ]; then
echo "Skipping Codex setup because $codex_home is not a directory" >&2
exit 1
fi
'';
home.activation.generateCodexConfig = lib.hm.dag.entryAfter ["writeBoundary"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
base=${lib.escapeShellArg "${cfg.worktreeCodexDir}/config.toml"}
local_config=${lib.escapeShellArg "${cfg.worktreeCodexDir}/config.local.toml"}
target="$codex_home/config.toml"
if [ ! -r "$base" ]; then
echo "Missing shared Codex config at $base" >&2
exit 1
fi
mkdir -p "$codex_home"
tmp="$(mktemp "$codex_home/config.toml.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
chmod 600 "$tmp"
cat "$base" > "$tmp"
if [ -r "$local_config" ]; then
printf '\n' >> "$tmp"
cat "$local_config" >> "$tmp"
fi
if [ -e "$target" ] && cmp -s "$tmp" "$target"; then
rm -f "$tmp"
else
mv -f "$tmp" "$target"
fi
'';
home.activation.setupCodexGeneratedSkills = lib.hm.dag.entryAfter ["writeBoundary"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
skills_dir=${lib.escapeShellArg cfg.skillsDir}

View File

@@ -29,12 +29,14 @@
./laptop.nix
./nix.nix
./notifications-tray-icon.nix
./noctalia.nix
./nvidia.nix
./options.nix
./plasma.nix
./postgres.nix
./rabbitmq.nix
./quickshell.nix
./remote-hyprland.nix
./secrets.nix
./ssh.nix
./sni.nix

View File

@@ -6,7 +6,20 @@
makeEnable,
...
}:
makeEnable config "myModules.desktop" true {
let
cfg = config.myModules.desktop;
desktopShellUi = pkgs.writeShellApplication {
name = "desktop_shell_ui";
runtimeInputs = [
pkgs.bash
pkgs.coreutils
pkgs.systemd
];
text = ''
exec ${../dotfiles/lib/bin/desktop_shell_ui} "$@"
'';
};
enabledModule = makeEnable config "myModules.desktop" true {
services.greenclip.enable = true;
imports = [
./fonts.nix
@@ -40,8 +53,11 @@ makeEnable config "myModules.desktop" true {
enable = true;
};
# This is for the benefit of VSCODE running natively in wayland
environment.sessionVariables.NIXOS_OZONE_WL = "1";
environment.sessionVariables = {
# This is for the benefit of VSCODE running natively in wayland
NIXOS_OZONE_WL = "1";
IM_HYPRLAND_SHELL_UI = cfg.shellUi;
};
system.activationScripts.playwrightChromeCompat.text = lib.optionalString (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ''
# Playwright's Chrome channel lookup expects the FHS path below.
@@ -87,6 +103,8 @@ makeEnable config "myModules.desktop" true {
environment.systemPackages = with pkgs;
[
desktopShellUi
# Appearance
adwaita-icon-theme
hicolor-icon-theme
@@ -169,4 +187,18 @@ makeEnable config "myModules.desktop" true {
]
else []
);
};
in
enabledModule
// {
options = lib.recursiveUpdate enabledModule.options {
myModules.desktop.shellUi = lib.mkOption {
type = lib.types.enum [ "noctalia" "taffybar" ];
default = "taffybar";
description = ''
Desktop shell UI used by Hyprland-oriented bindings. This controls
the active shell service and Hyprland launcher/window picker dispatch.
'';
};
};
}

View File

@@ -1,5 +1,8 @@
{ config, lib, ... }:
let
{
config,
lib,
...
}: let
# Replicate the useful part of rcm/rcup:
# - dotfiles live in ~/dotfiles/dotfiles (no leading dots in the repo)
# - links in $HOME add a leading '.' to the first path component
@@ -16,6 +19,9 @@ let
srcDotfiles = ../dotfiles;
excludedTop = [
# Managed by nix-shared/home-manager/codex-generated-skills.nix so
# config.toml can be generated from shared and machine-local fragments.
"codex"
# Managed by Nix directly (PATH/fpath), not meant to appear as ~/.lib.
"lib"
# Avoid colliding with HM-generated xdg.configFile entries for now.
@@ -24,27 +30,25 @@ let
"emacs.d"
];
firstComponent = rel:
let parts = lib.splitString "/" rel;
in lib.elemAt parts 0;
firstComponent = rel: let
parts = lib.splitString "/" rel;
in
lib.elemAt parts 0;
isExcluded = rel: lib.elem (firstComponent rel) excludedTop;
listFilesRec = dir:
let
entries = builtins.readDir dir;
names = builtins.attrNames entries;
go = name:
let
ty = entries.${name};
path = dir + "/${name}";
in
if ty == "directory" then
map (p: "${name}/${p}") (listFilesRec path)
else
[ name ];
listFilesRec = dir: let
entries = builtins.readDir dir;
names = builtins.attrNames entries;
go = name: let
ty = entries.${name};
path = dir + "/${name}";
in
lib.concatLists (map go names);
if ty == "directory"
then map (p: "${name}/${p}") (listFilesRec path)
else [name];
in
lib.concatLists (map go names);
managedRelFiles =
lib.filter (rel: !(isExcluded rel)) (listFilesRec srcDotfiles);
@@ -53,9 +57,10 @@ let
lib.nameValuePair ".${rel}" {
source = oos "${worktreeDotfiles}/${rel}";
};
in
{
imports = [ ../nix-shared/home-manager/codex-generated-skills.nix ];
in {
imports = [
../nix-shared/home-manager/codex-generated-skills.nix
];
home.file =
builtins.listToAttrs (map mkManaged managedRelFiles);
@@ -74,5 +79,4 @@ in
echo "Skipping ~/.emacs.d relink because it is not a symlink" >&2
fi
'';
}

View File

@@ -10,6 +10,8 @@ makeEnable config "myModules.extra" false {
signal-desktop
gource
gimp
kef
roborock-control
texlive.combined.scheme-full
tor
yt-dlp

702
nixos/flake.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -93,21 +93,24 @@
# Hyprland and plugins from official flakes for proper plugin compatibility
hyprland = {
url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.53.0";
url = "git+https://github.com/hyprwm/Hyprland?submodules=1";
};
hyprland-lua-config = {
# Experimental Lua config branch from PR 13817.
url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/pull/13817/head";
hyprNStack = {
url = "github:colonelpanic8/hyprNStack?ref=hyprland-lua-integration";
inputs = {
hyprland.follows = "hyprland";
nixpkgs.follows = "nixpkgs";
};
};
hy3 = {
url = "github:outfoxxed/hy3?ref=hl0.53.0";
hyprland-plugins-lua = {
url = "github:colonelpanic8/hyprland-plugins?ref=codex/fix-main-ci";
inputs.hyprland.follows = "hyprland";
};
hyprland-plugins = {
url = "github:hyprwm/hyprland-plugins?ref=v0.53.0";
hyprwinview = {
url = "github:colonelpanic8/hyprwinview";
inputs.hyprland.follows = "hyprland";
};
@@ -148,26 +151,11 @@
};
};
taffybar = {
# Use a remote, lockfile-pinned input so rebuilds are reproducible across
# machines. For local development, use `nixos-rebuild --override-input taffybar path:...`.
url = "github:taffybar/taffybar?ref=codex/fix-gdk-backend-strut-detection";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
xmonad.follows = "xmonad";
xmonad-contrib.follows = "xmonad-contrib";
weeder-nix.url = "github:NorfairKing/weeder-nix";
weeder-nix.inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
};
};
imalison-taffybar = {
url = "path:../dotfiles/config/taffybar";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
taffybar.follows = "taffybar";
xmonad.follows = "xmonad";
};
};
@@ -218,6 +206,11 @@
inputs.nixpkgs.follows = "nixpkgs";
};
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs @ {
@@ -225,7 +218,6 @@
nixpkgs,
nixos-hardware,
home-manager,
taffybar,
xmonad,
nixtheplanet,
xmonad-contrib,
@@ -234,8 +226,6 @@
agenix,
imalison-taffybar,
hyprland,
hy3,
hyprland-plugins,
org-agenda-api,
flake-utils,
...
@@ -430,6 +420,7 @@
"https://colonelpanic8-dotfiles.cachix.org"
"https://codex-cli.cachix.org"
"https://claude-code.cachix.org"
"https://noctalia.cachix.org"
];
extra-trusted-substituters = [
"https://ai.cachix.org"
@@ -451,6 +442,7 @@
"colonelpanic8-dotfiles.cachix.org-1:O6GF3nptpeMFapX29okzO92eSWXR36zqW6ZF2C8P0eQ="
"codex-cli.cachix.org-1:1Br3H1hHoRYG22n//cGKJOk3cQXgYobUel6O8DgSing="
"claude-code.cachix.org-1:YeXf2aNu7UTX8Vwrze0za1WEDS+4DuI2kVeWEE4fsRk="
"noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4="
];
};
nixosConfigurations =
@@ -467,6 +459,7 @@
} // flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
# Get short revs for tagging
orgApiRev = builtins.substring 0 7 (org-agenda-api.rev or "unknown");
@@ -483,10 +476,178 @@
containerLib = import ../org-agenda-api/container.nix {
inherit pkgs system tangledConfig org-agenda-api orgApiRev dotfilesRev;
};
hyprlandPkgs = import nixpkgs {
inherit system;
overlays = [ hyprland.overlays.hyprland-packages ];
};
hyprWorkspaceHistory = hyprlandPkgs.hyprlandPlugins.mkHyprlandPlugin {
pluginName = "hypr-workspace-history";
version = "0.1.0";
src = builtins.path {
path = ../dotfiles/config/hypr/workspace-history-plugin;
name = "hypr-workspace-history-source";
};
inherit (hyprland.packages.${system}.hyprland) nativeBuildInputs;
meta = {
description = "Workspace history cycling plugin for Hyprland";
license = lib.licenses.bsd3;
platforms = lib.platforms.linux;
};
};
in {
packages = {
colonelpanic-org-agenda-api = containerLib.containers.colonelpanic;
kat-org-agenda-api = containerLib.containers.kat;
} // lib.optionalAttrs pkgs.stdenv.isLinux {
hyprNStack = inputs.hyprNStack.packages.${system}.hyprNStack;
hyprexpo-lua = inputs.hyprland-plugins-lua.packages.${system}.hyprexpo;
hyprwinview = inputs.hyprwinview.packages.${system}.hyprwinview;
hypr-workspace-history = hyprWorkspaceHistory;
};
checks = lib.optionalAttrs pkgs.stdenv.isLinux {
hyprNStack = inputs.hyprNStack.packages.${system}.hyprNStack;
hyprexpo-lua = inputs.hyprland-plugins-lua.packages.${system}.hyprexpo;
hyprwinview = inputs.hyprwinview.packages.${system}.hyprwinview;
hypr-workspace-history = hyprWorkspaceHistory;
hyprland-config-syntax = pkgs.runCommand "hyprland-config-syntax" {
nativeBuildInputs = [ pkgs.lua5_4 ];
} ''
cp ${../dotfiles/config/hypr/hyprland.lua} hyprland.lua
luac -p hyprland.lua
if grep -n 'hyprctl' hyprland.lua | grep -v 'hyprctl reload' | grep -v 'hyprctl dispatch hyprwinview:overview'; then
echo "hyprland.lua should not shell out to hyprctl for window/workspace manipulation" >&2
exit 1
fi
lua <<'LUA'
local callbacks = {}
local function noop() end
local function dispatcher_proxy()
local proxy = {}
return setmetatable(proxy, {
__index = function()
return dispatcher_proxy()
end,
__call = function()
return noop
end,
})
end
local notification = {
is_alive = function()
return true
end,
set_text = noop,
set_timeout = noop,
pause = noop,
resume = noop,
set_paused = noop,
dismiss = noop,
}
local monitor = {
id = 1,
name = "stub-monitor",
focused = true,
}
local workspace = {
id = 1,
name = "1",
windows = 0,
special = false,
monitor = monitor,
}
monitor.active_workspace = workspace
hl = {
animation = noop,
bind = noop,
config = noop,
curve = noop,
env = noop,
exec_cmd = noop,
define_submap = function(_, reset_or_callback, callback)
local cb = type(reset_or_callback) == "function" and reset_or_callback or callback
if cb then
cb()
end
end,
monitor = noop,
workspace_rule = noop,
window_rule = noop,
dsp = dispatcher_proxy(),
notification = {
create = function()
return notification
end,
},
plugin = {
load = noop,
},
get_active_workspace = function()
return workspace
end,
get_active_monitor = function()
return monitor
end,
get_active_window = function()
return nil
end,
get_monitor = function()
return monitor
end,
get_workspace = function(id)
if tostring(id) == "1" then
return workspace
end
return nil
end,
get_windows = function()
return {}
end,
get_workspace_windows = function()
return {}
end,
on = function(_, callback)
callbacks[#callbacks + 1] = callback
end,
timer = function(callback)
callback()
return {
set_enabled = noop,
}
end,
}
dofile("./hyprland.lua")
for _, callback in ipairs(callbacks) do
callback()
end
LUA
touch "$out"
'';
hyprland-verify-config = let
hyprlandPackage = inputs.hyprland.packages.${system}.hyprland;
hyprNStackPackage = inputs.hyprNStack.packages.${system}.hyprNStack;
in pkgs.runCommand "hyprland-lua-verify-config" {} ''
cp ${../dotfiles/config/hypr/hyprland.lua} hyprland.lua
substituteInPlace hyprland.lua \
--replace-fail /run/current-system/sw/lib/libhyprNStack.so \
${hyprNStackPackage}/lib/libhyprNStack.so
export XDG_RUNTIME_DIR="$TMPDIR/runtime"
mkdir -p "$XDG_RUNTIME_DIR"
HYPRLAND_NO_CRASHREPORTER=1 ${pkgs.coreutils}/bin/timeout 20s \
${hyprlandPackage}/bin/Hyprland --verify-config --config "$PWD/hyprland.lua"
touch "$out"
'';
};
# Dev shell for org-agenda-api deployment

View File

@@ -6,6 +6,152 @@
...
}: let
mimeMap = desktopId: mimeTypes: lib.genAttrs mimeTypes (_: [desktopId]);
browser = "google-chrome.desktop";
imageViewer = "org.kde.gwenview.desktop";
pdfViewer = "okularApplication_pdf.desktop";
comicViewer = "okularApplication_comicbook.desktop";
djvuViewer = "okularApplication_djvu.desktop";
ebookViewer = "okularApplication_epub.desktop";
mobiViewer = "okularApplication_mobi.desktop";
xpsViewer = "okularApplication_xps.desktop";
mediaPlayer = "vlc.desktop";
archiveManager = "org.gnome.FileRoller.desktop";
fileManager = "thunar.desktop";
wordProcessor = "writer.desktop";
spreadsheet = "calc.desktop";
presentation = "impress.desktop";
defaultApplications =
(mimeMap imageViewer [
"image/avif"
"image/bmp"
"image/gif"
"image/heic"
"image/heif"
"image/jpeg"
"image/jxl"
"image/png"
"image/svg+xml"
"image/svg+xml-compressed"
"image/tiff"
"image/vnd.microsoft.icon"
"image/webp"
])
// (mimeMap pdfViewer [
"application/pdf"
"application/x-bzpdf"
"application/x-gzpdf"
])
// (mimeMap comicViewer [
"application/x-cb7"
"application/x-cbr"
"application/x-cbt"
"application/x-cbz"
])
// (mimeMap djvuViewer [
"image/vnd.djvu"
])
// (mimeMap ebookViewer [
"application/epub+zip"
])
// (mimeMap mobiViewer [
"application/x-mobipocket-ebook"
])
// (mimeMap xpsViewer [
"application/oxps"
"application/vnd.ms-xpsdocument"
])
// (mimeMap mediaPlayer [
"application/ogg"
"audio/flac"
"audio/mp4"
"audio/mpeg"
"audio/ogg"
"audio/opus"
"audio/webm"
"audio/wav"
"audio/x-flac"
"audio/x-wav"
"video/mp4"
"video/ogg"
"video/quicktime"
"video/webm"
"video/x-matroska"
"video/x-msvideo"
])
// (mimeMap archiveManager [
"application/bzip2"
"application/gzip"
"application/vnd.rar"
"application/x-7z-compressed"
"application/x-bzip"
"application/x-compressed-tar"
"application/x-gzip"
"application/x-rar"
"application/x-rar-compressed"
"application/x-tar"
"application/x-xz"
"application/x-zip-compressed"
"application/zip"
"application/zstd"
])
// (mimeMap wordProcessor [
"application/msword"
"application/rtf"
"application/vnd.ms-word"
"application/vnd.oasis.opendocument.text"
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
])
// (mimeMap spreadsheet [
"application/vnd.ms-excel"
"application/vnd.oasis.opendocument.spreadsheet"
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
"text/csv"
"text/tab-separated-values"
])
// (mimeMap presentation [
"application/mspowerpoint"
"application/vnd.ms-powerpoint"
"application/vnd.oasis.opendocument.presentation"
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
"application/vnd.openxmlformats-officedocument.presentationml.slideshow"
])
// (mimeMap fileManager [
"inode/directory"
])
// (mimeMap browser [
"application/rdf+xml"
"application/rss+xml"
"application/xhtml+xml"
"application/xhtml_xml"
"application/xml"
"text/html"
"text/xml"
"x-scheme-handler/about"
"x-scheme-handler/http"
"x-scheme-handler/https"
"x-scheme-handler/unknown"
])
// {
"x-scheme-handler/element" = ["element-desktop.desktop"];
"x-scheme-handler/magnet" = ["transmission-gtk.desktop"];
};
mimeAppsListText = let
formatApplications = applications:
lib.concatStringsSep "\n" (
lib.mapAttrsToList (
mimeType: desktopIds: "${mimeType}=${lib.concatStringsSep ";" desktopIds};"
)
applications
);
in ''
[Added Associations]
${formatApplications defaultApplications}
[Default Applications]
${formatApplications defaultApplications}
[Removed Associations]
'';
in {
# Automatic garbage collection of old home-manager generations
nix.gc = {
@@ -27,148 +173,44 @@ in {
static_history = []
'';
xdg.configFile."zellij/config.kdl".source =
config.lib.file.mkOutOfStoreSymlink
"${config.home.homeDirectory}/dotfiles/dotfiles/config/zellij/config.kdl";
xdg.mimeApps = lib.mkIf nixos.config.myModules.desktop.enable (
let
browser = "google-chrome.desktop";
imageViewer = "org.kde.gwenview.desktop";
pdfViewer = "okularApplication_pdf.desktop";
comicViewer = "okularApplication_comicbook.desktop";
djvuViewer = "okularApplication_djvu.desktop";
ebookViewer = "okularApplication_epub.desktop";
mobiViewer = "okularApplication_mobi.desktop";
xpsViewer = "okularApplication_xps.desktop";
mediaPlayer = "vlc.desktop";
archiveManager = "org.gnome.FileRoller.desktop";
fileManager = "thunar.desktop";
wordProcessor = "writer.desktop";
spreadsheet = "calc.desktop";
presentation = "impress.desktop";
xdg.configFile."menus/applications.menu" = lib.mkIf nixos.config.myModules.desktop.enable {
source = "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu";
};
defaultApplications =
(mimeMap imageViewer [
"image/avif"
"image/bmp"
"image/gif"
"image/heic"
"image/heif"
"image/jpeg"
"image/jxl"
"image/png"
"image/svg+xml"
"image/svg+xml-compressed"
"image/tiff"
"image/vnd.microsoft.icon"
"image/webp"
])
// (mimeMap pdfViewer [
"application/pdf"
"application/x-bzpdf"
"application/x-gzpdf"
])
// (mimeMap comicViewer [
"application/x-cb7"
"application/x-cbr"
"application/x-cbt"
"application/x-cbz"
])
// (mimeMap djvuViewer [
"image/vnd.djvu"
])
// (mimeMap ebookViewer [
"application/epub+zip"
])
// (mimeMap mobiViewer [
"application/x-mobipocket-ebook"
])
// (mimeMap xpsViewer [
"application/oxps"
"application/vnd.ms-xpsdocument"
])
// (mimeMap mediaPlayer [
"application/ogg"
"audio/flac"
"audio/mp4"
"audio/mpeg"
"audio/ogg"
"audio/opus"
"audio/webm"
"audio/wav"
"audio/x-flac"
"audio/x-wav"
"video/mp4"
"video/ogg"
"video/quicktime"
"video/webm"
"video/x-matroska"
"video/x-msvideo"
])
// (mimeMap archiveManager [
"application/bzip2"
"application/gzip"
"application/vnd.rar"
"application/x-7z-compressed"
"application/x-bzip"
"application/x-compressed-tar"
"application/x-gzip"
"application/x-rar"
"application/x-rar-compressed"
"application/x-tar"
"application/x-xz"
"application/x-zip-compressed"
"application/zip"
"application/zstd"
])
// (mimeMap wordProcessor [
"application/msword"
"application/rtf"
"application/vnd.ms-word"
"application/vnd.oasis.opendocument.text"
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
])
// (mimeMap spreadsheet [
"application/vnd.ms-excel"
"application/vnd.oasis.opendocument.spreadsheet"
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
"text/csv"
"text/tab-separated-values"
])
// (mimeMap presentation [
"application/mspowerpoint"
"application/vnd.ms-powerpoint"
"application/vnd.oasis.opendocument.presentation"
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
"application/vnd.openxmlformats-officedocument.presentationml.slideshow"
])
// (mimeMap fileManager [
"inode/directory"
])
// (mimeMap browser [
"application/rdf+xml"
"application/rss+xml"
"application/xhtml+xml"
"application/xhtml_xml"
"application/xml"
"text/html"
"text/xml"
"x-scheme-handler/about"
"x-scheme-handler/http"
"x-scheme-handler/https"
"x-scheme-handler/unknown"
])
// {
"x-scheme-handler/element" = ["element-desktop.desktop"];
"x-scheme-handler/magnet" = ["transmission-gtk.desktop"];
};
in {
enable = true;
associations.added = defaultApplications;
inherit defaultApplications;
}
);
xdg.configFile."kde-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.configFile."none+xmonad-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.configFile."xmonad-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.configFile."hyprland-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.dataFile."mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.dataFile."applications/kde-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.mimeApps = lib.mkIf nixos.config.myModules.desktop.enable {
enable = true;
associations.added = defaultApplications;
inherit defaultApplications;
};
home.activation.refreshChromeDesktopMimeCache = lib.hm.dag.entryAfter ["writeBoundary"] ''
applications_dir="$HOME/.local/share/applications"
@@ -180,6 +222,7 @@ in {
do
if [ -f "$desktop_file" ]; then
${pkgs.gnused}/bin/sed -i \
-e 's,application/pdf;,,g' \
-e 's,image/gif;,,g' \
-e 's,image/jpeg;,,g' \
-e 's,image/png;,,g' \
@@ -188,10 +231,34 @@ in {
fi
done
for desktop_file in "$applications_dir"/okular*.desktop "$applications_dir"/vlc*.desktop; do
if [ -f "$desktop_file" ]; then
${pkgs.gnused}/bin/sed -i \
-e 's,image/avif;,,g' \
-e 's,image/bmp;,,g' \
-e 's,image/gif;,,g' \
-e 's,image/heic;,,g' \
-e 's,image/heif;,,g' \
-e 's,image/jpeg;,,g' \
-e 's,image/jxl;,,g' \
-e 's,image/png;,,g' \
-e 's,image/svg+xml;,,g' \
-e 's,image/svg+xml-compressed;,,g' \
-e 's,image/tiff;,,g' \
-e 's,image/vnd.microsoft.icon;,,g' \
-e 's,image/webp;,,g' \
"$desktop_file"
fi
done
${pkgs.desktop-file-utils}/bin/update-desktop-database "$applications_dir" >/dev/null 2>&1 || true
fi
'';
home.activation.refreshKdeServiceCache = lib.hm.dag.entryAfter ["refreshChromeDesktopMimeCache"] ''
${pkgs.kdePackages.kservice}/bin/kbuildsycoca6 --noincremental >/dev/null 2>&1 || true
'';
xsession = {
enable = true;
preferStatusNotifierItems = true;

View File

@@ -1,23 +1,93 @@
{ config, pkgs, lib, makeEnable, inputs, ... }:
{
config,
pkgs,
lib,
makeEnable,
inputs,
...
}:
let
cfg = config.myModules.hyprland;
system = pkgs.stdenv.hostPlatform.system;
enableExternalPluginPackages = !cfg.useLuaConfigBranch;
hyprlandInput =
if cfg.useLuaConfigBranch
then inputs.hyprland-lua-config
else inputs.hyprland;
hyprexpoPatched = inputs.hyprland-plugins.packages.${system}.hyprexpo.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./patches/hyprexpo-pr-612-workspace-numbers.patch
./patches/hyprexpo-pr-616-bring-mode.patch
hyprlandInput = inputs.hyprland;
hyprlandPluginPackages = [
inputs.hyprNStack.packages.${system}.hyprNStack
inputs.hyprland-plugins-lua.packages.${system}.hyprexpo
inputs.hyprwinview.packages.${system}.hyprwinview
inputs.self.packages.${system}.hypr-workspace-history
];
hyprRofiWindow = pkgs.writeShellApplication {
name = "hypr_rofi_window";
runtimeInputs = [
pkgs.python3
pkgs.rofi
hyprlandInput.packages.${system}.hyprland
];
});
text = ''
exec python3 ${../dotfiles/lib/bin/hypr_rofi_window} "$@"
'';
};
hyprShellUi = pkgs.writeShellApplication {
name = "hypr_shell_ui";
runtimeInputs = [
pkgs.rofi
hyprRofiWindow
inputs.noctalia.packages.${system}.default
];
text = ''
exec ${../dotfiles/lib/bin/hypr_shell_ui} "$@"
'';
};
hyprscratchSettings = {
daemon_options = "clean";
global_options = "";
global_rules = "float;size monitor_w*0.95 monitor_h*0.95;center";
htop = {
command = "alacritty --class htop-scratch --title htop -e htop";
class = "htop-scratch";
};
volume = {
command = "pavucontrol";
class = "org.pulseaudio.pavucontrol";
};
spotify = {
command = "spotify";
class = "spotify";
};
element = {
command = "element-desktop";
class = "Element";
};
slack = {
command = "slack";
class = "Slack";
};
transmission = {
command = "transmission-gtk";
class = "transmission-gtk";
};
dropdown = {
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown";
class = "com.mitchellh.ghostty.dropdown";
options = "persist";
rules = "float;size monitor_w monitor_h*0.5;move 0 60;noborder;noshadow;animation slide";
};
};
enabledModule = makeEnable config "myModules.hyprland" true {
myModules.taffybar.enable = true;
# Install both shell service units so `desktop_shell_ui set ...` can switch
# between them at runtime without a NixOS rebuild.
myModules.noctalia.enable = lib.mkDefault true;
myModules.taffybar.enable = lib.mkDefault true;
# Needed for hyprlock authentication without PAM fallback warnings.
security.pam.services.hyprlock = {};
security.pam.services.hyprlock = { };
# DDC/CI monitor control for keyboard-driven input switching.
hardware.i2c = {
@@ -35,150 +105,106 @@ let
home-manager.sharedModules = [
inputs.hyprscratch.homeModules.default
{
services.kanshi = {
enable = true;
systemdTarget = "graphical-session.target";
settings = [
{
# USB-C connector names can move between DP-* ports across docks/reboots.
# Match the ultrawide by make/model and allow the serial field to vary.
profile.name = "ultrawide-usbc-desk";
profile.outputs = [
{
criteria = "eDP-1";
status = "enable";
mode = "2560x1600@240Hz";
position = "0,0";
scale = 1.0;
}
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "2560,0";
scale = 1.0;
}
];
}
{
# When the laptop panel is unavailable (e.g. lid-closed docked use),
# still drive the ultrawide at its full refresh rate.
profile.name = "ultrawide-only";
profile.outputs = [
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "0,0";
scale = 1.0;
}
];
}
];
};
(
{ config, lib, ... }:
let
hyprConfig =
name:
config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/dotfiles/config/hypr/${name}";
in
{
services.kanshi = {
enable = true;
systemdTarget = "graphical-session.target";
settings = [
{
# USB-C connector names can move between DP-* ports across docks/reboots.
# Match the ultrawide by make/model and allow the serial field to vary.
profile.name = "ultrawide-usbc-desk";
profile.outputs = [
{
criteria = "eDP-1";
status = "enable";
mode = "2560x1600@240Hz";
position = "0,0";
scale = 1.0;
}
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "2560,0";
scale = 1.0;
}
];
}
{
# When the laptop panel is unavailable (e.g. lid-closed docked use),
# still drive the ultrawide at its full refresh rate.
profile.name = "ultrawide-only";
profile.outputs = [
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "0,0";
scale = 1.0;
}
];
}
];
};
programs.hyprscratch = {
enable = true;
settings = {
daemon_options = "clean";
global_options = "";
global_rules = "float;size monitor_w*0.95 monitor_h*0.95;center";
programs.hyprscratch = {
enable = false;
settings = { };
};
htop = {
command = "alacritty --class htop-scratch --title htop -e htop";
class = "htop-scratch";
};
volume = {
command = "pavucontrol";
class = "org.pulseaudio.pavucontrol";
};
spotify = {
command = "spotify";
class = "spotify";
};
element = {
command = "element-desktop";
class = "Element";
};
slack = {
command = "slack";
class = "Slack";
};
transmission = {
command = "transmission-gtk";
class = "transmission-gtk";
};
dropdown = {
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown";
class = "com.mitchellh.ghostty.dropdown";
options = "persist";
rules = "float;size monitor_w monitor_h*0.5;move 0 60;noborder;noshadow;animation slide";
};
gmail = {
command = "google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox";
class = "google-chrome";
title = "Gmail";
};
messages = {
command = "google-chrome-stable --new-window https://messages.google.com/web/conversations";
class = "google-chrome";
title = "Messages";
xdg.configFile."hyprscratch/config.conf" = lib.mkIf false {
text = lib.hm.generators.toHyprconf {
attrs = hyprscratchSettings;
};
};
};
}
xdg.configFile."hypr/hyprland.lua" = {
force = true;
source = hyprConfig "hyprland.lua";
};
xdg.configFile."hypr/hypridle.conf".source = hyprConfig "hypridle.conf";
xdg.configFile."hypr/hyprlock.conf".source = hyprConfig "hyprlock.conf";
xdg.configFile."hypr/scripts".enable = false;
}
)
];
# Hyprland-specific packages
environment.systemPackages = with pkgs; [
# Hyprland utilities
hyprpaper # Wallpaper
hypridle # Idle daemon
hyprlock # Screen locker
hyprcursor # Cursor themes
wl-clipboard # Clipboard for Wayland
wtype # Wayland input typing
cliphist # Clipboard history
grim # Screenshot utility
slurp # Region selection
swappy # Screenshot annotation
nwg-displays # GUI monitor arrangement
mpv # Graphical screensaver payload
ddcutil # Monitor input switching over DDC/CI
environment.systemPackages =
with pkgs;
[
# Hyprland utilities
hyprpaper # Wallpaper
hypridle # Idle daemon
hyprlock # Screen locker
hyprcursor # Cursor themes
wl-clipboard # Clipboard for Wayland
wtype # Wayland input typing
cliphist # Clipboard history
grim # Screenshot utility
slurp # Region selection
swappy # Screenshot annotation
nwg-displays # GUI monitor arrangement
mpvpaper # Layer-shell video screensaver payload
ddcutil # Monitor input switching over DDC/CI
# For scripts
jq
] ++ lib.optionals enableExternalPluginPackages [
# External plugin packages are pinned to the stable 0.53 stack.
# PR 13817's Hyprland branch builds, but hy3 / hyprexpo do not yet build
# against it, so keep them out of the experimental Lua branch for now.
inputs.hy3.packages.${system}.hy3
hyprexpoPatched
];
# For scripts
hyprRofiWindow
hyprShellUi
jq
]
++ hyprlandPluginPackages;
};
in
enabledModule // {
options = lib.recursiveUpdate enabledModule.options {
myModules.hyprland.useLuaConfigBranch = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Use the experimental Hyprland PR 13817 Lua-config branch for the
Hyprland package itself. Third-party plugins are left on the stable
stack and are excluded from the experimental package set because current
`hy3` and `hyprexpo` sources do not build against PR 13817 yet. The
existing `hyprland.conf` remains active until a sibling `hyprland.lua`
file is added.
'';
};
};
}
enabledModule

View File

@@ -184,16 +184,11 @@
type = "Application";
categories = [ "Network" "WebBrowser" ];
mimeType = [
"application/pdf"
"application/rdf+xml"
"application/rss+xml"
"application/xhtml+xml"
"application/xhtml_xml"
"application/xml"
"image/gif"
"image/jpeg"
"image/png"
"image/webp"
"text/html"
"text/xml"
"x-scheme-handler/http"

View File

@@ -63,6 +63,7 @@
# Disable the old multi-node railbird k3s setup
myModules.railbird-k3s.enable = false;
myModules."keepbook-sync".enable = true;
myModules.remote-hyprland.enable = true;
# Mirror the old biskcomp "Syncthing hosting" pattern: serve the synced railbird tree over HTTPS with autoindex.
services.nginx.virtualHosts."syncthing.railbird.ai" = {

View File

@@ -9,6 +9,7 @@
features.full.enable = true;
myModules.kubelet.enable = false;
myModules.nvidia.enable = true;
myModules.hyprland.useLuaConfigBranch = true;
# Needed for now because monitors have different refresh rates
myModules.xmonad.picom.vSync.enable = false;
myModules.cache-server = {

View File

@@ -113,6 +113,9 @@
codex = inputs.codex-cli-nix.packages.${prev.stdenv.hostPlatform.system}.default;
claude-code = inputs.claude-code-nix.packages.${prev.stdenv.hostPlatform.system}.default;
git-sync-rs = inputs.git-sync-rs.packages.${prev.stdenv.hostPlatform.system}.default;
kef = final.callPackage ./packages/kef {};
pykefcontrol = final.python3Packages.callPackage ./packages/pykefcontrol {};
roborock-control = final.callPackage ./packages/roborock-control {};
})
]
++ (

87
nixos/noctalia.nix Normal file
View File

@@ -0,0 +1,87 @@
{
config,
inputs,
lib,
makeEnable,
pkgs,
...
}:
let
system = pkgs.stdenv.hostPlatform.system;
noctaliaPackage = inputs.noctalia.packages.${system}.default;
waitForWayland = pkgs.writeShellScript "noctalia-wait-for-wayland" ''
runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}"
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
if [ -n "''${WAYLAND_DISPLAY:-}" ] && [ -S "$runtime_dir/$WAYLAND_DISPLAY" ]; then
exit 0
fi
${pkgs.coreutils}/bin/sleep 0.1
done
echo "Wayland socket not ready: WAYLAND_DISPLAY=''${WAYLAND_DISPLAY:-<unset>} XDG_RUNTIME_DIR=$runtime_dir" >&2
exit 1
'';
in
makeEnable config "myModules.noctalia" false {
environment.systemPackages = [
noctaliaPackage
];
# Noctalia's battery widget talks to UPower. Hosts that deliberately do not
# have batteries can still override this back to false.
services.upower.enable = lib.mkDefault true;
home-manager.sharedModules = [
inputs.noctalia.homeModules.default
({ lib, ... }: {
programs.noctalia-shell = {
enable = true;
# This module provides the Hyprland-scoped service below.
systemd.enable = false;
};
home.activation.noctaliaLauncherOverviewLayer =
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
settings_file="$HOME/.config/noctalia/settings.json"
settings_tmp="$(${pkgs.coreutils}/bin/mktemp)"
${pkgs.coreutils}/bin/mkdir -p "$(${pkgs.coreutils}/bin/dirname "$settings_file")"
if [ -e "$settings_file" ] && ${lib.getExe pkgs.jq} -e . "$settings_file" >/dev/null 2>&1; then
${lib.getExe pkgs.jq} \
'.appLauncher = (.appLauncher // {}) | .appLauncher.overviewLayer = true' \
"$settings_file" > "$settings_tmp"
${pkgs.coreutils}/bin/mv "$settings_tmp" "$settings_file"
else
${pkgs.coreutils}/bin/printf '%s\n' \
'{' \
' "appLauncher": {' \
' "overviewLayer": true' \
' }' \
'}' > "$settings_file"
${pkgs.coreutils}/bin/rm -f "$settings_tmp"
fi
'';
systemd.user.services.noctalia-shell = {
Unit = {
Description = "Noctalia Shell";
Documentation = "https://docs.noctalia.dev";
PartOf = [ "hyprland-session.target" ];
After = [ "hyprland-session.target" ];
};
Service = {
ExecCondition = "/run/current-system/sw/bin/desktop_shell_ui exec-condition noctalia";
ExecStartPre = "${waitForWayland}";
ExecStart = "${lib.getExe noctaliaPackage} --no-duplicate";
Restart = "on-failure";
RestartSec = 1;
};
Install.WantedBy = [ "hyprland-session.target" ];
};
})
];
}

View File

@@ -0,0 +1,30 @@
{
lib,
python3,
python3Packages,
writeShellApplication,
}:
let
pykefcontrol = python3Packages.callPackage ../pykefcontrol {};
python = python3.withPackages (ps: [
pykefcontrol
ps.zeroconf
]);
in
writeShellApplication {
name = "kef";
runtimeInputs = [ python ];
text = ''
exec python ${./kef.py} "$@"
'';
meta = {
description = "Command-line controller for KEF W2 speakers";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ imalison ];
mainProgram = "kef";
};
}

251
nixos/packages/kef/kef.py Normal file
View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
import argparse
import json
import os
import sys
import threading
import time
import requests
from pykefcontrol.kef_connector import KefConnector
from zeroconf import IPVersion, ServiceBrowser, ServiceListener, Zeroconf
SOURCES = ("wifi", "bluetooth", "tv", "optic", "coaxial", "analog", "standby")
DISCOVERY_SERVICE_TYPES = (
"_kef-info._tcp.local.",
"_sues800device._tcp.local.",
"_http._tcp.local.",
)
class KefDiscoveryListener(ServiceListener):
def __init__(self):
self._lock = threading.Lock()
self.speakers = {}
def add_service(self, zeroconf, service_type, name):
self._record_service(zeroconf, service_type, name)
def update_service(self, zeroconf, service_type, name):
self._record_service(zeroconf, service_type, name)
def remove_service(self, zeroconf, service_type, name):
return None
def _record_service(self, zeroconf, service_type, name):
info = zeroconf.get_service_info(service_type, name, timeout=1000)
if info is None:
return
properties = {
key.decode("utf-8", errors="replace"): (
value.decode("utf-8", errors="replace") if value is not None else ""
)
for key, value in info.properties.items()
}
addresses = [
address
for address in info.parsed_addresses(IPVersion.V4Only)
if not address.startswith("127.")
]
if not addresses:
return
is_kef = (
service_type in ("_kef-info._tcp.local.", "_sues800device._tcp.local.")
or properties.get("manufacturer", "").lower() == "kef"
or "kef" in name.lower()
or "ls50" in name.lower()
)
if not is_kef:
return
key = addresses[0]
with self._lock:
existing = self.speakers.get(key, {})
self.speakers[key] = {
**existing,
"address": addresses[0],
"hostname": (info.server.rstrip(".") if info.server else None)
or existing.get("hostname"),
"name": properties.get("name")
or properties.get("fn")
or existing.get("name")
or strip_service_name(name),
"model": properties.get("modelName")
or properties.get("model")
or properties.get("mn")
or existing.get("model"),
"version": properties.get("version") or existing.get("version"),
"service": existing.get("service") or service_type.removesuffix(".local."),
}
def strip_service_name(name):
for suffix in DISCOVERY_SERVICE_TYPES:
if name.endswith("." + suffix):
return name[: -(len(suffix) + 1)]
return name.rstrip(".")
def discover_speakers(timeout):
listener = KefDiscoveryListener()
zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
try:
browsers = [
ServiceBrowser(zeroconf, service_type, listener)
for service_type in DISCOVERY_SERVICE_TYPES
]
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if listener.speakers:
time.sleep(0.25)
break
time.sleep(0.05)
return sorted(
listener.speakers.values(),
key=lambda speaker: (
speaker.get("name") or "",
speaker.get("address") or "",
),
)
finally:
for browser in locals().get("browsers", []):
browser.cancel()
zeroconf.close()
def discover_host(timeout):
speakers = discover_speakers(timeout)
if not speakers:
raise SystemExit(
"Could not discover a KEF speaker. Pass --host <speaker-ip> or set KEF_HOST."
)
if len(speakers) > 1:
choices = ", ".join(
f"{speaker.get('name') or 'KEF'} at {speaker['address']}" for speaker in speakers
)
raise SystemExit(f"Multiple KEF speakers discovered: {choices}. Pass --host explicitly.")
return speakers[0]["address"]
def connector(args):
host = (
args.host
or os.environ.get("KEF_HOST")
or os.environ.get("KEF_IP")
or discover_host(args.discovery_timeout)
)
return KefConnector(host, model=args.model)
def print_status(speaker):
info = {
"name": speaker.speaker_name,
"model": speaker.speaker_model,
"firmware": speaker.firmware_version,
"status": speaker.status,
"source": speaker.source,
"volume": speaker.volume,
"playing": speaker.is_playing,
}
try:
song = speaker.get_song_information()
if any(song.values()):
info["song"] = song
except (KeyError, IndexError, TypeError, requests.RequestException):
pass
print(json.dumps(info, indent=2, sort_keys=True))
def bounded_volume(value):
return max(0, min(100, value))
def main():
parser = argparse.ArgumentParser(prog="kef", description="Control KEF W2 speakers.")
parser.add_argument(
"--host",
help="Speaker IP address. Defaults to KEF_HOST/KEF_IP, then mDNS discovery.",
)
parser.add_argument(
"--model",
default="LS50W2",
help="Speaker model passed to pykefcontrol. Default: LS50W2.",
)
parser.add_argument(
"--discovery-timeout",
default=2.0,
type=float,
help="Seconds to wait for mDNS discovery when --host/KEF_HOST is unset.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("discover", help="List discovered KEF speakers as JSON.")
subparsers.add_parser("status", help="Print speaker status as JSON.")
subparsers.add_parser("on", help="Power on.")
subparsers.add_parser("standby", help="Put the speaker in standby.")
subparsers.add_parser("mute", help="Mute by setting volume to 0.")
volume = subparsers.add_parser("volume", help="Get or set volume.")
volume.add_argument("level", nargs="?", type=int, help="Volume level, 0-100.")
up = subparsers.add_parser("up", help="Increase volume.")
up.add_argument("step", nargs="?", default=3, type=int)
down = subparsers.add_parser("down", help="Decrease volume.")
down.add_argument("step", nargs="?", default=3, type=int)
source = subparsers.add_parser("source", help="Get or set source.")
source.add_argument("name", nargs="?", choices=SOURCES)
subparsers.add_parser("play-pause", help="Toggle play/pause.")
subparsers.add_parser("next", help="Next track.")
subparsers.add_parser("previous", help="Previous track.")
args = parser.parse_args()
if args.command == "discover":
print(json.dumps(discover_speakers(args.discovery_timeout), indent=2, sort_keys=True))
return 0
speaker = connector(args)
try:
if args.command == "status":
print_status(speaker)
elif args.command == "on":
speaker.power_on()
elif args.command == "standby":
speaker.shutdown()
elif args.command == "mute":
speaker.volume = 0
elif args.command == "volume":
if args.level is None:
print(speaker.volume)
else:
speaker.volume = bounded_volume(args.level)
elif args.command == "up":
speaker.volume = bounded_volume(speaker.volume + args.step)
elif args.command == "down":
speaker.volume = bounded_volume(speaker.volume - args.step)
elif args.command == "source":
if args.name is None:
print(speaker.source)
else:
speaker.source = args.name
elif args.command == "play-pause":
speaker.toggle_play_pause()
elif args.command == "next":
speaker.next_track()
elif args.command == "previous":
speaker.previous_track()
except requests.RequestException as error:
print(f"kef: failed to reach speaker: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,37 @@
{
lib,
buildPythonPackage,
fetchPypi,
hatchling,
aiohttp,
requests,
}:
buildPythonPackage rec {
pname = "pykefcontrol";
version = "0.9.2";
pyproject = true;
src = fetchPypi {
inherit pname version;
hash = "sha256-3kGhN+E7driiE6ePyF0EZOEnUhTm07sxHCKdzrn/MxM=";
};
build-system = [
hatchling
];
dependencies = [
aiohttp
requests
];
pythonImportsCheck = [ "pykefcontrol" ];
meta = {
description = "Python library for controlling KEF LS50WII, LSX II, and LS60 speakers";
homepage = "https://github.com/N0ciple/pykefcontrol";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ imalison ];
};
}

View File

@@ -0,0 +1,30 @@
{
lib,
python3,
writeShellApplication,
}:
let
python = python3.withPackages (ps: [
ps.python-roborock
ps.pyshark
ps.pyyaml
]);
in
writeShellApplication {
name = "roborock-control";
runtimeInputs = [ python ];
text = ''
export ROBOROCK_CONTROL_RUNNER=direct
exec python ${../../../dotfiles/lib/bin/roborock-control} "$@"
'';
meta = {
description = "Command-line controller for Roborock vacuums";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ imalison ];
mainProgram = "roborock-control";
};
}

View File

@@ -1,228 +0,0 @@
From aaefc0ff0bc4348de04f311ad0101da44c62ae94 Mon Sep 17 00:00:00 2001
From: Ivan Malison <IvanMalison@gmail.com>
Date: Wed, 4 Feb 2026 00:54:52 -0800
Subject: [PATCH 1/2] hyprexpo: optionally render workspace numbers
---
hyprexpo/README.md | 3 +-
hyprexpo/main.cpp | 2 +
hyprexpo/overview.cpp | 109 ++++++++++++++++++++++++++++++++++++++++++
hyprexpo/overview.hpp | 4 ++
4 files changed, 117 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 97bd1d4..aac2e97 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,8 @@ gap_size | number | gap between desktops | `5`
bg_col | color | color in gaps (between desktops) | `rgb(000000)`
workspace_method | [center/first] [workspace] | position of the desktops | `center current`
skip_empty | boolean | whether the grid displays workspaces sequentially by id using selector "r" (`false`) or skips empty workspaces using selector "m" (`true`) | `false`
+show_workspace_numbers | boolean | show numeric labels for workspaces | `false`
+workspace_number_color | color | color of workspace number labels | `rgb(ffffff)`
gesture_distance | number | how far is the max for the gesture | `300`
### Keywords
@@ -57,4 +59,3 @@ off | hides the overview
disable | same as `off`
on | displays the overview
enable | same as `on`
-
diff --git a/main.cpp b/main.cpp
index 883fd82..ff9f380 100644
--- a/main.cpp
+++ b/main.cpp
@@ -239,6 +239,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:bg_col", Hyprlang::INT{0xFF111111});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method", Hyprlang::STRING{"center current"});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty", Hyprlang::INT{0});
+ HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_numbers", Hyprlang::INT{0});
+ HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_number_color", Hyprlang::INT{0xFFFFFFFF});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:gesture_distance", Hyprlang::INT{200});
diff --git a/overview.cpp b/overview.cpp
index 5721948..926a9f8 100644
--- a/overview.cpp
+++ b/overview.cpp
@@ -1,5 +1,8 @@
#include "overview.hpp"
#include <any>
+#include <algorithm>
+#include <cmath>
+#include <pango/pangocairo.h>
#define private public
#include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/Compositor.hpp>
@@ -15,6 +18,86 @@
#undef private
#include "OverviewPassElement.hpp"
+static Vector2D renderLabelTexture(SP<CTexture> out, const std::string& text, const CHyprColor& color, int fontSizePx) {
+ if (!out || text.empty() || fontSizePx <= 0)
+ return {};
+
+ auto measureSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1);
+ auto measureCairo = cairo_create(measureSurface);
+
+ PangoLayout* measureLayout = pango_cairo_create_layout(measureCairo);
+ pango_layout_set_text(measureLayout, text.c_str(), -1);
+ auto* fontDesc = pango_font_description_from_string("Sans Bold");
+ pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE);
+ pango_layout_set_font_description(measureLayout, fontDesc);
+ pango_font_description_free(fontDesc);
+
+ PangoRectangle inkRect, logicalRect;
+ pango_layout_get_extents(measureLayout, &inkRect, &logicalRect);
+
+ const int textW = std::max(1, (int)std::ceil(logicalRect.width / (double)PANGO_SCALE));
+ const int textH = std::max(1, (int)std::ceil(logicalRect.height / (double)PANGO_SCALE));
+
+ g_object_unref(measureLayout);
+ cairo_destroy(measureCairo);
+ cairo_surface_destroy(measureSurface);
+
+ const int pad = std::max(4, (int)std::round(fontSizePx * 0.35));
+ const int width = textW + pad * 2;
+ const int height = textH + pad * 2;
+
+ auto surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ auto cairo = cairo_create(surface);
+
+ // Clear the pixmap
+ cairo_save(cairo);
+ cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cairo);
+ cairo_restore(cairo);
+
+ // Background for legibility
+ cairo_set_source_rgba(cairo, 0.0, 0.0, 0.0, 0.55);
+ cairo_rectangle(cairo, 0, 0, width, height);
+ cairo_fill(cairo);
+
+ PangoLayout* layout = pango_cairo_create_layout(cairo);
+ pango_layout_set_text(layout, text.c_str(), -1);
+ fontDesc = pango_font_description_from_string("Sans Bold");
+ pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE);
+ pango_layout_set_font_description(layout, fontDesc);
+ pango_font_description_free(fontDesc);
+
+ pango_layout_get_extents(layout, &inkRect, &logicalRect);
+ const double xOffset = (width - logicalRect.width / (double)PANGO_SCALE) / 2.0;
+ const double yOffset = (height - logicalRect.height / (double)PANGO_SCALE) / 2.0;
+
+ cairo_set_source_rgba(cairo, color.r, color.g, color.b, color.a);
+ cairo_move_to(cairo, xOffset, yOffset);
+ pango_cairo_show_layout(cairo, layout);
+
+ g_object_unref(layout);
+
+ cairo_surface_flush(surface);
+
+ const auto DATA = cairo_image_surface_get_data(surface);
+ out->allocate();
+ glBindTexture(GL_TEXTURE_2D, out->m_texID);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+#ifndef GLES2
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
+#endif
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
+
+ cairo_destroy(cairo);
+ cairo_surface_destroy(surface);
+
+ return {width, height};
+}
+
static void damageMonitor(WP<Hyprutils::Animation::CBaseAnimatedVariable> thisptr) {
g_pOverview->damage();
}
@@ -34,11 +117,14 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
static auto* const* PGAPS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:gap_size")->getDataStaticPtr();
static auto* const* PCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:bg_col")->getDataStaticPtr();
static auto* const* PSKIP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty")->getDataStaticPtr();
+ static auto* const* PSHOWNUM = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_numbers")->getDataStaticPtr();
+ static auto* const* PNUMCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_number_color")->getDataStaticPtr();
static auto const* PMETHOD = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method")->getDataStaticPtr();
SIDE_LENGTH = **PCOLUMNS;
GAP_WIDTH = **PGAPS;
BG_COLOR = **PCOL;
+ showWorkspaceNumbers = **PSHOWNUM;
// process the method
bool methodCenter = true;
@@ -126,6 +212,17 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
Vector2D tileRenderSize = (pMonitor->m_size - Vector2D{GAP_WIDTH * pMonitor->m_scale, GAP_WIDTH * pMonitor->m_scale} * (SIDE_LENGTH - 1)) / SIDE_LENGTH;
CBox monbox{0, 0, tileSize.x * 2, tileSize.y * 2};
+ if (showWorkspaceNumbers) {
+ const CHyprColor numberColor = **PNUMCOL;
+ const int fontSizePx = std::max(12, (int)std::round(tileRenderSize.y * pMonitor->m_scale * 0.22));
+ for (auto& image : images) {
+ if (image.workspaceID == WORKSPACE_INVALID)
+ continue;
+ image.labelTex = makeShared<CTexture>();
+ image.labelSizePx = renderLabelTexture(image.labelTex, std::to_string(image.workspaceID), numberColor, fontSizePx);
+ }
+ }
+
if (!ENABLE_LOWRES)
monbox = {{0, 0}, pMonitor->m_pixelSize};
@@ -452,6 +549,18 @@ void COverview::fullRender() {
texbox.round();
CRegion damage{0, 0, INT16_MAX, INT16_MAX};
g_pHyprOpenGL->renderTextureInternal(images[x + y * SIDE_LENGTH].fb.getTexture(), texbox, {.damage = &damage, .a = 1.0});
+
+ if (showWorkspaceNumbers) {
+ auto& image = images[x + y * SIDE_LENGTH];
+ if (image.workspaceID != WORKSPACE_INVALID && image.labelTex && image.labelTex->m_texID != 0 && image.labelSizePx.x > 0 && image.labelSizePx.y > 0) {
+ const Vector2D labelSize = image.labelSizePx / pMonitor->m_scale;
+ const float margin = std::max(4.0, tileRenderSize.y * 0.05);
+ CBox labelBox = {x * tileRenderSize.x + x * GAPSIZE + margin, y * tileRenderSize.y + y * GAPSIZE + margin, labelSize.x, labelSize.y};
+ labelBox.scale(pMonitor->m_scale).translate(pos->value());
+ labelBox.round();
+ g_pHyprOpenGL->renderTexture(image.labelTex, labelBox, {.a = 1.0});
+ }
+ }
}
}
}
diff --git a/overview.hpp b/overview.hpp
index 4b02400..1f6bf3c 100644
--- a/overview.hpp
+++ b/overview.hpp
@@ -8,6 +8,7 @@
#include <hyprland/src/helpers/AnimatedVariable.hpp>
#include <hyprland/src/managers/HookSystemManager.hpp>
#include <vector>
+class CTexture;
// saves on resources, but is a bit broken rn with blur.
// hyprland's fault, but cba to fix.
@@ -58,6 +59,8 @@ class COverview {
int64_t workspaceID = -1;
PHLWORKSPACE pWorkspace;
CBox box;
+ SP<CTexture> labelTex;
+ Vector2D labelSizePx;
};
Vector2D lastMousePosLocal = Vector2D{};
@@ -81,6 +84,7 @@ class COverview {
bool swipe = false;
bool swipeWasCommenced = false;
+ bool showWorkspaceNumbers = false;
friend class COverviewPassElement;
};
--
2.52.0

View File

@@ -1,147 +0,0 @@
From edc05ce88f79ceda0cdcb9aa68ec371b1af323de Mon Sep 17 00:00:00 2001
From: Ivan Malison <IvanMalison@gmail.com>
Date: Mon, 16 Feb 2026 21:50:16 -0800
Subject: [PATCH 2/2] hyprexpo: add bring selection mode
---
hyprexpo/README.md | 1 +
hyprexpo/main.cpp | 50 +++++++++++++++++++++++++++++++++++++++++++
hyprexpo/overview.cpp | 12 +++++++++--
hyprexpo/overview.hpp | 3 ++-
4 files changed, 63 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index aac2e97..084f02b 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ Here are a list of options you can use:
| --- | --- |
toggle | displays if hidden, hide if displayed
select | selects the hovered desktop
+bring | brings a window from the hovered desktop to the current desktop
off | hides the overview
disable | same as `off`
on | displays the overview
diff --git a/main.cpp b/main.cpp
index ff9f380..78bac24 100644
--- a/main.cpp
+++ b/main.cpp
@@ -65,6 +65,47 @@ static void hkAddDamageB(void* thisptr, const pixman_region32_t* rg) {
g_pOverview->onDamageReported();
}
+static PHLWINDOW windowToBringFromWorkspace(const PHLWORKSPACE& workspace) {
+ if (!workspace)
+ return nullptr;
+
+ for (auto it = g_pCompositor->m_windows.rbegin(); it != g_pCompositor->m_windows.rend(); ++it) {
+ const auto& w = *it;
+ if (!w || w->m_workspace != workspace || !w->m_isMapped || w->isHidden())
+ continue;
+
+ return w;
+ }
+
+ return nullptr;
+}
+
+static SDispatchResult bringWindowFromWorkspace(int64_t sourceWorkspaceID) {
+ if (sourceWorkspaceID == WORKSPACE_INVALID)
+ return {.success = false, .error = "selected workspace is empty"};
+
+ const auto FOCUSSTATE = Desktop::focusState();
+ const auto MONITOR = FOCUSSTATE->monitor();
+ if (!MONITOR || !MONITOR->m_activeWorkspace)
+ return {.success = false, .error = "no active monitor/workspace"};
+
+ if (sourceWorkspaceID == MONITOR->activeWorkspaceID())
+ return {};
+
+ const auto SOURCEWORKSPACE = g_pCompositor->getWorkspaceByID(sourceWorkspaceID);
+ if (!SOURCEWORKSPACE)
+ return {.success = false, .error = "selected workspace is not open"};
+
+ const auto WINDOW = windowToBringFromWorkspace(SOURCEWORKSPACE);
+ if (!WINDOW)
+ return {.success = false, .error = "selected workspace has no mapped windows"};
+
+ g_pCompositor->moveWindowToWorkspaceSafe(WINDOW, MONITOR->m_activeWorkspace);
+ FOCUSSTATE->fullWindowFocus(WINDOW);
+ g_pCompositor->warpCursorTo(WINDOW->middle());
+ return {};
+}
+
static SDispatchResult onExpoDispatcher(std::string arg) {
if (g_pOverview && g_pOverview->m_isSwiping)
@@ -77,6 +118,15 @@ static SDispatchResult onExpoDispatcher(std::string arg) {
}
return {};
}
+ if (arg == "bring") {
+ if (g_pOverview) {
+ g_pOverview->selectHoveredWorkspace();
+ const auto BRINGRESULT = bringWindowFromWorkspace(g_pOverview->selectedWorkspaceID());
+ g_pOverview->close(false);
+ return BRINGRESULT;
+ }
+ return {};
+ }
if (arg == "toggle") {
if (g_pOverview)
g_pOverview->close();
diff --git a/overview.cpp b/overview.cpp
index 926a9f8..45ee982 100644
--- a/overview.cpp
+++ b/overview.cpp
@@ -343,6 +343,14 @@ void COverview::selectHoveredWorkspace() {
closeOnID = x + y * SIDE_LENGTH;
}
+int64_t COverview::selectedWorkspaceID() const {
+ const int ID = closeOnID == -1 ? openedID : closeOnID;
+ if (ID < 0 || ID >= (int)images.size())
+ return WORKSPACE_INVALID;
+
+ return images[ID].workspaceID;
+}
+
void COverview::redrawID(int id, bool forcelowres) {
if (!pMonitor)
return;
@@ -451,7 +459,7 @@ void COverview::onDamageReported() {
g_pCompositor->scheduleFrameForMonitor(pMonitor.lock());
}
-void COverview::close() {
+void COverview::close(bool switchToSelection) {
if (closing)
return;
@@ -471,7 +479,7 @@ void COverview::close() {
redrawAll();
- if (TILE.workspaceID != pMonitor->activeWorkspaceID()) {
+ if (switchToSelection && TILE.workspaceID != pMonitor->activeWorkspaceID()) {
pMonitor->setSpecialWorkspace(0);
// If this tile's workspace was WORKSPACE_INVALID, move to the next
diff --git a/overview.hpp b/overview.hpp
index 1f6bf3c..ca59f32 100644
--- a/overview.hpp
+++ b/overview.hpp
@@ -33,8 +33,9 @@ class COverview {
void onSwipeEnd();
// close without a selection
- void close();
+ void close(bool switchToSelection = true);
void selectHoveredWorkspace();
+ int64_t selectedWorkspaceID() const;
bool blockOverviewRendering = false;
bool blockDamageReporting = false;
--
2.52.0

200
nixos/remote-hyprland.nix Normal file
View File

@@ -0,0 +1,200 @@
{ config, lib, pkgs, makeEnable, ... }:
let
cfg = config.myModules.remote-hyprland;
hyprlandPackage = config.programs.hyprland.package;
geometry = "${toString cfg.width}x${toString cfg.height}@${toString cfg.refreshRate}";
monitorRule = "${cfg.output},${geometry},0x0,${toString cfg.scale}";
remoteHyprlandStartVnc = pkgs.writeShellScript "remote-hyprland-start-vnc" ''
set -euo pipefail
export WAYLAND_DISPLAY=${cfg.socket}
export XDG_CURRENT_DESKTOP=Hyprland
export XDG_SESSION_DESKTOP=Hyprland
export XDG_SESSION_TYPE=wayland
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
if ${hyprlandPackage}/bin/hyprctl -j monitors >/dev/null 2>&1; then
break
fi
${pkgs.coreutils}/bin/sleep 0.1
done
# Give wayvnc a stable output name instead of relying on Hyprland's
# fallback HEADLESS-* naming.
${hyprlandPackage}/bin/hyprctl output create headless ${cfg.output} >/dev/null 2>&1 || true
${hyprlandPackage}/bin/hyprctl keyword monitor '${monitorRule}' >/dev/null 2>&1 || true
exec ${pkgs.wayvnc}/bin/wayvnc \
--log-level=info \
--output ${cfg.output} \
${cfg.bindAddress} ${toString cfg.port}
'';
remoteHyprlandConfig = pkgs.writeText "remote-hyprland.conf" ''
monitor=${monitorRule}
monitor=,${geometry},0x0,${toString cfg.scale}
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_DESKTOP,Hyprland
env = XDG_SESSION_TYPE,wayland
input {
kb_layout = us
follow_mouse = 1
}
general {
gaps_in = 4
gaps_out = 8
border_size = 2
layout = dwindle
}
decoration {
rounding = 4
}
dwindle {
pseudotile = true
preserve_split = true
}
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
force_default_wallpaper = 0
}
$mainMod = SUPER
bind = $mainMod, Return, exec, ${cfg.terminalCommand}
bind = $mainMod, D, exec, ${pkgs.rofi}/bin/rofi -show drun
bind = $mainMod, Q, killactive
bind = $mainMod SHIFT, M, exit
bind = $mainMod, H, movefocus, l
bind = $mainMod, J, movefocus, d
bind = $mainMod, K, movefocus, u
bind = $mainMod, L, movefocus, r
bind = $mainMod SHIFT, H, movewindow, l
bind = $mainMod SHIFT, J, movewindow, d
bind = $mainMod SHIFT, K, movewindow, u
bind = $mainMod SHIFT, L, movewindow, r
bind = $mainMod, 1, workspace, 1
bind = $mainMod, 2, workspace, 2
bind = $mainMod, 3, workspace, 3
bind = $mainMod, 4, workspace, 4
bind = $mainMod, 5, workspace, 5
bind = $mainMod, 6, workspace, 6
bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
exec-once = ${remoteHyprlandStartVnc}
exec-once = ${cfg.terminalCommand}
'';
enabledModule = makeEnable config "myModules.remote-hyprland" false {
myModules.hyprland.enable = true;
users.manageLingering = true;
users.users.${cfg.user}.linger = true;
environment.systemPackages = [ pkgs.wayvnc ];
home-manager.users.${cfg.user}.systemd.user.services.remote-hyprland = {
Unit = {
Description = "Headless Hyprland session for remote VNC access";
After = [ "default.target" ];
};
Service = {
ExecStart = "${hyprlandPackage}/bin/Hyprland --socket ${cfg.socket} --config ${remoteHyprlandConfig}";
Restart = "on-failure";
RestartSec = 5;
Environment = [
"XDG_CURRENT_DESKTOP=Hyprland"
"XDG_SESSION_DESKTOP=Hyprland"
"XDG_SESSION_TYPE=wayland"
"WAYLAND_DISPLAY=${cfg.socket}"
];
};
Install = {
WantedBy = [ "default.target" ];
};
};
};
in
enabledModule // {
options = lib.recursiveUpdate enabledModule.options {
myModules.remote-hyprland = {
user = lib.mkOption {
type = lib.types.str;
default = "imalison";
description = "User account that owns the remote Hyprland session.";
};
bindAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address for wayvnc to bind. Keep localhost when using SSH or Tailscale forwarding.";
};
port = lib.mkOption {
type = lib.types.port;
default = 5900;
description = "TCP port for wayvnc.";
};
socket = lib.mkOption {
type = lib.types.str;
default = "wayland-remote-hyprland";
description = "Wayland socket name used by the remote Hyprland instance.";
};
output = lib.mkOption {
type = lib.types.str;
default = "remote";
description = "Stable Hyprland headless output name captured by wayvnc.";
};
width = lib.mkOption {
type = lib.types.ints.positive;
default = 1920;
description = "Remote output width.";
};
height = lib.mkOption {
type = lib.types.ints.positive;
default = 1080;
description = "Remote output height.";
};
refreshRate = lib.mkOption {
type = lib.types.ints.positive;
default = 60;
description = "Remote output refresh rate.";
};
scale = lib.mkOption {
type = lib.types.number;
default = 1;
description = "Remote output scale.";
};
terminalCommand = lib.mkOption {
type = lib.types.str;
default = "${pkgs.ghostty}/bin/ghostty --gtk-single-instance=false";
description = "Command launched for the default terminal binding and initial window.";
};
};
};
}

View File

@@ -2,47 +2,49 @@
makeEnable,
config,
...
}: let
}:
let
shared = import ../nix-shared/syncthing.nix;
inherit (shared) devices allDevices;
in
makeEnable config "myModules.syncthing" true {
system.activationScripts.syncthingPermissions = {
text = ''
chown -R syncthing:syncthing /var/lib/syncthing
chmod -R 2770 /var/lib/syncthing
mkdir -p /var/lib/syncthing/sync
mkdir -p /var/lib/syncthing/railbird
'';
makeEnable config "myModules.syncthing" true {
system.activationScripts.syncthingPermissions = {
text = ''
mkdir -p /var/lib/syncthing/sync
mkdir -p /var/lib/syncthing/sync/Screensaver/use
mkdir -p /var/lib/syncthing/railbird
chown -R syncthing:syncthing /var/lib/syncthing
chmod -R 2770 /var/lib/syncthing
'';
};
systemd.services.syncthing = {
serviceConfig = {
AmbientCapabilities = "CAP_CHOWN";
CapabilityBoundingSet = "CAP_CHOWN";
};
systemd.services.syncthing = {
serviceConfig = {
AmbientCapabilities = "CAP_CHOWN";
CapabilityBoundingSet = "CAP_CHOWN";
};
};
services.syncthing = {
enable = true;
settings = {
inherit devices;
folders = {
sync = {
path = "~/sync";
devices = allDevices;
ignorePerms = true;
copyOwnershipFromParent = true;
};
railbird = {
path = "~/railbird";
devices = allDevices;
ignorePerms = true;
copyOwnershipFromParent = true;
};
};
services.syncthing = {
enable = true;
settings = {
inherit devices;
folders = {
sync = {
path = "~/sync";
devices = allDevices;
ignorePerms = true;
copyOwnershipFromParent = true;
};
options = {
relaysEnabled = true;
localAnnounceEnabled = true;
railbird = {
path = "~/railbird";
devices = allDevices;
ignorePerms = true;
copyOwnershipFromParent = true;
};
};
options = {
relaysEnabled = true;
localAnnounceEnabled = true;
};
};
}
};
}

View File

@@ -56,41 +56,29 @@ let
exec ${taffybarPackage}/bin/taffybar "$@"
'';
skipTaffybarInKde = pkgs.writeShellScript "skip-taffybar-in-kde" ''
skipTaffybarInOtherShells = pkgs.writeShellScript "skip-taffybar-in-other-shells" ''
current_desktop="''${XDG_CURRENT_DESKTOP:-}"
desktop_session="''${DESKTOP_SESSION:-}"
case "''${current_desktop}:''${desktop_session}" in
*KDE*|*kde*|*Plasma*|*plasma*) exit 1 ;;
*) exit 0 ;;
esac
exit 0
'';
taffybarExecCondition = pkgs.writeShellScript "taffybar-exec-condition" ''
${skipTaffybarInOtherShells} || exit 1
if [ -x /run/current-system/sw/bin/desktop_shell_ui ]; then
exec /run/current-system/sw/bin/desktop_shell_ui exec-condition taffybar
fi
exit 0
'';
in
makeEnable config "myModules.taffybar" false {
myModules.sni.enable = true;
nixpkgs.overlays = with inputs; (
if builtins.isList taffybar.overlays
then taffybar.overlays
else builtins.attrValues taffybar.overlays
) ++ [
# status-notifier-item's test suite spawns `dbus-daemon`; ensure it's on PATH.
(final: prev: {
haskellPackages = prev.haskellPackages.override (old: {
overrides =
final.lib.composeExtensions (old.overrides or (_: _: {}))
(hself: hsuper: {
status-notifier-item = hsuper.status-notifier-item.overrideAttrs (oldAttrs: {
checkInputs = (oldAttrs.checkInputs or []) ++ [ prev.dbus ];
# The test suite assumes a system-wide /etc/dbus-1/session.conf,
# which isn't present in Nix sandboxes.
doCheck = false;
});
});
});
})
];
environment.systemPackages = [
taffybarPackage
];
@@ -121,7 +109,7 @@ makeEnable config "myModules.taffybar" false {
rmdir --ignore-fail-on-non-empty "$HOME/.config/systemd/user/taffybar.service.d" 2>/dev/null || true
'';
systemd.user.services.taffybar.Service = {
ExecCondition = "${skipTaffybarInKde}";
ExecCondition = "${taffybarExecCondition}";
ExecStart = lib.mkForce "${taffybarStart}";
# Temporary startup debugging: keep a plain-text log outside journald so
# the next login/startup leaves easy-to-inspect tray traces behind.

View File

@@ -1,6 +1,6 @@
{ config, pkgs, inputs, makeEnable, ... }:
{ config, pkgs, inputs, lib, makeEnable, ... }:
makeEnable config "myModules.xmonad" true {
myModules.taffybar.enable = true;
myModules.taffybar.enable = lib.mkDefault (config.myModules.desktop.shellUi == "taffybar");
nixpkgs.overlays = with inputs; [
xmonad.overlay