Compare commits
38 Commits
hyprland-l
...
f6026b5cac
| Author | SHA1 | Date | |
|---|---|---|---|
| f6026b5cac | |||
| 34906469b9 | |||
| 3cb0301f9a | |||
| 6f489d14ab | |||
| acae19d9c5 | |||
| c30a67facf | |||
| d48edc9bb8 | |||
| af570360d3 | |||
| 34fd60e8f2 | |||
| f826c6ae75 | |||
| 1a2b75adcb | |||
| 4e52e81a50 | |||
| df0b7b6db4 | |||
| a7769545f1 | |||
| bb32668387 | |||
| 8ccf5fb7de | |||
| 52861430da | |||
| d9ebb812c5 | |||
| 5cf2eda008 | |||
| 6299ad2c7d | |||
| 672cc14713 | |||
| 64c45e1060 | |||
| a5413331d9 | |||
| 1044565bf7 | |||
| d684f6fbc5 | |||
| 71deb64ed0 | |||
| bb909849bd | |||
| a37e83fb23 | |||
| 53d8a69a31 | |||
| 87fd1681e2 | |||
| 8933f8e545 | |||
| ed90130233 | |||
| aa1fbf9699 | |||
| 3e05939ce3 | |||
| 8e2128b8d4 | |||
| 1696845579 | |||
| 5522b8bacd | |||
| 9c9af9f856 |
218
docs/hyprland-lua-migration-checklist.md
Normal file
218
docs/hyprland-lua-migration-checklist.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
2
dotfiles/codex/.gitignore
vendored
2
dotfiles/codex/.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
!AGENTS.md
|
||||
!config.toml
|
||||
!skills
|
||||
|
||||
# Generated/local Codex state, including config.local.toml, stays ignored.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
1918
dotfiles/config/hypr/hyprland.lua
Normal file
1918
dotfiles/config/hypr/hyprland.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
23
dotfiles/config/hypr/workspace-history-plugin/CMakeLists.txt
Normal file
23
dotfiles/config/hypr/workspace-history-plugin/CMakeLists.txt
Normal 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)
|
||||
406
dotfiles/config/hypr/workspace-history-plugin/src/main.cpp
Normal file
406
dotfiles/config/hypr/workspace-history-plugin/src/main.cpp
Normal 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();
|
||||
}
|
||||
10
dotfiles/config/taffybar/flake.lock
generated
10
dotfiles/config/taffybar/flake.lock
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 ];
|
||||
});
|
||||
|
||||
8
dotfiles/config/taffybar/icons/claude-symbol.svg
Normal file
8
dotfiles/config/taffybar/icons/claude-symbol.svg
Normal 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 |
5
dotfiles/config/taffybar/icons/openai-symbol.svg
Normal file
5
dotfiles/config/taffybar/icons/openai-symbol.svg
Normal 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 |
Submodule dotfiles/config/taffybar/taffybar updated: 59e3c75990...23dbc827ad
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)'")
|
||||
|
||||
|
||||
93
dotfiles/lib/bin/desktop_shell_ui
Executable file
93
dotfiles/lib/bin/desktop_shell_ui
Executable 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
|
||||
@@ -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
254
dotfiles/lib/bin/hypr_rofi_window
Executable 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
78
dotfiles/lib/bin/hypr_shell_ui
Executable 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
275
dotfiles/lib/bin/roborock-control
Executable 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()
|
||||
@@ -69,6 +69,7 @@
|
||||
multiplexerAliases = import ../../shared/multiplexer-aliases.nix;
|
||||
|
||||
excludedTopLevelEntries = [
|
||||
"codex"
|
||||
"config"
|
||||
];
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
|
||||
}
|
||||
|
||||
@@ -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
702
nixos/flake.lock
generated
File diff suppressed because it is too large
Load Diff
213
nixos/flake.nix
213
nixos/flake.nix
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
87
nixos/noctalia.nix
Normal 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" ];
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
30
nixos/packages/kef/default.nix
Normal file
30
nixos/packages/kef/default.nix
Normal 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
251
nixos/packages/kef/kef.py
Normal 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())
|
||||
37
nixos/packages/pykefcontrol/default.nix
Normal file
37
nixos/packages/pykefcontrol/default.nix
Normal 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 ];
|
||||
};
|
||||
}
|
||||
30
nixos/packages/roborock-control/default.nix
Normal file
30
nixos/packages/roborock-control/default.nix
Normal 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";
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
200
nixos/remote-hyprland.nix
Normal 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.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user