319 Commits

Author SHA1 Message Date
d3c49ce7ed Update hyprexpo preview layout preservation fix 2026-05-19 14:27:18 -07:00
ddde85ab3f Update hyprexpo preview activation fix 2026-05-19 10:03:06 -07:00
9c1d280c92 Update hyprexpo preview refresh fix 2026-05-19 05:19:16 -07:00
937b49c11a Point hyprexpo at refreshed main 2026-05-19 04:23:09 -07:00
3269e803fd Point hyprexpo at window drag branch 2026-05-19 03:33:41 -07:00
9eef758ba2 Update Codex flakes 2026-05-18 18:53:17 -07:00
6b76df2f27 Route Codex scratchpads through launcher helper 2026-05-17 20:55:23 -07:00
1642626d2a Add Hyprland inactive opacity toggle 2026-05-17 14:53:07 -07:00
0a118d0673 Lower inactive window opacity 2026-05-17 14:15:21 -07:00
423d5cd35b Refine rofi styling 2026-05-17 09:39:26 -07:00
5fddfb64d9 Show windows beneath rofi blur 2026-05-17 09:12:12 -07:00
3371aa6781 style rofi as glass launcher 2026-05-17 09:05:59 -07:00
911f02bb55 nixos: update hyprutils PR cleanup 2026-05-17 02:11:58 -07:00
39f2f4037d nixos: update hyprutils spring timing fix 2026-05-17 02:07:08 -07:00
cb35d31104 hyprland: retune spring animations 2026-05-17 01:52:04 -07:00
94826c2275 flake: update Hyprland inputs 2026-05-16 23:20:35 -07:00
ca6ae7c34f Fix Hyprland login startup delays 2026-05-16 20:11:25 -07:00
dbc7ec267c flake: update codex desktop linux input 2026-05-16 12:34:06 -07:00
199a2e1aab wallpapers: add crop generation helper 2026-05-16 12:33:07 -07:00
007d6ea4de nixos: harden switch upgrade service guard 2026-05-16 12:33:02 -07:00
24c1a0a4d4 hyprland: switch overview to patched hyprexpo 2026-05-16 12:32:50 -07:00
890bdb0925 hyprland: keep file chooser dialogs focused 2026-05-16 00:24:51 -07:00
83b4889982 nix: pin custom hyprutils input 2026-05-16 00:24:38 -07:00
5cac4d4fc3 Use latest kernel on ryzen-shine 2026-05-16 00:23:35 -07:00
9d6ef77676 Enable Codex remote control feature 2026-05-15 13:54:40 -07:00
f60abcb876 Propagate hyprexpo selection labels 2026-05-15 13:44:05 -07:00
22f6fa1b69 Use combined Codex Desktop package 2026-05-15 12:58:01 -07:00
c5627de004 Fix manual NixOS switch 2026-05-15 03:15:02 -07:00
445f6bb2d7 Make Hyprland windows slide from bottom 2026-05-15 02:41:53 -07:00
79f24aa0ae Enable Codex remote control 2026-05-15 02:30:33 -07:00
e9c95cfc45 Use upstream hyprexpo flake package 2026-05-15 02:08:01 -07:00
e203230c4d Bump keepbook flake 2026-05-15 01:57:36 -07:00
038f0c1896 Update taffybar submodule 2026-05-15 01:45:25 -07:00
442710bc69 nixos: prune stale flake lock input 2026-05-15 01:16:58 -07:00
724fb61054 hyprland: make Hyprspace the primary overview 2026-05-15 01:16:46 -07:00
8250cfdbc9 codex: preserve generated local config sections 2026-05-15 01:15:34 -07:00
ac2295b017 Remove unused flake bindings 2026-05-15 00:58:49 -07:00
a44b21d681 Replace hyprexpo-plus with hyprexpo 2026-05-15 00:16:32 -07:00
1d701304fe Update Codex Desktop input 2026-05-15 00:06:19 -07:00
07d382fc03 Merge remote-tracking branch 'origin/master' 2026-05-14 23:31:10 -07:00
74bd7e76da nixos: add hyprland plugin dev tooling 2026-05-14 23:30:58 -07:00
2ddeb42416 nixos: bump org-agenda-api 2026-05-14 23:21:26 -07:00
0ff3100904 Expose rofi Roborock menu as package 2026-05-14 00:36:18 -07:00
f781e4a406 Add rofi Roborock control menu 2026-05-14 00:30:38 -07:00
f94209b7f5 Update taffybar release 2026-05-13 13:49:07 -07:00
72d0960fb1 Flake lock bump for nix-darwin 2026-05-13 10:33:01 -07:00
f4dcefe392 Fix taffybar config for updated workspace API 2026-05-13 04:10:54 -07:00
327a1768ab Bump taffybar input 2026-05-13 02:48:28 -07:00
814ea1283e Update taffybar submodule 2026-05-13 02:47:38 -07:00
bcc61aa6fa Disable Hyprglass in Hyprland config 2026-05-13 02:32:19 -07:00
8ae7a2e0e4 Update Hyprexpo input and cancel key 2026-05-13 02:31:20 -07:00
996d02cc60 Add Hyprland rofi action picker 2026-05-13 02:23:21 -07:00
bd85161f7a Propagate taffybar audio and favicon updates 2026-05-13 02:06:34 -07:00
cade9b9628 Further group Hyprland bindings 2026-05-13 02:04:26 -07:00
4055dfe0b9 xmonad: add KEF optical shortcut 2026-05-13 02:04:26 -07:00
6427e89ee4 rofi: follow wallpaper symlinks 2026-05-13 02:04:26 -07:00
df6dcc2153 nixos: tidy desktop terminal settings 2026-05-13 02:04:26 -07:00
dbb8f6addf nixos: repair xwayland socket before steam 2026-05-13 02:04:26 -07:00
96d456edc2 nixos: add daily Rust target sweep 2026-05-13 02:04:26 -07:00
ef84d1a270 Enable built-in audio duplex profile 2026-05-13 01:11:38 -07:00
9aea5407db Use Chrome favicons in taffybar workspaces 2026-05-13 01:11:38 -07:00
d91ca93750 Fix Hyprland Emacs Everywhere binds 2026-05-13 00:09:44 -07:00
098ccbf72b Reorganize Hyprland bindings 2026-05-13 00:02:15 -07:00
5d414403d8 Move hyprpaper startup logic into scripts 2026-05-12 23:58:06 -07:00
35bee5750f Simplify justfile 2026-05-12 23:42:43 -07:00
1742467799 hyprland: add KEF optical shortcut 2026-05-12 23:33:24 -07:00
bdc42f1ab1 hyprland: slow dropdown animation 2026-05-12 23:32:48 -07:00
7e9502cbf2 hyprland: configure hyprglass 2026-05-12 23:32:38 -07:00
43db4b8f1b hyprland: refresh plugin package set 2026-05-12 23:32:21 -07:00
191a83bb7b Add Hyprspace to Hyprland 2026-05-12 20:41:25 -07:00
7946892f7f Fix stale Hyprland fullscreen state 2026-05-12 18:30:36 -07:00
5c80b986ed Use no-fade spring slide for Ghostty dropdown 2026-05-12 14:11:44 -07:00
842f161416 Make Ghostty dropdown a transparent visor 2026-05-12 13:55:23 -07:00
92d8472bd2 Work around Chrome Wayland fractional scaling 2026-05-12 07:21:31 -07:00
f5a88df96b Update Codex desktop flake input 2026-05-12 02:07:12 -07:00
c2ca860a99 Generate Ghostty dropdown config 2026-05-12 01:13:09 -07:00
a6dee77f58 [Hyprland] Suppress Flameshot overlay effects 2026-05-12 01:10:50 -07:00
3e0b8873e5 Add Flameshot screenshot bindings 2026-05-12 01:07:17 -07:00
ddaa3a78ac Extract Hyprland scratchpad module 2026-05-12 00:03:14 -07:00
973b67f185 Prune obsolete docs 2026-05-12 00:01:13 -07:00
33066b3abf Respect selected project when finding files 2026-05-11 23:41:57 -07:00
1baf114689 Unify single-node k3s configuration 2026-05-11 23:24:06 -07:00
aed1f43818 Make project switching find files 2026-05-11 22:49:38 -07:00
ba07ad9747 Point Codex desktop at NixOS browser fix branch 2026-05-11 19:01:29 -07:00
0f00f7d33f Use slide animations for Hyprland windows 2026-05-11 16:45:03 -07:00
463c842d4f Fix PipeWire audio helpers 2026-05-11 16:12:42 -07:00
4c669b60f9 Update nix-darwin flake inputs 2026-05-11 13:39:10 -07:00
65243e8a7e Stop overriding taffybar flake input 2026-05-10 21:11:29 -07:00
a121100271 Simplify just switch 2026-05-10 19:50:13 -07:00
45c85fae55 Fix Wayland screensharing 2026-05-10 17:38:55 -07:00
23e4cd033a Update taffybar submodule pointer 2026-05-10 13:58:03 -07:00
de44814a00 Update desktop and Codex configuration 2026-05-10 13:56:25 -07:00
21d8d75d86 Update hyprNStack flake lock 2026-05-10 12:40:54 -07:00
f6386afb49 Stop overriding hyprNStack with local path 2026-05-10 12:19:46 -07:00
2a036581c7 Use host identity colors for tmux status 2026-05-10 02:37:58 -07:00
e7486cb2c4 Fix NixOS switch with local HyprNStack 2026-05-10 01:11:42 -07:00
57cccedcf9 Tune Hyprland animations with spring curves 2026-05-10 00:54:25 -07:00
cef847f117 Update keepbook input 2026-05-09 23:40:32 -07:00
1ee2625490 Disable broken Hyprland verify check 2026-05-09 13:01:57 -07:00
fdaaf130f2 Split Hyprland Lua config into modules 2026-05-09 13:01:57 -07:00
c12b9c05db Update nix-darwin flake inputs 2026-05-09 02:15:25 -07:00
82b4dff20a Make tmux bar default blue 2026-05-09 02:09:04 -07:00
d74fa81e10 Reduce scale a abit further 2026-05-09 00:36:25 -07:00
08eeeb0ad7 Bump Codex flakes 2026-05-09 00:21:17 -07:00
2d2d1f3ca8 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	nixos/flake.nix
2026-05-08 23:42:46 -07:00
ab22bd551d chore: add remaining local artifacts 2026-05-08 23:38:35 -07:00
f56cb6ac42 nixos: use grub on strixi-minaj 2026-05-08 23:36:50 -07:00
1c5dc8a0c7 flake: update hyprland inputs 2026-05-08 23:16:13 -07:00
e07d738857 emacs: enable doom modeline on startup 2026-05-08 23:16:07 -07:00
dbd58a9488 nixos: disable ryzen-shine gitea runner 2026-05-08 23:16:03 -07:00
dd71d880f6 nix: trust taffybar cachix 2026-05-08 23:15:58 -07:00
17f9f85073 taffybar: update tray animation 2026-05-08 23:15:46 -07:00
46e3e7db59 nix: add elegant grub2 theme package 2026-05-08 23:12:32 -07:00
4a57e6f936 zellij: add session switcher 2026-05-08 23:12:06 -07:00
ad4b8c267e Add pinned window indicators 2026-05-08 22:16:38 -07:00
d44736aec9 Add tmcodex resume launcher binding 2026-05-08 21:01:42 -07:00
2d92e9d55d Use WhiteSur ultrawide GRUB theme 2026-05-08 20:48:05 -07:00
b8e6abd628 nixos: update host tmux colors 2026-05-08 20:02:58 -07:00
787f312cbe hyprland: dispatch bind callbacks explicitly 2026-05-08 18:00:31 -07:00
968abf1a05 Remove obsolete ryzen-shine original config 2026-05-08 18:00:11 -07:00
9a28a63ba3 Add Hyprland rofi layout selector 2026-05-08 12:37:08 -07:00
ee35eb2af0 xmonad: update xmonad-contrib 2026-05-08 11:07:39 -07:00
65297d652e nixos: split flake outputs 2026-05-08 11:07:01 -07:00
d28ec5cdd4 Fix notifications tray URL handling 2026-05-08 10:53:11 -07:00
5e67c1c795 hyprland: preserve tiled geometry when dragging float
Snapshot tiled window geometry before enabling floating from the mouse drag/resize bindings, then restore it so detached tiled windows keep their current size.
2026-05-08 03:49:45 -07:00
db56ef8aa1 desktop: remove noctalia and caelestia shells 2026-05-08 02:36:23 -07:00
21868cca81 machines: adjust strixi internal display profile 2026-05-08 02:36:00 -07:00
8c1687fa83 checks: extract hyprland config smoke test 2026-05-08 02:35:32 -07:00
e5678819f9 Update kanshi-sni input 2026-05-08 02:29:25 -07:00
10d26e9968 nixos: update hyprwinview 2026-05-08 02:27:51 -07:00
2cf561bf78 Merge remote-tracking branch 'origin/master' 2026-05-08 02:13:23 -07:00
a51fb925ed hypr: use reverted hyprwinview damage hook 2026-05-08 02:11:46 -07:00
f602cdbe95 Merge remote-tracking branch 'origin/master' 2026-05-08 02:11:08 -07:00
06b0790647 hypr: disable monitor added animation 2026-05-08 01:13:28 -07:00
bb54a004ae Configure host identity in Nix 2026-05-08 00:45:23 -07:00
cf6533ac2f codex: preserve generated local machine state 2026-05-07 23:38:28 -07:00
c33fcca67b taffybar: tune SNI tray display 2026-05-07 23:38:28 -07:00
86b8891084 Enable hover expansion for SNI tray 2026-05-07 23:38:08 -07:00
500a51b0fa Disable system autorandr for Hyprland 2026-05-07 23:16:11 -07:00
598abae7b3 nixos: update codex desktop input 2026-05-07 22:51:16 -07:00
fd8f4a222a nixos: run railbird-sf on railbird k3s 2026-05-07 22:43:16 -07:00
7a98dd1bcf nixos: add GPU support to railbird k3s 2026-05-07 22:42:29 -07:00
42e8e6db6f nixos: keep k3s from claiming host web ports 2026-05-07 22:37:34 -07:00
5ba22bb56a Point hyprNStack at combined branch 2026-05-07 22:21:50 -07:00
a56d93d4b1 tmux: improve ghostty window titles 2026-05-07 21:50:25 -07:00
0fbb831462 Use hyprwinview for window switch bindings 2026-05-07 21:25:20 -07:00
b38c7867c2 ci: cache imalison-taffybar flake 2026-05-07 20:23:15 -07:00
dce81586ac Format NixOS flake with alejandra 2026-05-07 15:48:26 -07:00
e1fd076982 Save current desktop config updates 2026-05-07 15:46:17 -07:00
d04c6b4cd5 taffybar: pin before minimized workspace change 2026-05-07 14:54:24 -07:00
291e497d63 nixos: use nvidia device plugin for k3s gpu 2026-05-07 14:54:21 -07:00
1ae061da47 nixos: fix railbird-sf rebuild blockers 2026-05-07 14:40:42 -07:00
54c86b2366 home: add vector image tools 2026-05-07 14:02:01 -07:00
1ffaa8c5ee nix-darwin: update flake inputs 2026-05-07 14:02:01 -07:00
58ad1bc679 Show Hyprland minimized windows in taffybar 2026-05-07 02:49:37 -07:00
a58b8fb6aa nixos: disable broken codex desktop package 2026-05-07 02:46:15 -07:00
0ab53ed0fb nixos: refresh taffybar path input 2026-05-07 02:42:11 -07:00
fb3af2543a nixos: update org-agenda-api input 2026-05-07 02:40:45 -07:00
13c465efef taffybar: restore strixi icon spacing 2026-05-07 02:33:54 -07:00
e3474040b2 Remove Anthropic usage widget from taffybar 2026-05-07 02:33:54 -07:00
7ef9b4be0d Update keepbook input 2026-05-06 23:27:28 -07:00
f6b2a1ae8c emacs: replace projectile integrations with project.el variants 2026-05-06 15:10:28 -07:00
34793d7075 nixos: update hyprwinview 2026-05-06 15:07:29 -07:00
aaf2ebd569 Preserve holistic multiplexer titles 2026-05-06 15:05:52 -07:00
544da689ab emacs: migrate project navigation to project.el 2026-05-06 15:03:18 -07:00
e28cbee448 emacs: disable dbus and gvfs backends 2026-05-06 14:59:52 -07:00
32cb3944cc Use hyprNStack focus-local placement branch 2026-05-06 13:51:17 -07:00
837ba834ba keepbook: update package build fix 2026-05-05 23:23:09 -07:00
def5b968e2 keepbook: install dioxus desktop app 2026-05-05 22:58:35 -07:00
fa28f4c433 nix: set alejandra formatter 2026-05-05 22:47:44 -07:00
8f2bb38d23 Use GitHub source for xmonad-river 2026-05-05 22:39:44 -07:00
fc293e079a Update Hyprland scratchpad workarea cache 2026-05-05 13:23:32 -07:00
d3912fc060 nix: update darwin flake inputs 2026-05-05 13:04:16 -07:00
3ced6dc45c Include taffybar config modules in flake source 2026-05-05 03:18:12 -07:00
e0865300ef taffybar: update submodule 2026-05-05 03:06:58 -07:00
9cb7da28e4 nixos: allow hyprctl eval in config check 2026-05-05 03:06:48 -07:00
1817c73609 emacs: defer daemon startup initialization 2026-05-05 03:06:41 -07:00
a59c316d85 refactor: split taffybar config into modules 2026-05-05 03:04:15 -07:00
63fcebf392 Update hyprwinview navigation bindings
Advance hyprwinview to the commits that add Ctrl-WASD and Super-WASD defaults for filter-mode directional selection. The plugin remains configurable through the existing keys_filter_* options.
2026-05-05 02:53:13 -07:00
c53405bcf7 Update hyprwinview input
Advance hyprwinview to include the filter-mode bring bindings and the overview-bind repro harness. The previous active system generation still had an older plugin build without keys_filter_bring, so Ctrl-B in filter mode could not dispatch bring even though the dotfiles config expected the default.
2026-05-05 02:45:31 -07:00
eb95ee9faa Fix hyprwinview overview bind shadowing
Mark the overview keybinds transparent so Hyprland does not shadow the first Super+Tab after a focus-moving bind. The missed first press was reproduced with a ydotool harness as a keybind-level failure: after Super+D, the first Super+Tab did not reach the hyprwinview Lua callback, while the second did. With transparent overview binds, the harness passes consistently.

While touching the overview wrapper, keep the hyprwinview show action explicit and gate diagnostic tracing behind /tmp/hypr-overview-bind.enable. Also use Hyprland's integer notification icon enum values so missing-plugin/debug notifications do not emit invalid icon errors.
2026-05-05 02:38:20 -07:00
8dac748f56 Optimize rofi tmcodex session scan 2026-05-05 01:52:34 -07:00
dc95fe6561 Add Codex scratchpad 2026-05-05 01:41:50 -07:00
108b491f6a Update hyprwinview filter bring bindings 2026-05-05 01:27:57 -07:00
8d7947a773 Update hyprwinview filter close binding 2026-05-05 01:10:53 -07:00
103cdeaa9f Update hyprwinview filter keybindings 2026-05-05 00:59:41 -07:00
6e89a3fcb5 Update hyprwinview keyboard input fix 2026-05-05 00:20:30 -07:00
4bec7af523 Update hyprwinview filter input fix 2026-05-05 00:06:26 -07:00
168435d3e7 Start hyprwinview in filter mode 2026-05-04 23:53:44 -07:00
91f539547c Broaden Google Messages scratchpad match 2026-05-04 23:51:08 -07:00
e4cccc54a4 Propagate desktop config updates 2026-05-04 23:49:36 -07:00
2d69c143b1 Use bfd linker for imalison-taffybar 2026-05-04 23:18:20 -07:00
16fa31887a Add Google Messages scratchpad 2026-05-04 23:17:28 -07:00
cfe0ca59bf Set PATH for git-sync tray services 2026-05-04 22:17:23 -07:00
200504318b Order taffybar after status notifier watcher 2026-05-04 22:17:16 -07:00
52aa541aee Remove taffybar widget debug profiles 2026-05-04 22:17:09 -07:00
211aa60b73 Suppress Emacs TRAMP DBus noise 2026-05-04 22:14:03 -07:00
c6536b76cd Harden Emacs notification startup 2026-05-04 22:13:56 -07:00
a573176200 Pin Emacs compatibility package sources 2026-05-04 22:13:31 -07:00
5f5b43839b Update rofi tmcodex launcher 2026-05-04 22:13:15 -07:00
9a0612e608 Add NeoWall wallpaper integration 2026-05-04 22:13:01 -07:00
4b552afb7a Update command launcher keybindings 2026-05-04 22:12:36 -07:00
43a718536a Modularize NixOS bootloaders 2026-05-04 22:03:01 -07:00
77a03e2dc6 Propagate taffybar workspace activation fix 2026-05-04 22:00:42 -07:00
4ea7a163e7 Enable Codex fast mode by default 2026-05-04 21:59:00 -07:00
576605e3cf Update tiling WM overview bindings 2026-05-04 15:03:13 -07:00
c0278f9411 Update taffybar flake locks 2026-05-04 14:54:50 -07:00
06ea3eec29 Reduce strixi-minaj display scale 2026-05-04 14:53:01 -07:00
a4374a99ec Replace root README with repo overview 2026-05-04 11:56:17 -07:00
829a0846a1 Remove taffybar screensaver widget 2026-05-04 08:24:01 -07:00
df36fe2d12 Propagate taffybar redraw fix 2026-05-04 08:13:17 -07:00
07fb87ddbb taffybar: wait for display socket before startup 2026-05-03 12:41:26 -07:00
5cdea7dd1a hyprland: launch through start wrapper 2026-05-03 12:41:26 -07:00
6cb3415987 nix: update codex desktop linux input 2026-05-03 12:41:26 -07:00
364f9fdc6a taffybar: bump local checkout 2026-05-03 12:41:26 -07:00
8ee4a242ab taffybar: stabilize workspace icon padding 2026-05-03 00:13:17 -07:00
ba435c5119 roborock: add segment cleaning support 2026-05-03 00:13:02 -07:00
ee1a6b8904 nixos: add Cua sandbox module 2026-05-03 00:12:41 -07:00
ca5b2b566f Add xo alias for xdg-open 2026-05-02 23:52:40 -07:00
67589779df home-manager: keep codex home writable 2026-05-02 21:14:34 -07:00
9a5a9ec5da nix: update darwin flake inputs 2026-05-02 21:11:03 -07:00
d652f80d05 darwin: add imalison migration target 2026-05-02 21:11:03 -07:00
b4a7096ac9 hammerspoon: add desktop move and status widgets 2026-05-02 21:11:03 -07:00
117b836227 Propagate taffybar icon fix through flakes 2026-05-02 16:01:57 -07:00
11ae2f489c nixos: expose ydotool socket to user sessions 2026-05-02 16:00:55 -07:00
6801a90e32 hyprland: restore tab groups by visual order 2026-05-02 15:52:22 -07:00
d9058deb4b Update taffybar switcher icon fix 2026-05-02 13:31:10 -07:00
815601568f Bump taffybar version 2026-05-02 12:15:29 -07:00
600be3e2b7 hyprland: preserve tab group restore order 2026-05-02 12:02:01 -07:00
5664aa7aae Use env for GitHub credential helper 2026-05-02 11:56:17 -07:00
52febc5943 Link Hyprland config directory 2026-05-02 11:26:08 -07:00
281ec0347e Update taffybar omni menu 2026-05-02 11:12:05 -07:00
54384995d4 nixos: bump keepbook 2026-05-02 11:05:14 -07:00
a06919778d nixos: harden switch recipe 2026-05-02 06:03:30 -07:00
7384d7f17c nixos: update code tooling setup 2026-05-02 06:00:10 -07:00
95739ca7b4 Suppress Codex unstable feature warning 2026-05-02 05:44:10 -07:00
660b1fa8f3 Enable Codex goals feature 2026-05-02 05:41:10 -07:00
0c0dc2d318 Use NixOS logo for taffybar omni menu 2026-05-02 05:37:13 -07:00
e7b8ff2fc4 Refine taffybar omni menu launchers 2026-05-02 05:33:06 -07:00
ee3afe7fdd Add taffybar omni menu 2026-05-02 05:29:12 -07:00
beeb505cdd nixos: point codex desktop at runtime fix branch 2026-05-02 00:48:03 -07:00
d12cbe0b79 taffybar: tune strixi-minaj density 2026-05-02 00:20:21 -07:00
0e5d635132 flake: refresh locked inputs 2026-05-01 22:54:42 -07:00
439b95a593 taffybar: tighten session and host handling 2026-05-01 22:53:52 -07:00
0750934622 hyprland: clean session startup and scratchpads 2026-05-01 22:53:05 -07:00
b153adcb8c river-xmonad: expand window management support 2026-05-01 22:52:26 -07:00
7d7daeb91f Propagate taffybar icon scaling fix 2026-05-01 22:52:26 -07:00
950a7994d6 nixos: enable games on ryzen-shine 2026-05-01 22:47:29 -07:00
a0a71f5d2d nixos: add codex desktop input 2026-05-01 22:46:42 -07:00
ad23acab4e hypr: cycle workspace layouts 2026-05-01 22:43:54 -07:00
0aba31c21f emacs: defer more startup packages 2026-05-01 22:43:38 -07:00
4cccd9db2d Fix taffybar Hyprland launch environment 2026-05-01 22:37:25 -07:00
5637db1182 nixos: use combined hyprexpo branch
Point hyprland-plugins-lua at the branch that combines the PR fixes with the workspace number support used by the local Hyprland Lua config.
2026-05-01 21:19:51 -07:00
0a0024d009 Wire taffybar usage menu fixes 2026-05-01 20:15:30 -07:00
030a67364e Propagate workspace history state export 2026-05-01 18:48:27 -07:00
716405b1ef Delete lua migration checklist. 2026-05-01 18:48:05 -07:00
854b55086c taffybar: update pinned checkout 2026-04-30 11:27:22 -07:00
ae4a398e77 claude: enable remote control at startup 2026-04-30 11:27:22 -07:00
8d346bc37e nixos: add river xmonad session 2026-04-30 11:27:22 -07:00
90bd377335 nixos: add session environment guards 2026-04-30 11:27:22 -07:00
9008190a90 Remove stale Flameshot grim adapter setting 2026-04-30 10:32:57 -07:00
b16695b574 Speed up hyprwinview animation 2026-04-30 10:20:54 -07:00
937174080c Update hyprwinview close zoom timing 2026-04-30 09:37:57 -07:00
9b970c6458 Slow hyprwinview workspace zoom stage 2026-04-30 09:09:16 -07:00
1bc58095b5 Quarter hyprwinview animation speed 2026-04-30 09:07:12 -07:00
d9ea8b0e1f Halve hyprwinview animation speed 2026-04-30 09:06:20 -07:00
385b28d6a4 Slow hyprwinview animation 2026-04-30 09:02:36 -07:00
24ebab874f Enable hyprwinview background blur 2026-04-30 03:39:17 -07:00
0857e4b6da Re-enable hyprexpo with hyprwinview 2026-04-30 03:25:52 -07:00
ea75c960e8 Update hyprwinview input 2026-04-30 03:14:25 -07:00
8936112348 nixos: add cachix populate recipe 2026-04-30 03:07:52 -07:00
7ac4e091c2 docs: evaluate River window manager options 2026-04-30 02:17:06 -07:00
29eefa99e3 sni: add tray restart helper 2026-04-30 02:16:54 -07:00
b3d77bb310 quickshell: build caelestia with clang 2026-04-30 02:16:40 -07:00
d1061a75ad nix: lock workspace history plugin input 2026-04-30 02:15:34 -07:00
59f5d22b09 taffybar: update vendored checkout 2026-04-30 02:15:18 -07:00
98b8c6fbd2 taffybar: use GitHub source by default 2026-04-30 02:15:01 -07:00
231b22d8ae hyprland: use workspace history plugin input 2026-04-30 02:13:58 -07:00
1d85ed76d6 zsh: add managed just completion 2026-04-30 02:12:37 -07:00
b65010283c desktop: configure qtct appearance 2026-04-30 02:12:16 -07:00
022580f1af hyprland: finish Lua config migration cleanup 2026-04-30 02:11:52 -07:00
f6026b5cac hyprland: add workspace history plugin 2026-04-30 01:33:08 -07:00
34906469b9 hyprland: match Element scratchpad Wayland class 2026-04-30 00:47:59 -07:00
3cb0301f9a Add per-monitor workspace history cycling 2026-04-30 00:37:49 -07:00
6f489d14ab Pin taffybar Hyprland recovery fix 2026-04-29 22:59:22 -07:00
acae19d9c5 hyprland: move hyprexpo to alt-tab 2026-04-29 22:58:14 -07:00
c30a67facf Fix Hyprland workspace tab grouping 2026-04-29 21:28:48 -07:00
d48edc9bb8 Remove direct fullscreen WM bindings 2026-04-29 14:26:33 -07:00
af570360d3 taffybar: respect desktop shell selector 2026-04-29 14:23:15 -07:00
34fd60e8f2 Remove Chrome-backed scratchpads 2026-04-29 14:19:15 -07:00
f826c6ae75 xmonad: respect desktop shell UI selector 2026-04-29 14:12:04 -07:00
1a2b75adcb Fix KDE MIME defaults in xmonad session 2026-04-29 14:08:19 -07:00
4e52e81a50 Fix desktop shell UI systemd condition 2026-04-29 14:07:14 -07:00
df0b7b6db4 Split local Codex config from dotfiles 2026-04-29 13:57:19 -07:00
a7769545f1 hyprland: pin custom plugin forks 2026-04-29 13:35:03 -07:00
bb32668387 hyprland: run screensaver as layer overlay 2026-04-29 13:34:04 -07:00
8ccf5fb7de hyprland: add noctalia shell module 2026-04-29 13:33:39 -07:00
52861430da hyprland: route shell actions through wrapper 2026-04-29 13:31:48 -07:00
d9ebb812c5 desktop: add shell ui selector 2026-04-29 13:30:40 -07:00
5cf2eda008 hyprland: animate ghostty dropdown 2026-04-29 13:29:42 -07:00
6299ad2c7d hyprland: expand animation leaf config 2026-04-29 13:29:24 -07:00
672cc14713 hyprland: style grouped window tabs 2026-04-29 13:28:38 -07:00
64c45e1060 hyprland: add tabbed workspace grouping 2026-04-29 13:27:47 -07:00
a5413331d9 hyprland: route grouped directional controls 2026-04-29 13:24:19 -07:00
1044565bf7 hyprland: dispatch windows by address selector 2026-04-29 13:23:33 -07:00
d684f6fbc5 hyprland: import runtime dir into session environment 2026-04-29 13:22:37 -07:00
71deb64ed0 taffybar: fix local flake warning build 2026-04-29 12:18:00 -07:00
bb909849bd Add reusable remote Hyprland module 2026-04-29 11:17:02 -07:00
a37e83fb23 Install Hyprland rofi window picker 2026-04-29 07:55:41 -07:00
53d8a69a31 Restore rofi window picker for Hyprland 2026-04-29 07:34:45 -07:00
87fd1681e2 Restore Hyprland reload binding 2026-04-29 07:26:42 -07:00
8933f8e545 Remove Hyprland shell script bindings 2026-04-29 07:21:27 -07:00
ed90130233 Merge branch 'hyprland-lua-squashed' 2026-04-29 03:00:36 -07:00
aa1fbf9699 Show AI usage remaining percentages 2026-04-29 01:45:30 -07:00
3e05939ce3 Add random Hyprland screensaver rotation 2026-04-29 01:45:30 -07:00
8e2128b8d4 Add Roborock vacuum control
Add a python-roborock based CLI wrapper and package it for the NixOS system profile.
2026-04-29 01:45:30 -07:00
1696845579 Add KEF speaker control CLI 2026-04-29 01:45:30 -07:00
5522b8bacd Add logos to AI usage widgets 2026-04-29 01:45:30 -07:00
9c9af9f856 Implement Hyprland Lua migration 2026-04-28 21:10:34 -07:00
251 changed files with 15679 additions and 8415 deletions

View File

@@ -1,22 +1,20 @@
name: Build and Push Cachix (NixOS)
name: Build and Push Cachix (imalison-taffybar)
on:
push:
branches: [master]
paths:
- "nixos/**"
- "org-agenda-api/**"
- "dotfiles/config/taffybar/**"
- ".github/workflows/cachix.yml"
pull_request:
branches: [master]
paths:
- "nixos/**"
- "org-agenda-api/**"
- "dotfiles/config/taffybar/**"
- ".github/workflows/cachix.yml"
workflow_dispatch: {}
jobs:
nixos-strixi-minaj:
imalison-taffybar:
runs-on: ubuntu-latest
permissions:
@@ -51,9 +49,6 @@ jobs:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v16
- name: Use GitHub Actions Cache for /nix/store
uses: DeterminateSystems/magic-nix-cache-action@v7
- name: Require Cachix config (push only)
if: github.event_name == 'push'
env:
@@ -85,11 +80,10 @@ jobs:
name: ${{ vars.CACHIX_CACHE_NAME }}
skipPush: true
- name: Build NixOS system (strixi-minaj)
- name: Build imalison-taffybar
run: |
set -euxo pipefail
nix build \
--no-link \
--print-build-logs \
./nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel \
--override-input railbird-secrets ./nixos/ci/railbird-secrets-stub
./dotfiles/config/taffybar#defaultPackage.x86_64-linux

View File

@@ -1 +0,0 @@
dotfiles/emacs.d/README.org

228
README.org Normal file
View File

@@ -0,0 +1,228 @@
# -*- mode: org; -*-
#+TITLE: colonelpanic8's Dotfiles
This repository is the source of truth for my machines, user environment, and a
large set of day-to-day workflow scripts. It started as an Emacs configuration,
and that is still here, but the repo is now mostly a Nix-managed personal
systems repo: NixOS hosts, a nix-darwin host, Home Manager link management,
desktop/window-manager configuration, shell tooling, agent configuration, and
org-agenda-api deployment glue.
The old literate Emacs README lives at [[file:dotfiles/emacs.d/README.org][dotfiles/emacs.d/README.org]]. The
published GitHub Pages site is still generated from that document.
* What This Manages
- NixOS systems under [[file:nixos/][nixos/]], with one flake configuration per file in
[[file:nixos/machines/][nixos/machines/]].
- A nix-darwin configuration under [[file:nix-darwin/][nix-darwin/]] for the macOS machine.
- Shared Nix modules and overlays under [[file:nix-shared/][nix-shared/]].
- Home Manager placement of files from [[file:dotfiles/][dotfiles/]] into =$HOME= and
=$XDG_CONFIG_HOME=.
- Shell functions and executable helpers in [[file:dotfiles/lib/][dotfiles/lib/]], added to
=PATH= and =fpath= by the NixOS environment module.
- Desktop environment and tiling-window-manager configuration for Hyprland,
XMonad, River/XMonad experiments, Taffybar, Waybar, Rofi, Alacritty,
autorandr, and related utilities.
- Emacs and org-mode configuration, including the tangled org configuration used
by the org-agenda-api container.
- Agent and tool configuration for Codex, Claude, project guides, and local
task-specific skills.
- Container and deployment configuration for personal org-agenda-api instances.
This is not intended to be a generic starter dotfiles repo. Many modules assume
my users, hostnames, hardware, SSH keys, secrets layout, and local checkout path
=(~/dotfiles=). It is still useful as a reference for how the pieces fit
together.
* Layout
| Path | Purpose |
|------+---------|
| [[file:nixos/][nixos/]] | Main NixOS flake. Imports feature modules, host files, agenix secrets, Home Manager, overlays, and package checks. |
| [[file:nixos/machines/][nixos/machines/]] | Per-host NixOS entrypoints such as =strixi-minaj=, =ryzen-shine=, =railbird-sf=, WSL hosts, and Raspberry Pi hosts. |
| [[file:nix-darwin/][nix-darwin/]] | macOS system flake using nix-darwin, nix-homebrew, Home Manager, agenix, and shared packages. |
| [[file:nix-shared/][nix-shared/]] | Shared package lists, overlays, Home Manager modules, and Syncthing fragments used by Linux and macOS. |
| [[file:dotfiles/][dotfiles/]] | Files that are linked into the home directory. Top-level entries become dotfiles; =dotfiles/config/*= becomes XDG config. |
| [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] | User commands and desktop helpers, including Rofi scripts, Hyprland helpers, audio controls, and Syncthing utilities. |
| [[file:dotfiles/lib/functions/][dotfiles/lib/functions/]] | Zsh autoload functions and shell helpers. |
| [[file:dotfiles/config/hypr/][dotfiles/config/hypr/]] | Hyprland Lua config, lock/idle config, workspace files, scripts, and plugin state. |
| [[file:dotfiles/config/xmonad/][dotfiles/config/xmonad/]] | XMonad configuration, local Cabal package, flake, and upstream submodules. |
| [[file:dotfiles/config/taffybar/][dotfiles/config/taffybar/]] | Personal Taffybar package/configuration, CSS themes, scripts, and local upstream checkout. |
| [[file:dotfiles/emacs.d/][dotfiles/emacs.d/]] | Emacs configuration, literate org config, org-mode setup, snippets, and generated/tangled Elisp. |
| [[file:dotfiles/agents/][dotfiles/agents/]] | Agent instructions, project constellation guides, and local Codex skills. |
| [[file:org-agenda-api/][org-agenda-api/]] | Instance-specific config and container/deploy glue for org-agenda-api. |
| [[file:docs/][docs/]] | Design notes for Cachix, tiling WM behavior, River evaluation, and org-agenda-api consolidation. |
| [[file:gen-gh-pages/][gen-gh-pages/]] | Legacy/publication pipeline that exports the Emacs README to GitHub Pages. |
* NixOS
The NixOS flake is [[file:nixos/flake.nix][nixos/flake.nix]]. It discovers host configurations from
[[file:nixos/machines/][nixos/machines/]] and exposes them as =nixosConfigurations.<hostname>=.
The broad feature set is assembled by [[file:nixos/configuration.nix][nixos/configuration.nix]], where
=features.full.enable= expands into the normal desktop/profile modules.
Common workflow:
#+begin_src sh
cd ~/dotfiles/nixos
just switch
#+end_src
The local =just switch= recipe wraps =nixos-rebuild switch --flake ".#"=, waits
for an already-running switch to finish, and overrides the Taffybar inputs to
the live checkout under this repo. Use it instead of running =nixos-rebuild=
directly.
Useful variants:
#+begin_src sh
cd ~/dotfiles/nixos
just switch-remote
just switch-local-taffybar
just remote-switch <host>
#+end_src
Build/check examples:
#+begin_src sh
nix flake check ~/dotfiles/nixos
nix build ~/dotfiles/nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel
#+end_src
The flake also exposes package/check outputs for Hyprland plugins and a
Hyprland Lua config syntax/verification check.
* nix-darwin
The macOS configuration lives in [[file:nix-darwin/flake.nix][nix-darwin/flake.nix]]. It uses
nix-darwin, nix-homebrew, Home Manager, agenix, and the shared package list in
[[file:nix-shared/system/essential.nix][nix-shared/system/essential.nix]].
Common workflow:
#+begin_src sh
cd ~/dotfiles/nix-darwin
just switch
#+end_src
The active host configuration is =mac-demarco-mini=. There is also a
=mac-demarco-mini-imalison= target used while migrating the primary macOS user.
* Home File Linking
The NixOS Home Manager module [[file:nixos/dotfiles-links.nix][nixos/dotfiles-links.nix]] reproduces the useful
part of =rcm/rcup=:
- files under [[file:dotfiles/][dotfiles/]] are linked into =$HOME= with a leading dot;
- directories under [[file:dotfiles/config/][dotfiles/config/]] are linked into =$XDG_CONFIG_HOME=;
- links are out-of-store symlinks, so editing the checkout updates runtime
config immediately;
- generated or special directories such as =codex=, =lib=, =config=, and
=emacs.d= are handled separately.
On NixOS, shell scripts belong in [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] and autoloaded shell
functions belong in [[file:dotfiles/lib/functions/][dotfiles/lib/functions/]]. [[file:nixos/environment.nix][nixos/environment.nix]] adds
those paths to the shell environment.
The nix-darwin Home Manager module in [[file:nix-darwin/home/common.nix][nix-darwin/home/common.nix]] uses the
same basic idea for macOS, with extra launchd, GPG, Raycast, Homebrew, and agent
setup.
* Desktop Stack
The desktop setup is modular. [[file:nixos/desktop.nix][nixos/desktop.nix]] enables the common desktop
surface, while individual modules layer in window managers, panels, launchers,
notifications, SNI/tray support, fonts, and app defaults.
The currently important pieces are:
- Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], with imported Lua
modules under [[file:dotfiles/config/hypr/hyprland/][dotfiles/config/hypr/hyprland/]], backed by custom plugin inputs in
the NixOS flake.
- XMonad configuration in [[file:dotfiles/config/xmonad/xmonad.hs][dotfiles/config/xmonad/xmonad.hs]], with upstream
=xmonad= and =xmonad-contrib= available as submodules/checkouts.
- Taffybar configuration in [[file:dotfiles/config/taffybar/taffybar.hs][dotfiles/config/taffybar/taffybar.hs]], plus a local
flake and scripts for restart, screenshots, and SNI debugging.
- Waybar, Rofi, autorandr, Alacritty, Zellij, and miscellaneous app configs
under [[file:dotfiles/config/][dotfiles/config/]].
The intended tiling-WM behavior is documented in
[[file:docs/tiling-wm-experience.md][docs/tiling-wm-experience.md]].
* Emacs And Org
Emacs is still a major part of the repo, just no longer the only thing here.
The main files are:
- [[file:dotfiles/emacs.d/README.org][dotfiles/emacs.d/README.org]]: the original literate Emacs README.
- [[file:dotfiles/emacs.d/init.el][dotfiles/emacs.d/init.el]] and [[file:dotfiles/emacs.d/early-init.el][early-init.el]]: runtime entrypoints.
- [[file:dotfiles/emacs.d/org-config.org][dotfiles/emacs.d/org-config.org]]: the org-mode configuration that is tangled
for normal Emacs and for org-agenda-api.
- [[file:gen-gh-pages/][gen-gh-pages/]] and [[file:.github/workflows/gh-pages.yml][.github/workflows/gh-pages.yml]]: export the Emacs README
to the public GitHub Pages site.
* org-agenda-api
The repo carries the personal integration layer for
[[https://github.com/colonelpanic8/org-agenda-api][org-agenda-api]].
[[file:nixos/org-agenda-api.nix][nixos/org-agenda-api.nix]] tangles the org-mode configuration from
[[file:dotfiles/emacs.d/org-config.org][dotfiles/emacs.d/org-config.org]]. [[file:org-agenda-api/container.nix][org-agenda-api/container.nix]] combines that
tangled config with per-instance loaders under [[file:org-agenda-api/configs/][org-agenda-api/configs/]] and
builds OCI containers exposed by the NixOS flake.
The host-side NixOS module [[file:nixos/org-agenda-api-host.nix][nixos/org-agenda-api-host.nix]] runs the container
behind nginx with ACME certificates and Podman.
To enter the deployment shell:
#+begin_src sh
nix develop ~/dotfiles/nixos#org-agenda-api
#+end_src
* Secrets
Secrets are intentionally not stored as plaintext in the repo. Nix-managed
secrets use agenix files under [[file:nixos/secrets/][nixos/secrets/]]. Runtime credentials and
personal service passwords live in =pass=. Modules and scripts should consume
secrets from those sources at runtime rather than checking derived values into
git.
* Submodules And Local Checkouts
Some third-party or upstream projects are tracked as submodules:
- =dotfiles/config/taffybar/taffybar=
- =dotfiles/config/xmonad/xmonad=
- =dotfiles/config/xmonad/xmonad-contrib=
- =dotfiles/config/alacritty/themes=
- =nixos/railbird.ai=
Clone with submodules when bootstrapping a new checkout:
#+begin_src sh
git clone --recurse-submodules git@github.com:IvanMalison/dotfiles.git ~/dotfiles
#+end_src
This repo also contains project-local git worktrees under =.worktrees/= during
active development. Those are machine-local working state and are ignored.
* CI And Caches
[[file:.github/workflows/cachix.yml][.github/workflows/cachix.yml]] can build the =strixi-minaj= NixOS closure and
push paths to Cachix.
The top-level [[file:justfile][justfile]] contains helper commands for populating the
=colonelpanic8-dotfiles= Cachix cache from a local machine.
* Working In This Repo
- Prefer Nix modules for system-level behavior and Home Manager modules for
user-level placement and services.
- Put user commands in [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] and shell functions in
[[file:dotfiles/lib/functions/][dotfiles/lib/functions/]].
- Run NixOS switches from [[file:nixos/][nixos/]] with =just switch=.
- Run macOS switches from [[file:nix-darwin/][nix-darwin/]] with =just switch=.
- Keep host-specific behavior in [[file:nixos/machines/][nixos/machines/]] where possible.
- Do not commit secrets or generated local state; use agenix, =pass=, or ignored
machine-local files.

View File

@@ -1,37 +0,0 @@
# Cachix for this repo
This repo's NixOS flake lives under `nixos/`.
The workflow in `.github/workflows/cachix.yml` can build the `strixi-minaj`
system closure on GitHub Actions and push the results to a Cachix cache.
## One-time setup
1. Create a Cachix cache (on cachix.org).
2. Create a Cachix auth token with write access to that cache.
3. In the GitHub repo settings:
- Add a repo variable `CACHIX_CACHE_NAME` (the cache name).
- Add a repo secret `CACHIX_AUTH_TOKEN` (the write token).
After that, pushes to `master` will populate the cache.
## Using the cache locally
Option A: ad-hoc (non-declarative)
```sh
cachix use <your-cache-name>
```
Option B: declarative via flake `nixConfig` (recommended for NixOS)
1. Get the cache public key from the Cachix UI:
- Open `https://app.cachix.org/cache/<your-cache-name>#pull`
- Copy the `Public Key` value shown there.
2. Add it to `nixos/flake.nix` under `nixConfig.extra-substituters` and
`nixConfig.extra-trusted-public-keys`.
Note: `nixos/nix.nix` sets `nix.settings.accept-flake-config = true`, so the
flake `nixConfig` is honored during rebuilds.

View File

@@ -1,152 +0,0 @@
# Org-Agenda-API Consolidation Design
## Overview
Consolidate org-agenda-api container builds and fly.io deployment into the dotfiles repository. This eliminates the separate `colonelpanic-org-agenda-api` repo and provides:
- Container outputs available to NixOS machines directly
- Fly.io deployment from the same repo
- Fewer repos to maintain
- Cachix integration for faster builds
## Directory Structure
```
/home/imalison/dotfiles/
├── nixos/
│ ├── flake.nix # Main flake, adds container output
│ ├── org-agenda-api.nix # Existing tangling module (stays here)
│ └── ...
├── org-agenda-api/
│ ├── container.nix # Container build logic (mkContainer, etc.)
│ ├── configs/
│ │ ├── colonelpanic/
│ │ │ ├── custom-config.el
│ │ │ └── overrides.el (optional)
│ │ └── kat/
│ │ └── custom-config.el
│ ├── fly/
│ │ ├── fly.toml
│ │ ├── deploy.sh
│ │ └── config-{instance}.env
│ └── secrets/
│ ├── secrets.nix # agenix declarations
│ └── *.age # encrypted secrets
└── dotfiles/emacs.d/
└── org-config.org # Source of truth for org config
```
## Flake Integration
The main dotfiles flake at `/home/imalison/dotfiles/nixos/flake.nix` exposes container outputs:
```nix
outputs = inputs @ { self, nixpkgs, flake-utils, ... }:
{
nixosConfigurations = { ... }; # existing
} // flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
containerLib = import ../org-agenda-api/container.nix {
inherit pkgs system;
tangledConfig = (import ./org-agenda-api.nix {
inherit pkgs system;
inputs = inputs;
}).org-agenda-custom-config;
};
in {
packages = {
container-colonelpanic = containerLib.mkInstanceContainer "colonelpanic";
container-kat = containerLib.mkInstanceContainer "kat";
};
}
);
```
Build with: `nix build .#container-colonelpanic`
## Custom Elisp & Tangling
Single source of truth: `org-config.org` tangles to elisp files loaded by containers.
**What stays in custom-config.el (container-specific glue):**
- Path overrides (`/data/org` instead of `~/org`)
- Stubs for unavailable packages (`org-bullets-mode` no-op)
- Customize-to-setq format conversion
- Template conversion for org-agenda-api format
- Instance-specific settings
**Audit:** During implementation, verify no actual org logic is duplicated in custom-config.el.
## Cachix Integration
### Phase 1: Use upstream cache as substituter
Add to dotfiles flake's `nixConfig`:
```nix
nixConfig = {
extra-substituters = [
"https://org-agenda-api.cachix.org"
];
extra-trusted-public-keys = [
"org-agenda-api.cachix.org-1:PUBLIC_KEY_HERE"
];
};
```
Benefits:
- `container-base` (~500MB+ dependencies) fetched from cache
- Rebuilds only process the small custom config layer
### Phase 2 (future): Push custom builds
Set up GitHub Action or local push for colonelpanic-specific container builds.
## Fly.io Deployment
**What moves:**
- `fly.toml``dotfiles/org-agenda-api/fly/fly.toml`
- `deploy.sh``dotfiles/org-agenda-api/fly/deploy.sh`
- `configs/*/config.env``dotfiles/org-agenda-api/fly/config-{instance}.env`
- Agenix secrets → `dotfiles/org-agenda-api/secrets/`
**Deploy script changes:**
- Build path: `nix build "../nixos#container-${INSTANCE}"`
- Secrets path adjusts to new location
- Otherwise same logic
## Implementation Phases
### Phase 1: Pull latest & verify current state
- Pull latest changes in org-agenda-api and colonelpanic-org-agenda-api
- Build container, verify it works
- Fix any issues before restructuring
### Phase 2: Create dotfiles structure
- Create `/home/imalison/dotfiles/org-agenda-api/` directory
- Move container.nix logic (adapted from current colonelpanic-org-agenda-api flake)
- Move instance configs (colonelpanic/, kat/)
- Move fly.io deployment files
- Move agenix secrets
### Phase 3: Integrate with dotfiles flake
- Update `/home/imalison/dotfiles/nixos/flake.nix` to expose container outputs
- Add cachix substituter configuration
- Test build from dotfiles: `nix build .#container-colonelpanic`
### Phase 4: Verify deployment
- Test deploy.sh from new location
- Verify fly.io deployment works
- Run the container locally on a NixOS machine
### Phase 5: Audit & cleanup
- Review custom-config.el for any duplicated org logic
- Archive colonelpanic-org-agenda-api repo
- Update any references/documentation
## Repos Affected
- **dotfiles** - Receives container build + fly.io deployment
- **colonelpanic-org-agenda-api** - Becomes obsolete after migration
- **org-agenda-api** (upstream) - No changes, used as flake input

View File

@@ -43,13 +43,13 @@ Required behavior:
- Moving the focused window to the next empty workspace and following it is a
first-class operation.
- Normal workspaces are bounded to `1..9`.
- Workspace history is tracked per monitor.
- Last-workspace toggle uses the current monitor's workspace history.
- Workspace cycling works on the current monitor within the bounded workspace
set.
Important behavior:
- Workspace history is tracked per monitor.
- Last-workspace toggle uses the current monitor's workspace history.
- Workspace history cycling works on the current monitor within the bounded
workspace set.
- Swapping the current workspace contents with another workspace is available.
- Moving a window to an empty workspace on another monitor is available.
- Moving the focused window to another monitor without following keeps keyboard
@@ -62,6 +62,30 @@ Important behavior:
- Hidden/special workspaces are excluded from the status bar's normal workspace
list.
### Workspace History Cycling
Important behavior:
- The model is most-recently-used workspace switching, scoped to the monitor
where the action starts.
- Each monitor has its own ordered workspace history. The focused monitor's
history is not shared with other monitors.
- Only ordinary bounded workspaces are candidates. Special, scratchpad,
minimized, hidden, and out-of-range workspaces are excluded.
- Starting a cycle freezes the candidate list for that cycle. Previewing
workspaces while the cycle is active must not rewrite the history order.
- Starting a cycle previews the previous workspace for the current monitor.
- Repeating the forward cycle action continues farther back through that
monitor's frozen history.
- A reverse cycle action moves through the same frozen history in the opposite
direction.
- Releasing the initiating modifier key commits the currently previewed
workspace and updates history exactly once.
- A cancel path may return to the workspace where the cycle started.
This behavior is important for workflow continuity, but it is not a hard
requirement for a minimal daily-driver window manager.
## Directional Navigation
Required behavior:
@@ -74,6 +98,14 @@ Required behavior:
- Moving the focused window to an empty workspace on the monitor in a direction
remains required behavior, but it should not require an extra `Hyper`
modifier beyond `Shift`.
- `Super+w/a/s/d` focuses windows directionally.
- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally.
- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that
direction while preserving useful focus.
- `Super+Ctrl+Shift+w/a/s/d` moves the focused window to an empty workspace on
the monitor in that direction.
- `Hyper+w/a/s/d` focuses monitors directionally.
- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally.
- Directional focus in tabbed/fullscreen mode should cycle predictably through
windows even though their screen geometry overlaps.
@@ -82,14 +114,27 @@ Important behavior:
- Keyboard resize remains available, but it should not displace the directional
move-to-monitor binding.
## Pointer Focus
Required behavior:
- Focus-follows-mouse, or an equivalent pointer-driven focus model, is enabled.
- Moving the pointer over a managed window focuses that window without requiring
a click.
- Mouse-follows-focus is also enabled: keyboard or programmatic focus changes
move the pointer into the newly focused window.
## Layouts
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.
- Newly tiled windows are inserted near the currently focused tile, not
appended to the far end of the workspace.
- Removing windows dynamically redistributes all tiled windows evenly.
- Ordinary use should not require manually managing a split tree.
- Tabbed/fullscreen-style monocle layout is available.
@@ -100,6 +145,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 +157,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.
@@ -118,7 +165,8 @@ Nice behavior:
Required behavior:
- There is an expose-style way to inspect open windows or workspaces before
- There is a visual window overview for inspecting open windows before jumping.
- There is a visual workspace expose for inspecting normal workspaces before
jumping.
- There is a rofi-style window picker.
- Window picker entries show icons.
@@ -132,6 +180,15 @@ Required behavior:
Important behavior:
- Overview supports both "go" and "bring" workflows.
- Window overview and workspace expose are distinct surfaces, because window
selection and workspace selection are different navigation tasks.
- Window overview supports directional keyboard selection with the same
`w/a/s/d` spatial model as ordinary window focus.
- Window overview supports direct go, bring, and replace-window actions from the
selection UI.
- Workspace expose shows bounded normal workspaces, including empty workspaces,
with visible workspace numbers.
- Workspace expose can be opened in a bring-window-oriented mode when supported.
- Window switchers hide scratchpad windows unless the user is explicitly using a
scratchpad picker.
- Window switchers hide minimized windows unless the user is explicitly using a
@@ -143,10 +200,9 @@ Important behavior:
Required behavior:
- A named scratchpad exists for codex.
- 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 +291,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,19 +321,43 @@ Required behavior:
- `Super+p` opens the application launcher.
- `Super+Shift+p` opens the run menu.
- `Super+Shift+Return` opens a terminal.
- `Super+Tab` opens the overview.
- `Super+Shift+Tab` opens the overview in bring-window mode when supported.
- `Super+q` reloads the window manager config.
- `Super+Shift+c` closes the focused window.
- `Super+Shift+q` exits the window manager session.
- `Super+x` opens the command picker with `rofi_command.sh`.
- `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.
- `Hyper+e` focuses the next empty workspace.
- `Hyper+1` toggles inactive-window opacity reduction for the focused window.
- `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+Tab` opens the visual window overview.
- `Super+Shift+Tab` opens the visual window overview scoped to non-visible
windows or bring-window mode when supported.
- `Alt+Tab` opens the visual workspace expose.
- `Alt+Shift+Tab` opens the visual workspace expose in bring-window mode when
supported.
- Within visual window overview, `w/a/s/d`, `h/j/k/l`, and arrow keys move the
selection directionally.
- Within visual window overview, `Return`, `Space`, `g`, or `f` activates the
selected window.
- Within visual window overview, `b`, `Shift+Return`, or `Shift+Space` brings
the selected window to the current workspace.
- Within visual window overview, `Shift+b` replaces the focused window with the
selected window when supported.
- Within visual window overview, `Escape` or `q` closes the overview.
- `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:
@@ -305,10 +387,9 @@ Required behavior:
Required behavior:
- `Super+Alt+c` toggles the codex scratchpad.
- `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.
@@ -317,7 +398,6 @@ Required behavior:
Important behavior:
- `Super+Alt+grave` toggles the dropdown terminal scratchpad.
- `Super+Alt+c` raises or starts the browser.
- `Super+Alt+Return` enters the minimized-window picker or restores minimized
windows, depending on environment support.
- `Super+Alt` is reserved for app-specific raise/spawn, scratchpad, and
@@ -332,8 +412,8 @@ Required behavior:
- `Hyper+p` opens the password picker with `rofi-pass`.
- `Hyper+h` opens the screenshot tool with the compositor/session-appropriate
screenshot command.
- `Hyper+c` opens a shell command prompt with `shell_command.sh`.
- `Hyper+x` opens the command picker with `rofi_command.sh`.
- `Hyper+c` opens the Codex launcher with `rofi_tmcodex.sh`.
- `Hyper+Shift+c` opens the Codex launcher with `tmcodex resume`.
- `Hyper+k` opens the process killer with `rofi_kill_process.sh`.
- `Hyper+Shift+k` opens the kill-all/process-tree killer with
`rofi_kill_all.sh`.

View File

@@ -3,16 +3,18 @@
## Multiplexer session titling
- If the `TMUX` or `ZELLIJ` environment variable is set, treat this chat as the controller for the current tmux or zellij session.
- Use `set_multiplexer_title '<project> - <task>'` to update the title. The command detects tmux vs. zellij internally, prefers tmux when both are present, and no-ops outside a multiplexer.
- Maintain a session/window/pane title that updates when the task focus changes substantially.
- Prefer automatic titling: infer a concise <task> from the current user request and context without asking.
- Maintain a session/window/pane title that describes the durable purpose of the overall exchange.
- Prefer automatic titling: infer a concise <task> from the current user request and the existing chat context without asking.
- Choose holistic titles over granular turn summaries. The title should answer "what has this chat been for?" rather than describe the latest command, substep, clarification, or follow-up message.
- Preserve the existing <task> when the new user turn is a continuation, status check, refinement, or implementation detail within the same broader objective.
- Title format: "<project> - <task>".
- <project> is the basename of the current project directory.
- Prefer git repo root basename if available; otherwise use basename of the current working directory.
- <task> is a short, user-friendly description of what we are doing.
- Ask for a short descriptive <task> only when the task is ambiguous or you are not confident in an inferred title.
- When the task changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
- When the broader objective changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
- When a title is provided or updated, immediately run `set_multiplexer_title '<project> - <task>'`; do not call raw tmux or zellij rename commands unless debugging the helper itself.
- For Claude Code sessions, a UserPromptSubmit hook will also update titles automatically based on the latest prompt.
- For Claude Code sessions, a UserPromptSubmit hook may initialize titles automatically from the first substantive prompt, but it should not keep overwriting an established same-project title with the latest prompt.
## Pane usage
- Do not create extra panes or windows unless the user asks.

View File

@@ -16,10 +16,13 @@ sys.stdout.write(cwd)
sys.stdout.write("\0")
sys.stdout.write(prompt)
sys.stdout.write("\0")
sys.stdout.write(str(data.get("session_id") or ""))
sys.stdout.write("\0")
PY
)
cwd="${parsed[0]:-}"
prompt="${parsed[1]:-}"
session_id="${parsed[2]:-}"
if [[ -z "${cwd}" ]]; then
cwd="$PWD"
@@ -46,6 +49,17 @@ if [[ -z "$task" ]]; then
task="work"
fi
explicit_retitle=false
case "$lower" in
"new task:"*|"new topic:"*|"switch topic:"*|"switch context:"*|"rename title:"*|"title:"*)
explicit_retitle=true
task=$(printf '%s' "$prompt_first_line" | sed -E 's/^[^:]+:[[:space:]]*//')
if [[ -z "$task" ]]; then
task="work"
fi
;;
esac
# Trim to a reasonable length for multiplexer UI labels.
if [[ ${#task} -gt 60 ]]; then
task="${task:0:57}..."
@@ -53,9 +67,42 @@ fi
title="$project - $task"
# The hook only sees the newest prompt, not the full conversation. Avoid
# degrading a useful same-project title into a granular follow-up summary.
if [[ -n "${TMUX:-}" ]]; then
multiplexer="tmux"
elif [[ -n "${ZELLIJ:-}" ]]; then
multiplexer="zellij"
else
multiplexer=""
fi
hook_state_file=""
if [[ -n "$multiplexer" ]]; then
state_dir="${HOME}/.agents/state"
if [[ -n "$session_id" ]]; then
safe_session_id=$(printf '%s' "$session_id" | tr -c '[:alnum:]_.-' '_')
hook_state_file="${state_dir}/${multiplexer}-title-hook-${safe_session_id}"
else
hook_state_file="${state_dir}/${multiplexer}-title"
fi
if [[ -f "$hook_state_file" ]]; then
established_title=$(cat "$hook_state_file" 2>/dev/null || true)
if [[ "$established_title" == "$project - "* && "$established_title" != "$title" && "$explicit_retitle" != true ]]; then
exit 0
fi
fi
fi
if command -v set_multiplexer_title >/dev/null 2>&1; then
set_multiplexer_title "$title"
else
hook_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
"$hook_dir/../../lib/functions/set_multiplexer_title" "$title"
fi
if [[ -n "$hook_state_file" ]]; then
mkdir -p "$(dirname "$hook_state_file")"
printf '%s' "$title" > "$hook_state_file"
fi

View File

@@ -112,6 +112,8 @@ Recommended sequence:
Machine-specific note:
- Project-local `.worktrees/*/target` directories are common cleanup wins on this machine and are easy to miss with the old hard-coded workflow.
- `cargo-sweep` is installed through the NixOS `code.nix` package set, but stale manually-installed binaries under `~/.cargo/bin` can shadow `/run/current-system/sw/bin/cargo-sweep`. If `cargo sweep` fails with a missing loader or `No such file or directory`, run `type -a cargo-sweep` and remove the stale `~/.cargo/bin/cargo-sweep` entry.
- `nixos/imalison.nix` defines a daily user timer, `cargo-sweep-rust-targets.timer`, that runs `cargo-sweep sweep -r --hidden --maxsize 15GB` across `/home/imalison/Projects`, `/home/imalison/org`, and `/home/imalison/dotfiles`.
## Step 4: Investigation with `ncdu` and `du`

View File

@@ -9,25 +9,28 @@ How the taffybar ecosystem packages are consumed by the NixOS configuration thro
See also: `taffybar-ecosystem-release` for the package dependency graph, release workflow, and Hackage publishing.
## The Three-Layer Flake Chain
## The Flake Chain
The NixOS system build pulls in taffybar through three nested flake.nix files:
The NixOS system build pulls in taffybar through the personal
`imalison-taffybar` config flake. The top-level NixOS flake should not declare
or override a direct `taffybar` input; the config flake owns its taffybar
version.
```
nixos/flake.nix (top `just switch` reads this)
── taffybar path:.../taffybar/taffybar
│ ├── imalison-taffybar path:../dotfiles/config/taffybar
│ └── gtk-sni-tray, gtk-strut, etc. (GitHub inputs)
nixos/flake.nix (top - `just switch` reads this)
── imalison-taffybar path:../dotfiles/config/taffybar
dotfiles/config/taffybar/flake.nix (middle imalison-taffybar config)
dotfiles/config/taffybar/flake.nix (middle - imalison-taffybar config)
│ ├── taffybar path:.../taffybar/taffybar
│ └── gtk-sni-tray, gtk-strut, etc. (GitHub inputs)
dotfiles/config/taffybar/taffybar/flake.nix (bottom taffybar library)
dotfiles/config/taffybar/taffybar/flake.nix (bottom - taffybar library)
│ └── gtk-sni-tray, gtk-strut, etc. (flake = false GitHub inputs)
```
All three flakes declare their own top-level inputs for the ecosystem packages and use `follows` to keep versions consistent within each layer.
The NixOS layer may make `imalison-taffybar` follow shared inputs such as
`nixpkgs`, `flake-utils`, and `xmonad`, but it should not set
`imalison-taffybar.inputs.taffybar.follows`.
## Why Bottom-Up Updates Matter
@@ -43,14 +46,14 @@ cd ~/.config/taffybar/taffybar && nix flake update <pkg>
cd ~/.config/taffybar && nix flake update <pkg> taffybar
# Top:
cd ~/dotfiles/nixos && nix flake update <pkg> imalison-taffybar taffybar
cd ~/dotfiles/nixos && nix flake update imalison-taffybar
```
Not every change requires touching all three layers. Think about which flake.lock files actually contain stale references:
- Changed **taffybar itself** — it's the bottom layer, so start at the middle (`nix flake update taffybar`) then the top.
- Changed **taffybar itself** — it's owned by the config flake, so start at the middle (`nix flake update taffybar`) then update `imalison-taffybar` at the top.
- Changed a **leaf ecosystem package** (e.g. gtk-strut) — start at the bottom since taffybar's flake.lock references it, then cascade up.
- The nixos flake also has **direct GitHub inputs** for ecosystem packages with `follows` overrides. Updating those at the top level may be sufficient if nothing changed in the middle/bottom flake.lock files themselves.
- The nixos flake can still have unrelated direct inputs such as `kanshi-sni`. Do not add a top-level `taffybar` input just to control the config flake's taffybar source.
## Rebuilding

View File

@@ -16,5 +16,6 @@
"agent-browser@agent-browser": true
},
"effortLevel": "high",
"skipDangerousModePermissionPrompt": true
"skipDangerousModePermissionPrompt": true,
"remoteControlAtStartup": true
}

View File

@@ -3,3 +3,6 @@
!AGENTS.md
!config.toml
!skills
# Legacy generated/local Codex state under this repo stays ignored. Active
# host-local Codex fragments now live under ~/.codex.

View File

@@ -1,148 +1,12 @@
model = "gpt-5.5"
model_reasoning_effort = "high"
service_tier = "fast"
personality = "pragmatic"
suppress_unstable_features_warning = true
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. Home Manager regenerates ~/.codex/config.toml from
# this file, ~/.codex/config.local.toml, and Codex-owned sections preserved in
# ~/.codex/config.local-state.toml.
[mcp_servers.chrome-devtools]
command = "npx"
@@ -159,16 +23,9 @@ url = "https://developers.openai.com/mcp"
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"
goals = true
fast_mode = true
remote_control = true
[plugins."google-calendar@openai-curated"]
enabled = true
@@ -196,6 +53,3 @@ enabled = true
[plugins."browser-use@openai-bundled"]
enabled = true
[tui.model_availability_nux]
"gpt-5.5" = 4

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
local function config_dir()
local source = debug.getinfo(1, "S").source
if source:sub(1, 1) == "@" then
source = source:sub(2)
end
local dir = source:match("^(.*)/[^/]*$")
if dir and dir ~= "" then
return dir
end
return "."
end
local base_dir = config_dir()
package.path = table.concat({
base_dir .. "/?.lua",
base_dir .. "/?/init.lua",
package.path,
}, ";")
local modules = {
"hyprland.state",
"hyprland.scratchpads",
"hyprland.core",
"hyprland.layouts",
"hyprland.windows",
"hyprland.settings",
"hyprland.binds",
"hyprland.events",
}
for _, module in ipairs(modules) do
package.loaded[module] = nil
end
local ctx = require(modules[1])
setmetatable(ctx, { __index = _G })
for i = 2, #modules do
require(modules[i]).setup(ctx)
end

View File

@@ -0,0 +1,332 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function desc(description, opts)
local bind_opts = {}
for key, value in pairs(opts or {}) do
bind_opts[key] = value
end
bind_opts.description = description
return bind_opts
end
local function setup_launcher_and_app_bindings()
bind(main_mod .. " + P", exec(launcher_command), desc("Open application launcher"))
bind(main_mod .. " + SHIFT + P", exec(run_menu), desc("Open command runner"))
bind(main_mod .. " + SHIFT + Return", exec(terminal), desc("Open terminal"))
bind(main_mod .. " + E", exec("emacsclient --eval '(emacs-everywhere)'"), desc("Open Emacs Everywhere"))
bind(main_mod .. " + V", exec("wl-paste --no-newline | ydotool type --file -"), desc("Type clipboard contents"))
end
local function setup_shell_and_session_bindings()
bind(hyper .. " + SHIFT + N", exec(shell_ui_command .. " control-center"), desc("Open control center"))
bind(hyper .. " + CTRL + N", exec(shell_ui_command .. " settings"), desc("Open system settings"))
bind(main_mod .. " + Q", exec("hyprctl reload"), desc("Reload Hyprland"))
bind(main_mod .. " + R", exec("hyprctl reload"), desc("Reload Hyprland"))
bind(hyper .. " + SHIFT + L", exec("hyprlock"), desc("Lock screen"))
bind(hyper .. " + SHIFT + V", toggle_visual_performance_mode, desc("Toggle Hyprland performance mode"))
bind(hyper .. " + slash", function()
hl.exec_cmd("toggle_taffybar")
refresh_monitor_reserved_cache(0.25)
refresh_active_scratchpad_geometries_later(600)
end, desc("Toggle taffybar"))
end
local function setup_audio_media_bindings()
bind(main_mod .. " + I", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
bind(main_mod .. " + K", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
bind(main_mod .. " + U", exec("set_volume --toggle-mute"), desc("Toggle mute"))
bind(main_mod .. " + semicolon", exec("playerctl play-pause"), desc("Play or pause media"))
bind(main_mod .. " + L", exec("playerctl next"), desc("Skip to next media track"))
bind(main_mod .. " + J", exec("playerctl previous"), desc("Skip to previous media track"))
bind("XF86AudioPlay", exec("playerctl play-pause"), desc("Play or pause media"))
bind("XF86AudioPause", exec("playerctl play-pause"), desc("Play or pause media"))
bind("XF86AudioNext", exec("playerctl next"), desc("Skip to next media track"))
bind("XF86AudioPrev", exec("playerctl previous"), desc("Skip to previous media track"))
bind("XF86AudioRaiseVolume", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
bind("XF86AudioLowerVolume", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
bind("XF86AudioMute", exec("set_volume --toggle-mute"), desc("Toggle mute"))
bind(hyper .. " + O", exec("/home/imalison/dotfiles/dotfiles/lib/functions/rofi_paswitch"), desc("Open PulseAudio output switcher"))
bind(hyper .. " + SHIFT + O", exec("/home/imalison/dotfiles/dotfiles/lib/bin/kef-optical"), desc("Switch KEF speakers to optical input"))
end
local function setup_display_wallpaper_and_capture_bindings()
bind("XF86MonBrightnessUp", exec("brightness.sh up"), desc("Raise display brightness", { repeating = true }))
bind("XF86MonBrightnessDown", exec("brightness.sh down"), desc("Lower display brightness", { repeating = true }))
bind("Print", exec("flameshot gui"), desc("Take screenshot"))
bind(hyper .. " + H", exec("flameshot gui"), desc("Take screenshot"))
bind(hyper .. " + backslash", exec("/home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle"), desc("Toggle monitor input"))
bind(hyper .. " + comma", exec("rofi_wallpaper.sh"), desc("Open wallpaper menu"))
bind(hyper .. " + SHIFT + comma", exec("/home/imalison/dotfiles/dotfiles/lib/bin/neowall-wallpaper toggle"), desc("Toggle neowall wallpaper"))
end
local function setup_rofi_and_tool_bindings()
bind(main_mod .. " + X", exec("rofi_command.sh"), desc("Open command menu"))
bind(hyper .. " + V", exec([[cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy]]), desc("Open clipboard history"))
bind(hyper .. " + P", exec("rofi-pass"), desc("Open password menu"))
bind(hyper .. " + C", exec("rofi_tmcodex.sh"), desc("Open Codex session menu"))
bind(hyper .. " + SHIFT + C", exec("rofi_tmcodex.sh resume"), desc("Resume Codex session"))
bind(hyper .. " + L", exec("hypr_rofi_layout"), desc("Open Hyprland layout menu"))
bind(hyper .. " + K", exec("rofi_kill_process.sh"), desc("Open process kill menu"))
bind(hyper .. " + SHIFT + K", exec("rofi_kill_all.sh"), desc("Open kill-all menu"))
bind(hyper .. " + R", exec("rofi_systemd_mono"), desc("Open systemd unit menu"))
bind(hyper .. " + X", exec("hypr_rofi_action"), desc("Open Hyprland action menu"))
bind(hyper .. " + I", exec("rofi_select_input.hs"), desc("Open input selection menu"))
bind(hyper .. " + Y", exec("rofi_agentic_skill"), desc("Open agentic skill menu"))
end
local function setup_external_command_bindings()
setup_launcher_and_app_bindings()
setup_shell_and_session_bindings()
setup_audio_media_bindings()
setup_display_wallpaper_and_capture_bindings()
setup_rofi_and_tool_bindings()
end
local function setup_window_overview_bindings()
bind(main_mod .. " + SHIFT + C", hl.dsp.window.close(), desc("Close active window"))
bind(main_mod .. " + SHIFT + Q", hl.dsp.exit(), desc("Exit Hyprland"))
bind(main_mod .. " + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + Tab", hyprwinview({
action = "show",
include_current_workspace = false,
start_in_filter_mode = true,
default_action = "bring",
}), desc("Show all-workspace window overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + slash", hyprwinview({ action = "toggle-filter" }), desc("Toggle window overview filter", overview_bind_opts))
bind("ALT + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
bind("ALT + SHIFT + Tab", hyprexpo("on"), desc("Open hyprexpo workspace overview", overview_bind_opts))
bind(main_mod .. " + G", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "select",
}), desc("Show window overview", overview_bind_opts))
bind(main_mod .. " + B", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring",
}), desc("Bring window from overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + B", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring-replace",
}), desc("Replace active window from overview", overview_bind_opts))
end
local function setup_window_focus_and_move_bindings()
bind(main_mod .. " + W", function()
focus_direction("up")
end, desc("Focus window above"))
bind(main_mod .. " + S", function()
focus_direction("down")
end, desc("Focus window below"))
bind(main_mod .. " + A", function()
focus_direction("left")
end, desc("Focus window to the left"))
bind(main_mod .. " + D", function()
focus_direction("right")
end, desc("Focus window to the right"))
bind(main_mod .. " + SHIFT + W", function()
swap_direction("up")
end, desc("Swap active window upward"))
bind(main_mod .. " + SHIFT + S", function()
swap_direction("down")
end, desc("Swap active window downward"))
bind(main_mod .. " + SHIFT + A", function()
swap_direction("left")
end, desc("Swap active window left"))
bind(main_mod .. " + SHIFT + D", function()
swap_direction("right")
end, desc("Swap active window right"))
bind(main_mod .. " + CTRL + W", function()
move_window_to_monitor("u", false)
end, desc("Move window to monitor above"))
bind(main_mod .. " + CTRL + S", function()
move_window_to_monitor("d", false)
end, desc("Move window to monitor below"))
bind(main_mod .. " + CTRL + A", function()
move_window_to_monitor("l", false)
end, desc("Move window to monitor on the left"))
bind(main_mod .. " + CTRL + D", function()
move_window_to_monitor("r", false)
end, desc("Move window to monitor on the right"))
bind(main_mod .. " + CTRL + SHIFT + W", function()
move_window_to_empty_workspace_on_monitor("u")
end, desc("Move window to empty workspace on monitor above"))
bind(main_mod .. " + CTRL + SHIFT + S", function()
move_window_to_empty_workspace_on_monitor("d")
end, desc("Move window to empty workspace on monitor below"))
bind(main_mod .. " + CTRL + SHIFT + A", function()
move_window_to_empty_workspace_on_monitor("l")
end, desc("Move window to empty workspace on left monitor"))
bind(main_mod .. " + CTRL + SHIFT + D", function()
move_window_to_empty_workspace_on_monitor("r")
end, desc("Move window to empty workspace on right monitor"))
end
local function setup_submap_bindings()
hl.define_submap("swap-workspace", function()
for i = 1, 9 do
local workspace_id = i
bind(tostring(i), function()
swap_current_workspace_with(workspace_id)
dispatch(hl.dsp.submap("reset"))
end, desc("Swap current workspace with workspace " .. workspace_id))
end
bind("Escape", hl.dsp.submap("reset"), desc("Exit workspace swap mode"))
bind("catchall", hl.dsp.submap("reset"), desc("Exit workspace swap mode"))
end)
hl.define_submap("window-picker", function()
for i = 1, 9 do
local index = i
bind(tostring(i), function()
activate_window_picker_candidate(index)
end, desc("Activate window picker candidate " .. index))
end
bind("Escape", hl.dsp.submap("reset"), desc("Exit window picker"))
bind("catchall", hl.dsp.submap("reset"), desc("Exit window picker"))
end)
end
local function setup_window_resize_and_monitor_bindings()
bind(mod_alt .. " + SHIFT + W", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), desc("Shrink window height upward", { repeating = true }))
bind(mod_alt .. " + SHIFT + S", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), desc("Grow window height downward", { repeating = true }))
bind(mod_alt .. " + SHIFT + A", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), desc("Shrink window width leftward", { repeating = true }))
bind(mod_alt .. " + SHIFT + D", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), desc("Grow window width rightward", { repeating = true }))
bind(hyper .. " + W", hl.dsp.focus({ monitor = "u" }), desc("Focus monitor above"))
bind(hyper .. " + S", hl.dsp.focus({ monitor = "d" }), desc("Focus monitor below"))
bind(hyper .. " + A", hl.dsp.focus({ monitor = "l" }), desc("Focus monitor on the left"))
bind(hyper .. " + D", hl.dsp.focus({ monitor = "r" }), desc("Focus monitor on the right"))
bind(hyper .. " + SHIFT + W", function()
move_window_to_monitor("u", true)
end, desc("Move window to monitor above and follow"))
bind(hyper .. " + SHIFT + S", function()
move_window_to_monitor("d", true)
end, desc("Move window to monitor below and follow"))
bind(hyper .. " + SHIFT + A", function()
move_window_to_monitor("l", true)
end, desc("Move window to left monitor and follow"))
bind(hyper .. " + SHIFT + D", function()
move_window_to_monitor("r", true)
end, desc("Move window to right monitor and follow"))
end
local function setup_layout_and_window_state_bindings()
bind(main_mod .. " + Space", cycle_layout_or_restore_tabbed_group, desc("Cycle workspace layout"))
bind(main_mod .. " + SHIFT + Space", force_columns_layout, desc("Force columns layout"))
bind(main_mod .. " + CTRL + Space", gather_workspace_into_tabbed_group, desc("Gather workspace into tabbed group"))
bind(main_mod .. " + bracketright", monocle_next, desc("Focus next monocle window"))
bind(main_mod .. " + bracketleft", monocle_prev, desc("Focus previous monocle window"))
bind(main_mod .. " + T", hl.dsp.window.float({ action = "disable" }), desc("Tile active window"))
bind(main_mod .. " + O", toggle_pinned_active_window, desc("Toggle pinned active window"))
bind(main_mod .. " + M", minimize_active_window, desc("Minimize active window"))
bind(main_mod .. " + SHIFT + M", restore_last_minimized, desc("Restore last minimized window"))
bind(main_mod .. " + CTRL + SHIFT + M", function()
enter_window_picker("minimized")
end, desc("Pick minimized window to restore"))
bind(main_mod .. " + SHIFT + equal", schedule_nstack_count_update, desc("Update nstack window count"))
bind(main_mod .. " + CTRL + M", hl.dsp.window.toggle_swallow(), desc("Toggle window swallowing"))
bind(main_mod .. " + SHIFT + E", function()
move_to_next_empty_workspace(true)
end, desc("Move to next empty workspace"))
bind(main_mod .. " + CTRL + E", function()
move_to_next_empty_workspace(false)
end, desc("Move window to next empty workspace"))
bind(main_mod .. " + apostrophe", focus_next_class, desc("Focus next window class"))
bind(hyper .. " + 1", toggle_inactive_opacity_for_active_window, desc("Toggle inactive opacity reduction for active window"))
bind(mod_alt .. " + W", show_active_window_info, desc("Show active window info"))
end
local function setup_scratchpad_bindings()
bind(main_mod .. " + SHIFT + X", hl.dsp.workspace.toggle_special("NSP"), desc("Toggle NSP special workspace"))
bind(mod_alt .. " + C", function()
toggle_scratchpad("codex")
end, desc("Toggle Codex scratchpad"))
bind(mod_alt .. " + E", function()
toggle_scratchpad("element")
end, desc("Toggle Element scratchpad"))
bind(mod_alt .. " + H", function()
toggle_scratchpad("htop")
end, desc("Toggle htop scratchpad"))
bind(mod_alt .. " + K", function()
toggle_scratchpad("slack")
end, desc("Toggle Slack scratchpad"))
bind(mod_alt .. " + M", function()
toggle_scratchpad("messages")
end, desc("Toggle Messages scratchpad"))
bind(mod_alt .. " + S", function()
toggle_scratchpad("spotify")
end, desc("Toggle Spotify scratchpad"))
bind(mod_alt .. " + T", function()
toggle_scratchpad("transmission")
end, desc("Toggle Transmission scratchpad"))
bind(mod_alt .. " + V", function()
toggle_scratchpad("volume")
end, desc("Toggle volume scratchpad"))
bind(mod_alt .. " + grave", function()
toggle_scratchpad("dropdown")
end, desc("Toggle dropdown scratchpad"))
bind(mod_alt .. " + Space", minimize_other_classes, desc("Minimize other window classes"))
bind(mod_alt .. " + SHIFT + Space", restore_focused_class, desc("Restore focused window class"))
bind(mod_alt .. " + Return", restore_all_minimized, desc("Restore all minimized windows"))
end
local function setup_workspace_bindings()
for i = 1, 9 do
local workspace = tostring(i)
bind(main_mod .. " + " .. workspace, hl.dsp.focus({ workspace = workspace, on_current_monitor = true }), desc("Focus workspace " .. workspace))
bind(main_mod .. " + SHIFT + " .. workspace, hl.dsp.window.move({ workspace = workspace, follow = false }), desc("Move window to workspace " .. workspace))
bind(main_mod .. " + CTRL + " .. workspace, function()
dispatch(hl.dsp.window.move({ workspace = workspace, follow = false }))
dispatch(hl.dsp.focus({ workspace = workspace, on_current_monitor = true }))
end, desc("Move window to workspace " .. workspace .. " and follow"))
end
bind(main_mod .. " + backslash", workspacehistory("cycle", 1), desc("Cycle to next workspace in history"))
bind(main_mod .. " + slash", workspacehistory("cycle", -1), desc("Cycle to previous workspace in history"))
bind(main_mod .. " + Escape", workspacehistory("cancel"), desc("Cancel workspace history cycle"))
bind(main_mod .. " + Z", hl.dsp.focus({ monitor = "+1" }), desc("Focus next monitor"))
bind(main_mod .. " + SHIFT + Z", hl.dsp.window.move({ monitor = "+1" }), desc("Move window to next monitor"))
bind(main_mod .. " + mouse_down", function()
cycle_workspace(1)
end, desc("Cycle to next workspace"))
bind(main_mod .. " + mouse_up", function()
cycle_workspace(-1)
end, desc("Cycle to previous workspace"))
bind(hyper .. " + E", focus_next_empty_workspace, desc("Focus next empty workspace"))
bind(hyper .. " + 5", enter_workspace_swap_mode, desc("Enter workspace swap mode"))
bind(hyper .. " + G", gather_focused_class, desc("Gather focused window class"))
bind(hyper .. " + SHIFT + backslash", workspacehistory("debug"), desc("Show workspace history debug info"))
end
local function setup_mouse_bindings()
bind(main_mod .. " + mouse:272", float_and_drag_active_window, desc("Float and drag active window"))
bind(main_mod .. " + mouse:273", float_and_resize_active_window, desc("Float and resize active window"))
end
local function setup_internal_window_manager_bindings()
setup_window_overview_bindings()
setup_window_focus_and_move_bindings()
setup_submap_bindings()
setup_window_resize_and_monitor_bindings()
setup_layout_and_window_state_bindings()
setup_scratchpad_bindings()
setup_workspace_bindings()
setup_mouse_bindings()
end
setup_external_command_bindings()
setup_internal_window_manager_bindings()
end
return M

View File

@@ -0,0 +1,615 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function command_line_contains(needle)
local command_line = io.open("/proc/self/cmdline", "rb")
if not command_line then
return false
end
local contents = command_line:read("*a") or ""
command_line:close()
return contents:find(needle, 1, true) ~= nil
end
verify_config = command_line_contains("--verify-config")
dev_session = os.getenv("IMALISON_HYPRLAND_DEV_SESSION") == "1"
local function exec(command)
return hl.dsp.exec_cmd(command)
end
local function dispatch(dispatcher)
return hl.dispatch(dispatcher)
end
local action_registry = {}
local function action_text(value)
return tostring(value or ""):gsub("[\t\r\n]", " "):gsub(" +", " "):match("^%s*(.-)%s*$")
end
local function action_registry_path()
local runtime_dir = os.getenv("XDG_RUNTIME_DIR") or "/tmp"
return runtime_dir .. "/hyprland-actions.tsv"
end
local function register_action(keys, dispatcher, opts)
local description = opts and opts.description
if not description or description == "" then
return
end
local id = tostring(#action_registry + 1)
action_registry[#action_registry + 1] = {
id = id,
keys = action_text(keys),
description = action_text(description),
dispatcher = dispatcher,
}
end
local function bind(keys, dispatcher, opts)
hl.bind(keys, dispatcher, opts)
register_action(keys, dispatcher, opts)
end
_G.im_hyprland_write_actions = function()
local actions_file = io.open(action_registry_path(), "w")
if not actions_file then
return
end
for _, action in ipairs(action_registry) do
actions_file:write(action.id, "\t", action.description, "\t", action.keys, "\n")
end
actions_file:close()
end
_G.im_hyprland_run_action = function(id)
local action = action_registry[tonumber(id)]
if not action then
return
end
if type(action.dispatcher) == "function" then
action.dispatcher()
else
dispatch(action.dispatcher)
end
end
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function overview_trace(label)
local enabled = io.open(overview_trace_enabled_path, "r")
if not enabled then
return
end
enabled:close()
local trace = io.open(overview_trace_path, "a")
if trace then
trace:write(os.date("%Y-%m-%d %H:%M:%S "), label, "\n")
trace:close()
end
end
local function window_selector(window)
if not window or not window.address then
return nil
end
return "address:" .. tostring(window.address)
end
local function hyprexpo_call(method, arg)
return function()
overview_trace("hyprexpo:" .. method .. (arg and (" " .. tostring(arg)) or ""))
if hl.plugin and hl.plugin.hyprexpo and hl.plugin.hyprexpo[method] then
hl.plugin.hyprexpo[method](arg)
else
hl.notification.create({
text = "hyprexpo is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
end
local function hyprexpo(action)
return hyprexpo_call("expo", action or "toggle")
end
local function hyprwinview(action)
return function()
local label = "hyprwinview"
if type(action) == "table" and action.action then
label = label .. " " .. tostring(action.action)
elseif type(action) ~= "table" and action ~= nil then
label = label .. " " .. tostring(action)
end
local function invoke()
overview_trace(label)
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.overview then
hl.plugin.hyprwinview.overview(action)
else
hl.notification.create({
text = "hyprwinview is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
invoke()
end
end
local function workspacehistory(action, arg)
return function()
if hl.plugin and hl.plugin.workspacehistory and hl.plugin.workspacehistory[action] then
hl.plugin.workspacehistory[action](arg)
else
hl.notification.create({
text = "workspacehistory is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
end
local function apply_nstack_config()
if verify_config or not enable_nstack or not configure_nstack_plugin_from_lua then
return
end
hl.config({
plugin = {
nstack = {
layout = {
orientation = "left",
new_on_top = false,
new_near_focused = true,
new_is_master = false,
no_gaps_when_only = true,
special_scale_factor = 0.8,
inherit_fullscreen = true,
stacks = 1,
center_single_master = false,
mfact = 0.0,
single_mfact = 1.0,
},
},
},
})
end
local function apply_hyprexpo_config()
if verify_config or not enable_hyprexpo then
return
end
hl.config({
plugin = {
hyprexpo = {
columns = 3,
gap_size = 5,
gap_size_outer = 0,
bg_col = 0xff111111,
workspace_method = "first 1",
skip_empty = false,
max_workspace = max_workspace,
gesture_distance = 200,
keynav_wrap_h = 1,
keynav_wrap_v = 1,
keynav_reading_order = 0,
border_width = 2,
border_color_current = "rgb(66ccff)",
border_color_focus = "rgb(edb443)",
border_color_hover = "rgb(aabbcc)",
tile_rounding = 5,
tile_rounding_power = 2.0,
label_enable = 1,
label_font_size = 28,
label_text_mode = "id",
label_position = "center",
label_offset_x = 6,
label_offset_y = 6,
selection_label_enable = 0,
label_show = "always",
label_color_default = 0xffffffff,
label_color_hover = 0xffeeeeee,
label_color_focus = 0xffedb443,
label_color_current = 0xff66ccff,
label_bg_enable = 1,
label_bg_color = 0xcc000000,
label_bg_rounding = 10,
label_padding = 12,
label_font_bold = 1,
label_pixel_snap = 1,
},
},
})
end
local function apply_hyprwinview_config()
if verify_config or not enable_hyprwinview then
return
end
hl.config({
plugin = {
hyprwinview = {
gap_size = 24,
margin = 48,
background = "rgba(10101400)",
background_blur = 1,
border_col = "rgba(ffffff33)",
hover_border_col = "rgba(66ccffee)",
border_size = 3,
window_order = "application",
keys_default_action = "return,enter,space,g,f",
keys_filter_toggle = "/",
show_app_icon = 1,
app_icon_size = 48,
app_icon_theme_source = "auto",
app_icon_position = "bottom right",
app_icon_margin_x = 12,
app_icon_margin_y = 12,
app_icon_margin_relative_x = 0.0,
app_icon_margin_relative_y = 0.0,
app_icon_offset_x = 0,
app_icon_offset_y = 0,
app_icon_backplate_col = "rgba(00000066)",
app_icon_backplate_padding = 6,
show_window_text = 1,
window_text_font = "Sans",
window_text_size = 14,
window_text_color = "rgba(ffffffff)",
window_text_backplate_col = "rgba(00000099)",
window_text_padding = 6,
filter_animation_ms = 140,
animation = "workspace_zoom",
animation_in_ms = 280,
animation_out_ms = 220,
animation_speed = 1.0,
animation_scale = 0.94,
animation_stagger_ms = 16,
animation_stagger_max_ms = 120,
},
},
})
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.configure then
hl.plugin.hyprwinview.configure({
keys = {
left = { "a", "h", "left" },
right = { "d", "l", "right" },
up = { "w", "k", "up" },
down = { "s", "j", "down" },
default_action = { "return", "enter", "space", "g", "f" },
bring = { "b", "shift+return", "shift+space" },
bring_replace = { "shift + b" },
close = { "escape", "q" },
filter_toggle = { "/" },
},
})
end
end
local function active_workspace()
return hl.get_active_workspace()
end
local function active_workspace_id()
local workspace = active_workspace()
if workspace and type(workspace.id) == "number" and workspace.id >= 1 then
return math.min(max_workspace, math.max(1, workspace.id))
end
return 1
end
local function workspace_key(workspace)
workspace = workspace or active_workspace()
if workspace and workspace.id then
return tostring(workspace.id)
end
return tostring(active_workspace_id())
end
local function current_workspace_layout()
return workspace_layouts[workspace_key()] or columns_layout
end
local function write_layout_state()
local runtime_dir = os.getenv("XDG_RUNTIME_DIR")
if not runtime_dir then
return
end
local file = io.open(runtime_dir .. "/hyprland-layout-state", "w")
if not file then
return
end
local workspace = active_workspace()
file:write("workspace=", workspace_key(workspace), "\n")
file:write("layout=", current_layout, "\n")
for key, layout in pairs(workspace_layouts) do
file:write("workspace.", tostring(key), "=", tostring(layout), "\n")
end
file:close()
end
local function is_normal_workspace(workspace)
return workspace and not workspace.special and workspace.id and workspace.id >= 1
end
local function same_workspace(left, right)
if not left or not right then
return false
end
if left.name and right.name and tostring(left.name) == tostring(right.name) then
return true
end
return left.id and right.id and left.id == right.id
end
local function is_minimized_workspace(workspace)
if not workspace then
return false
end
local name = tostring(workspace.name or "")
return name == minimized_workspace or name == "minimized" or (workspace.special and name:find("minimized", 1, true) ~= nil)
end
local function is_minimized_window(window)
return window and is_minimized_workspace(window.workspace)
end
local function is_normal_window(window)
return window
and window.mapped ~= false
and not window.hidden
and window.workspace
and is_normal_workspace(window.workspace)
and not is_scratchpad_window(window)
and not is_minimized_window(window)
end
local function tiled_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if not window.floating and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function tiled_window_count(workspace)
return #tiled_windows(workspace)
end
local function sort_windows_by_focus_history(windows)
table.sort(windows, function(left, right)
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
end
local function window_address_set(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[window.address] = true
end
end
return addresses
end
local function window_address_list(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[#addresses + 1] = window.address
end
end
return addresses
end
local function window_address_in_set(window, addresses)
return window and window.address and addresses[window.address] or false
end
local function windows_by_address()
local windows = {}
for _, window in ipairs(hl.get_windows()) do
if window and window.address then
windows[window.address] = window
end
end
return windows
end
local function numeric_component(value, key, index)
if type(value) ~= "table" then
return 0
end
return tonumber(value[key] or value[index]) or 0
end
local function window_center(window)
local at = window and window.at or {}
local size = window and window.size or {}
return numeric_component(at, "x", 1) + numeric_component(size, "x", 1) / 2,
numeric_component(at, "y", 2) + numeric_component(size, "y", 2) / 2
end
local function tiled_window_geometry(window)
if not window or window.floating then
return nil
end
local selector = window_selector(window)
if not selector then
return nil
end
local at = window.at or {}
local size = window.size or {}
local width = math.floor(numeric_component(size, "x", 1))
local height = math.floor(numeric_component(size, "y", 2))
if width <= 0 or height <= 0 then
return nil
end
return {
selector = selector,
x = math.floor(numeric_component(at, "x", 1)),
y = math.floor(numeric_component(at, "y", 2)),
width = width,
height = height,
}
end
local function window_distance_squared(window, x, y)
local wx, wy = window_center(window)
local dx = wx - x
local dy = wy - y
return dx * dx + dy * dy
end
local function sort_windows_by_visual_position(windows)
table.sort(windows, function(left, right)
local left_x, left_y = window_center(left)
local right_x, right_y = window_center(right)
if math.abs(left_x - right_x) > 10 then
return left_x < right_x
end
if math.abs(left_y - right_y) > 10 then
return left_y < right_y
end
return tostring(left.address or "") < tostring(right.address or "")
end)
end
local function grouping_direction(window, anchor)
local wx, wy = window_center(window)
local ax, ay = window_center(anchor)
local dx = wx - ax
local dy = wy - ay
if math.abs(dx) >= math.abs(dy) then
return dx >= 0 and "left" or "right"
end
return dy >= 0 and "up" or "down"
end
local function grouping_directions(window, anchor)
local primary = grouping_direction(window, anchor)
local directions = { primary }
for _, direction in ipairs({ "left", "right", "up", "down" }) do
if direction ~= primary then
directions[#directions + 1] = direction
end
end
return directions
end
local function workspace_window_count(workspace_id)
local workspace = hl.get_workspace(tostring(workspace_id))
if not workspace then
return 0
end
return workspace.windows or tiled_window_count(workspace)
end
local function find_empty_workspace(target_monitor, exclude_id)
local unused_candidate = nil
local elsewhere_empty_candidate = nil
local target_monitor_name = target_monitor and target_monitor.name or nil
for i = 1, max_workspace do
if i ~= exclude_id then
local workspace = hl.get_workspace(tostring(i))
if not workspace then
unused_candidate = unused_candidate or i
elseif is_normal_workspace(workspace) and workspace_window_count(i) == 0 then
local monitor = workspace.monitor
if target_monitor_name and monitor and monitor.name == target_monitor_name then
return i
end
elsewhere_empty_candidate = elsewhere_empty_candidate or i
end
end
end
return unused_candidate or elsewhere_empty_candidate
end
ctx.command_line_contains = command_line_contains
ctx.bind = bind
ctx.exec = exec
ctx.dispatch = dispatch
ctx.shell_quote = shell_quote
ctx.overview_trace = overview_trace
ctx.window_selector = window_selector
ctx.hyprexpo = hyprexpo
ctx.hyprwinview = hyprwinview
ctx.workspacehistory = workspacehistory
ctx.apply_nstack_config = apply_nstack_config
ctx.apply_hyprexpo_config = apply_hyprexpo_config
ctx.apply_hyprwinview_config = apply_hyprwinview_config
ctx.active_workspace = active_workspace
ctx.active_workspace_id = active_workspace_id
ctx.workspace_key = workspace_key
ctx.current_workspace_layout = current_workspace_layout
ctx.write_layout_state = write_layout_state
ctx.is_normal_workspace = is_normal_workspace
ctx.same_workspace = same_workspace
ctx.is_minimized_workspace = is_minimized_workspace
ctx.is_minimized_window = is_minimized_window
ctx.is_normal_window = is_normal_window
ctx.tiled_windows = tiled_windows
ctx.tiled_window_count = tiled_window_count
ctx.sort_windows_by_focus_history = sort_windows_by_focus_history
ctx.window_address_set = window_address_set
ctx.window_address_list = window_address_list
ctx.window_address_in_set = window_address_in_set
ctx.windows_by_address = windows_by_address
ctx.numeric_component = numeric_component
ctx.window_center = window_center
ctx.tiled_window_geometry = tiled_window_geometry
ctx.window_distance_squared = window_distance_squared
ctx.sort_windows_by_visual_position = sort_windows_by_visual_position
ctx.grouping_direction = grouping_direction
ctx.grouping_directions = grouping_directions
ctx.workspace_window_count = workspace_window_count
ctx.find_empty_workspace = find_empty_workspace
end
return M

View File

@@ -0,0 +1,96 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local fullscreen_states = {}
local function unset_fullscreen_state(window, state)
dispatch(hl.dsp.window.fullscreen_state({
internal = state.internal,
client = state.client,
action = "unset",
window = window_selector(window),
}))
end
local function reconcile_fullscreen_state(window)
if not window or not window.address then
return
end
local address = tostring(window.address)
local previous = fullscreen_states[address]
local current = {
internal = tonumber(window.fullscreen) or 0,
client = tonumber(window.fullscreen_client) or 0,
}
fullscreen_states[address] = current
if window.floating or current_layout == monocle_layout then
return
end
if current.internal == 1 or (previous and previous.internal >= 2 and current.internal > 0 and current.client == 0) then
unset_fullscreen_state(window, current)
fullscreen_states[address] = { internal = 0, client = 0 }
end
end
hl.on("hyprland.start", function()
apply_nstack_config()
apply_hyprexpo_config()
apply_hyprwinview_config()
apply_hyprwobbly_config()
apply_hyprglass_config()
apply_visual_performance_mode()
apply_rules()
if not dev_session then
hl.exec_cmd("sh -lc '/run/current-system/sw/bin/uwsm finalize HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE XAUTHORITY IMALISON_SESSION_TYPE=wayland IMALISON_WINDOW_MANAGER=hyprland || dbus-update-activation-environment --systemd XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE IMALISON_WINDOW_MANAGER; systemctl --user start hyprland-session.target'")
hl.exec_cmd("hypridle")
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
end
write_layout_state()
schedule_nstack_count_update()
refresh_monitor_reserved_cache(0.25)
refresh_monitor_reserved_cache(1.25)
end)
hl.on("config.reloaded", apply_nstack_config)
hl.on("config.reloaded", apply_hyprexpo_config)
hl.on("config.reloaded", apply_hyprwinview_config)
hl.on("config.reloaded", apply_hyprwobbly_config)
hl.on("config.reloaded", apply_hyprglass_config)
hl.on("config.reloaded", apply_visual_performance_mode)
hl.on("config.reloaded", apply_rules)
hl.on("config.reloaded", refresh_shell_workarea_and_scratchpads)
hl.on("layer.opened", refresh_shell_workarea_and_scratchpads)
hl.on("layer.closed", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.added", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.removed", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.layout_changed", refresh_shell_workarea_and_scratchpads)
hl.on("window.open", schedule_nstack_count_update)
hl.on("window.destroy", schedule_nstack_count_update)
hl.on("window.kill", schedule_nstack_count_update)
hl.on("window.move_to_workspace", schedule_nstack_count_update)
hl.on("workspace.active", sync_layout_for_active_workspace)
hl.on("monitor.focused", sync_layout_for_active_workspace)
hl.on("window.open", update_monocle_notice)
hl.on("window.destroy", update_monocle_notice)
hl.on("window.kill", update_monocle_notice)
hl.on("window.move_to_workspace", update_monocle_notice)
hl.on("window.fullscreen", reconcile_fullscreen_state)
hl.on("window.update_rules", reconcile_fullscreen_state)
hl.on("window.open", adopt_matching_scratchpad_window)
hl.on("window.class", adopt_matching_scratchpad_window)
hl.on("window.title", adopt_matching_scratchpad_window)
hl.on("window.open", raise_file_chooser_window_later)
hl.on("window.class", raise_file_chooser_window_later)
hl.on("window.title", raise_file_chooser_window_later)
end
return M

View File

@@ -0,0 +1,596 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function is_nstack_layout(layout)
return layout == columns_layout or layout == grid_layout
end
local function hyprland_layout(layout)
if layout == grid_layout then
return columns_layout
end
return layout
end
local function update_nstack_count()
if not enable_nstack or not is_nstack_layout(current_layout) then
return
end
local workspace = hl.get_active_workspace()
local count = tiled_window_count(workspace)
if count == 0 then
return
end
local stack_count = count
if current_layout == grid_layout then
stack_count = math.ceil(math.sqrt(count))
end
stack_count = math.max(stack_count, 2)
dispatch(hl.dsp.layout("setstackcount " .. tostring(stack_count)))
end
local function schedule_nstack_count_update()
if stack_update_timer then
stack_update_timer:set_enabled(false)
end
stack_update_timer = hl.timer(update_nstack_count, { timeout = 25, type = "oneshot" })
end
local function dismiss_monocle_notice()
if monocle_notice and monocle_notice:is_alive() then
monocle_notice:dismiss()
end
monocle_notice = nil
end
local function update_monocle_notice()
if current_layout ~= monocle_layout then
dismiss_monocle_notice()
return
end
local workspace = hl.get_active_workspace()
local count = tiled_window_count(workspace)
if count <= 1 then
dismiss_monocle_notice()
return
end
local text = "Monocle: " .. tostring(count) .. " windows"
if monocle_notice and monocle_notice:is_alive() then
monocle_notice:set_text(text)
monocle_notice:set_timeout(60000)
monocle_notice:pause()
else
monocle_notice = hl.notification.create({
text = text,
duration = 60000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
monocle_notice:pause()
end
end
local function layout_name(layout)
return layout_names[layout] or tostring(layout)
end
local function notify_layout(layout)
hl.notification.create({
text = "Layout: " .. layout_name(layout),
duration = 1200,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function set_layout(layout)
workspace_layouts[workspace_key()] = layout
current_layout = layout
hl.config({ general = { layout = hyprland_layout(layout) } })
write_layout_state()
if is_nstack_layout(layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
update_monocle_notice()
end
end
_G.im_hyprland_set_layout = function(layout)
if not layout_names[layout] then
hl.notification.create({
text = "Unknown layout: " .. tostring(layout),
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
set_layout(layout)
notify_layout(layout)
end
local function sync_layout_for_active_workspace()
current_layout = current_workspace_layout()
hl.config({ general = { layout = hyprland_layout(current_layout) } })
write_layout_state()
if is_nstack_layout(current_layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
update_monocle_notice()
end
end
local function cycle_layout(delta)
local current_index = 1
for index, layout in ipairs(layout_cycle) do
if layout == current_layout then
current_index = index
break
end
end
local next_index = ((current_index - 1 + delta) % #layout_cycle) + 1
local next_layout = layout_cycle[next_index]
set_layout(next_layout)
notify_layout(next_layout)
end
local function toggle_columns_monocle()
if current_layout == columns_layout then
set_layout(monocle_layout)
else
set_layout(columns_layout)
end
end
local function active_group_size()
local window = hl.get_active_window()
return window and window.group and window.group.size or 0
end
local function monocle_next()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
dispatch(hl.dsp.group.next({ window = window_selector(window) }))
elseif current_layout == monocle_layout then
dispatch(hl.dsp.layout("cyclenext"))
update_monocle_notice()
else
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
end
end
local function monocle_prev()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
dispatch(hl.dsp.group.prev({ window = window_selector(window) }))
elseif current_layout == monocle_layout then
dispatch(hl.dsp.layout("cycleprev"))
update_monocle_notice()
else
dispatch(hl.dsp.window.cycle_next({ next = false, tiled = true, floating = false }))
end
end
local function focus_direction(direction)
overview_trace("focus_direction " .. direction)
if active_group_size() > 1 or current_layout == monocle_layout then
if direction == "up" or direction == "left" then
monocle_prev()
else
monocle_next()
end
return
end
dispatch(hl.dsp.focus({ direction = direction }))
end
local function swap_direction(direction)
if enable_nstack and is_nstack_layout(current_layout) and active_group_size() <= 1 then
dispatch(hl.dsp.layout("swapdirection " .. direction))
return
end
dispatch(hl.dsp.window.swap({ direction = direction }))
end
local function focus_workspace(workspace_id)
dispatch(hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true }))
end
local function move_window_to_workspace(workspace_id, follow, window)
local target_window = window or hl.get_active_window()
local target_selector = window_selector(target_window)
dispatch(hl.dsp.window.move({ workspace = tostring(workspace_id), follow = false, window = target_selector }))
if follow then
focus_workspace(workspace_id)
if target_selector then
dispatch(hl.dsp.focus({ window = target_selector }))
end
end
end
local function notify_tabbed_group(text)
hl.notification.create({
text = text,
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function active_workspace_tiled_group_candidates(workspace)
local candidates = tiled_windows(workspace)
sort_windows_by_focus_history(candidates)
return candidates
end
local function move_window_into_group(window, anchor)
local selector = window_selector(window)
if not selector then
return false
end
for _, direction in ipairs(grouping_directions(window, anchor)) do
dispatch(hl.dsp.focus({ window = selector }))
dispatch(hl.dsp.window.move({ into_group = direction, window = selector }))
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return true
end
end
return false
end
local function find_tabbed_group_anchor(state)
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return active
end
if not state then
return nil
end
for _, window in ipairs(hl.get_windows()) do
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
return window
end
end
return nil
end
local function ordered_windows_for_tabbed_group_restore(state, workspace_id)
local ordered = {}
local seen = {}
local live_windows = windows_by_address()
local workspace = workspace_id and hl.get_workspace(tostring(workspace_id)) or active_workspace()
if state and state.order then
for _, address in ipairs(state.order) do
local window = live_windows[address]
if window and not window.floating and not window.hidden and (not workspace or same_workspace(window.workspace, workspace)) then
ordered[#ordered + 1] = window
seen[address] = true
end
end
end
if workspace then
for _, window in ipairs(tiled_windows(workspace)) do
if window and window.address and not seen[window.address] then
ordered[#ordered + 1] = window
seen[window.address] = true
end
end
end
return ordered
end
local function restore_tabbed_group_window_order(state, workspace_id)
local ordered = ordered_windows_for_tabbed_group_restore(state, workspace_id)
if #ordered <= 1 or not workspace_id then
return
end
local restore_workspace = tabbed_group_restore_workspace_prefix .. tostring(workspace_id)
for _, window in ipairs(ordered) do
move_window_to_workspace(restore_workspace, false, window)
end
for _, window in ipairs(ordered) do
move_window_to_workspace(workspace_id, false, window)
end
end
local function restore_workspace_tabbed_group()
local key = workspace_key()
local state = tabbed_workspace_groups[key]
local anchor = find_tabbed_group_anchor(state)
local anchor_selector = window_selector(anchor)
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
if not anchor_selector then
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
notify_tabbed_group("No tabbed group to restore")
return
end
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
restore_tabbed_group_window_order(state, target_workspace_id)
dispatch(hl.dsp.focus({ window = anchor_selector }))
schedule_nstack_count_update()
end
local function gather_workspace_into_tabbed_group()
local workspace = active_workspace()
if not is_normal_workspace(workspace) then
return
end
local key = workspace_key(workspace)
if tabbed_workspace_groups[key] or active_group_size() > 1 then
restore_workspace_tabbed_group()
return
end
local original_windows = tiled_windows(workspace)
sort_windows_by_visual_position(original_windows)
local original_order = window_address_list(original_windows)
local candidates = active_workspace_tiled_group_candidates(workspace)
if #candidates <= 1 then
set_layout(columns_layout)
return
end
local candidate_addresses = window_address_set(candidates)
local focused = hl.get_active_window()
local anchor = nil
if focused and not focused.floating and not focused.group and window_address_in_set(focused, candidate_addresses) then
anchor = focused
end
if not anchor then
for _, window in ipairs(candidates) do
if not window.group then
anchor = window
break
end
end
end
local anchor_selector = window_selector(anchor)
if not anchor_selector then
notify_tabbed_group("Current tiled windows are already grouped")
return
end
set_layout(columns_layout)
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
local group_windows = {}
for _, window in ipairs(candidates) do
if window ~= anchor and not window.group then
group_windows[#group_windows + 1] = window
end
end
local anchor_x, anchor_y = window_center(anchor)
table.sort(group_windows, function(left, right)
return window_distance_squared(left, anchor_x, anchor_y) < window_distance_squared(right, anchor_x, anchor_y)
end)
local grouped_count = 1
for _, window in ipairs(group_windows) do
if move_window_into_group(window, anchor) then
grouped_count = grouped_count + 1
end
end
if grouped_count <= 1 then
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
notify_tabbed_group("Unable to group tiled windows")
return
elseif grouped_count < #candidates then
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
end
tabbed_workspace_groups[key] = {
anchor = anchor.address,
order = original_order,
windows = candidate_addresses,
}
dispatch(hl.dsp.focus({ window = anchor_selector }))
end
local function force_columns_layout()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
else
set_layout(columns_layout)
end
end
local function cycle_layout_or_restore_tabbed_group()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
return
end
cycle_layout(1)
end
local function copy_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if window and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function swap_current_workspace_with(target_id)
local current = active_workspace()
if not current or not current.id or current.id == target_id then
return
end
local target = hl.get_workspace(tostring(target_id))
local current_windows = copy_windows(current)
local target_windows = copy_windows(target)
for _, window in ipairs(current_windows) do
move_window_to_workspace(target_id, false, window)
end
for _, window in ipairs(target_windows) do
move_window_to_workspace(current.id, false, window)
end
focus_workspace(current.id)
end
local function enter_workspace_swap_mode()
hl.notification.create({
text = "Swap with workspace 1-9",
duration = 2200,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
dispatch(hl.dsp.submap("swap-workspace"))
end
local function focus_next_empty_workspace()
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
if workspace_id then
focus_workspace(workspace_id)
end
end
local function move_to_next_empty_workspace(follow)
local window = hl.get_active_window()
if not window then
return
end
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
if workspace_id then
move_window_to_workspace(workspace_id, follow, window)
end
end
local function cycle_workspace(delta)
local current = active_workspace_id()
local next_workspace = ((current - 1 + delta) % max_workspace) + 1
focus_workspace(next_workspace)
end
local function move_window_to_monitor(direction, follow)
local window = hl.get_active_window()
if not window then
return
end
local original_monitor = hl.get_active_monitor()
dispatch(hl.dsp.window.move({ monitor = direction, follow = follow, window = window_selector(window) }))
if not follow and original_monitor then
dispatch(hl.dsp.focus({ monitor = original_monitor }))
end
end
local function move_window_to_empty_workspace_on_monitor(direction)
local window = hl.get_active_window()
local original_monitor = hl.get_active_monitor()
local target_monitor = hl.get_monitor(direction)
if not window or not original_monitor or not target_monitor or target_monitor == original_monitor then
return
end
local workspace_id = find_empty_workspace(target_monitor, active_workspace_id())
if not workspace_id then
return
end
dispatch(hl.dsp.focus({ monitor = target_monitor }))
focus_workspace(workspace_id)
dispatch(hl.dsp.focus({ monitor = original_monitor }))
move_window_to_workspace(workspace_id, false, window)
end
ctx.is_nstack_layout = is_nstack_layout
ctx.hyprland_layout = hyprland_layout
ctx.update_nstack_count = update_nstack_count
ctx.schedule_nstack_count_update = schedule_nstack_count_update
ctx.dismiss_monocle_notice = dismiss_monocle_notice
ctx.update_monocle_notice = update_monocle_notice
ctx.layout_name = layout_name
ctx.notify_layout = notify_layout
ctx.set_layout = set_layout
ctx.sync_layout_for_active_workspace = sync_layout_for_active_workspace
ctx.cycle_layout = cycle_layout
ctx.toggle_columns_monocle = toggle_columns_monocle
ctx.active_group_size = active_group_size
ctx.monocle_next = monocle_next
ctx.monocle_prev = monocle_prev
ctx.focus_direction = focus_direction
ctx.swap_direction = swap_direction
ctx.focus_workspace = focus_workspace
ctx.move_window_to_workspace = move_window_to_workspace
ctx.notify_tabbed_group = notify_tabbed_group
ctx.active_workspace_tiled_group_candidates = active_workspace_tiled_group_candidates
ctx.move_window_into_group = move_window_into_group
ctx.find_tabbed_group_anchor = find_tabbed_group_anchor
ctx.ordered_windows_for_tabbed_group_restore = ordered_windows_for_tabbed_group_restore
ctx.restore_tabbed_group_window_order = restore_tabbed_group_window_order
ctx.restore_workspace_tabbed_group = restore_workspace_tabbed_group
ctx.gather_workspace_into_tabbed_group = gather_workspace_into_tabbed_group
ctx.force_columns_layout = force_columns_layout
ctx.cycle_layout_or_restore_tabbed_group = cycle_layout_or_restore_tabbed_group
ctx.copy_windows = copy_windows
ctx.swap_current_workspace_with = swap_current_workspace_with
ctx.enter_workspace_swap_mode = enter_workspace_swap_mode
ctx.focus_next_empty_workspace = focus_next_empty_workspace
ctx.move_to_next_empty_workspace = move_to_next_empty_workspace
ctx.cycle_workspace = cycle_workspace
ctx.move_window_to_monitor = move_window_to_monitor
ctx.move_window_to_empty_workspace_on_monitor = move_window_to_empty_workspace_on_monitor
end
return M

View File

@@ -0,0 +1,490 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
scratchpad_size_ratio = 0.95
dropdown_height_ratio = 0.5
dropdown_animation_frames = 18
dropdown_animation_frame_ms = 16
scratchpad_pending = {}
monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv"
scratchpad_fallback_reserved_top = 60
scratchpads = {
codex = {
command = "codex_desktop_scratchpad",
class = "codex-desktop",
},
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",
classes = { "Element", "electron" },
title = "Element",
},
slack = {
command = "slack",
class = "Slack",
},
messages = {
command = "google-chrome-stable --profile-directory=Default --app=https://messages.google.com/web/conversations",
class = "chrome-messages.google.com",
},
transmission = {
command = "transmission-gtk",
class = "transmission-gtk",
},
dropdown = {
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown",
class = "com.mitchellh.ghostty.dropdown",
dropdown = true,
},
}
local function lower_contains(value, needle)
if not needle or needle == "" then
return true
end
value = string.lower(tostring(value or ""))
needle = string.lower(tostring(needle))
return value:find(needle, 1, true) ~= nil
end
local function lower_contains_any(value, needles)
if type(needles) ~= "table" then
return lower_contains(value, needles)
end
for _, needle in ipairs(needles) do
if lower_contains(value, needle) then
return true
end
end
return false
end
local function scratchpad_window_matches(window, def)
return window
and not (type(is_file_chooser_window) == "function" and is_file_chooser_window(window))
and lower_contains_any(window.class, def.classes or def.class)
and lower_contains(window.title, def.title)
end
local function is_scratchpad_window(window)
for _, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
return true
end
end
return false
end
local function matching_scratchpad_name(window)
for name, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
return name
end
end
return nil
end
local function scratchpad_workspace(name)
return "special:scratch-" .. name
end
local function as_number(value, default)
local number = tonumber(value)
if number == nil then
return default
end
return number
end
local function logical_monitor_dimension(value, scale)
value = as_number(value, 0)
scale = as_number(scale, 1)
if scale <= 0 then
scale = 1
end
return math.floor((value / scale) + 0.5)
end
local function split_tsv(line)
local fields = {}
for field in (line .. "\t"):gmatch("([^\t]*)\t") do
fields[#fields + 1] = field
end
return fields
end
local function monitor_from_reserved_fields(monitor, fields)
if not monitor or not monitor.name or fields[1] ~= monitor.name or #fields < 10 then
return nil
end
return {
name = monitor.name,
x = tonumber(fields[2]),
y = tonumber(fields[3]),
width = tonumber(fields[4]),
height = tonumber(fields[5]),
scale = tonumber(fields[6]),
reserved = {
tonumber(fields[7]),
tonumber(fields[8]),
tonumber(fields[9]),
tonumber(fields[10]),
},
}
end
local function monitor_from_reserved_lines(monitor, lines)
if not monitor or not monitor.name then
return nil
end
for line in lines do
local cached = monitor_from_reserved_fields(monitor, split_tsv(line))
if cached then
return cached
end
end
return nil
end
local function monitor_from_reserved_cache(monitor)
if verify_config or not monitor or not monitor.name then
return nil
end
local file = io.open(monitor_reserved_cache_path, "r")
if not file then
return nil
end
local cached = monitor_from_reserved_lines(monitor, file:lines())
file:close()
return cached
end
local function refresh_monitor_reserved_cache(delay)
if verify_config then
return
end
local command = string.format(
[=[sleep %.2f; cache="${XDG_RUNTIME_DIR:-/tmp}/hyprland-monitor-reserved.tsv"; tmp="$cache.tmp"; /run/current-system/sw/bin/hyprctl -j monitors 2>/dev/null | /run/current-system/sw/bin/jq -r '.[] | [.name, .x, .y, .width, .height, .scale, .reserved[0], .reserved[1], .reserved[2], .reserved[3]] | @tsv' > "$tmp" && mv "$tmp" "$cache"]=],
as_number(delay, 0)
)
hl.exec_cmd("sh -lc " .. shell_quote(command))
end
local function monitor_workarea(monitor)
monitor = monitor_from_reserved_cache(monitor) or monitor
local width = logical_monitor_dimension(monitor.width, monitor.scale)
local height = logical_monitor_dimension(monitor.height, monitor.scale)
local reserved = monitor.reserved or { 0, scratchpad_fallback_reserved_top, 0, 0 }
local left = math.floor(as_number(reserved[1], 0))
local top = math.floor(as_number(reserved[2], 0))
local right = math.floor(as_number(reserved[3], 0))
local bottom = math.floor(as_number(reserved[4], 0))
local work_width = width - left - right
local work_height = height - top - bottom
if work_width <= 0 then
left = 0
right = 0
work_width = width
end
if work_height <= 0 then
top = 0
bottom = 0
work_height = height
end
return {
x = math.floor(as_number(monitor.x, 0)) + left,
y = math.floor(as_number(monitor.y, 0)) + top,
width = work_width,
height = work_height,
}
end
local function matching_scratchpad_windows(name)
local def = scratchpads[name]
local windows = {}
if not def then
return windows
end
for _, window in ipairs(hl.get_windows()) do
if scratchpad_window_matches(window, def) then
windows[#windows + 1] = window
end
end
return windows
end
local function scratchpad_geometry(name, target_monitor, position)
local def = scratchpads[name]
local monitor = target_monitor or hl.get_active_monitor()
if not def or not monitor then
return
end
local workarea = monitor_workarea(monitor)
local width
local height
local x
local y
if def.dropdown then
width = workarea.width
height = math.floor(workarea.height * dropdown_height_ratio)
x = workarea.x
y = workarea.y
if position == "above" then
y = workarea.y - height
elseif type(position) == "number" then
y = position
end
else
width = math.floor(workarea.width * scratchpad_size_ratio)
height = math.floor(workarea.height * scratchpad_size_ratio)
x = workarea.x + math.floor((workarea.width - width) / 2)
y = workarea.y + math.floor((workarea.height - height) / 2)
end
return {
width = width,
height = height,
x = x,
y = y,
}
end
local function apply_scratchpad_geometry(name, window, target_monitor, position)
local def = scratchpads[name]
if not def or not window then
return
end
local geometry = scratchpad_geometry(name, target_monitor, position)
if not geometry then
return
end
local selector = window_selector(window)
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+scratchpad", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector }))
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = selector }))
if def.dropdown then
dispatch(hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = selector }))
dispatch(hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = selector }))
end
end
local function schedule_scratchpad_geometry(name, window, target_monitor, position, timeout)
hl.timer(function()
apply_scratchpad_geometry(name, window, target_monitor, position)
end, { timeout = timeout or 50, type = "oneshot" })
end
local function dropdown_spring_progress(progress)
if progress >= 1 then
return 1
end
return 1 - (math.exp(-5.0 * progress) * math.cos(7.0 * progress))
end
local function animate_dropdown_scratchpad_down(name, window, target_monitor)
local from = scratchpad_geometry(name, target_monitor, "above")
local to = scratchpad_geometry(name, target_monitor)
if not from or not to then
schedule_scratchpad_geometry(name, window, target_monitor, nil, 35)
return
end
for frame = 1, dropdown_animation_frames do
local progress = frame / dropdown_animation_frames
local eased = dropdown_spring_progress(progress)
local y = math.floor(from.y + ((to.y - from.y) * eased) + 0.5)
schedule_scratchpad_geometry(name, window, target_monitor, y, frame * dropdown_animation_frame_ms)
end
end
local function hide_scratchpad_window(name, window)
remove_minimized_window(window)
move_window_to_workspace(scratchpad_workspace(name), false, window)
end
local function show_scratchpad_window(name, window, workspace, target_monitor)
workspace = workspace or active_workspace()
if not workspace then
return
end
remove_minimized_window(window)
if scratchpads[name] and scratchpads[name].dropdown then
apply_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), "above")
end
move_window_to_workspace(workspace.id, false, window)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
if scratchpads[name] and scratchpads[name].dropdown then
animate_dropdown_scratchpad_down(name, window, target_monitor or hl.get_active_monitor())
else
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
end
end
local function scratchpad_is_visible(window)
local workspace = active_workspace()
return workspace and window and same_workspace(window.workspace, workspace)
end
-- Active scratchpads are scratchpad windows visible on the active workspace.
-- Invoking a different scratchpad replaces that active set.
local function active_scratchpad_windows(except_name)
local windows = {}
for _, window in ipairs(hl.get_windows()) do
local name = matching_scratchpad_name(window)
if name and name ~= except_name and scratchpad_is_visible(window) then
windows[#windows + 1] = {
name = name,
window = window,
}
end
end
return windows
end
local function hide_active_scratchpads(except_name)
for _, active in ipairs(active_scratchpad_windows(except_name)) do
hide_scratchpad_window(active.name, active.window)
end
end
local function refresh_active_scratchpad_geometries()
local monitor = hl.get_active_monitor()
for _, active in ipairs(active_scratchpad_windows()) do
schedule_scratchpad_geometry(active.name, active.window, monitor)
end
end
local function refresh_active_scratchpad_geometries_later(timeout)
hl.timer(refresh_active_scratchpad_geometries, { timeout = timeout or 300, type = "oneshot" })
end
local function refresh_shell_workarea_and_scratchpads()
refresh_monitor_reserved_cache(0.15)
refresh_active_scratchpad_geometries_later(400)
end
local function adopt_matching_scratchpad_window(window)
if not window then
return
end
for name, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
if scratchpad_pending[name] then
local pending = scratchpad_pending[name]
scratchpad_pending[name] = nil
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor())
elseif scratchpad_is_visible(window) then
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
end
end
end
end
local function toggle_scratchpad(name)
local def = scratchpads[name]
if not def then
return
end
if current_layout == monocle_layout then
set_layout(columns_layout)
end
local windows = matching_scratchpad_windows(name)
if #windows == 0 then
hide_active_scratchpads(name)
scratchpad_pending[name] = {
monitor = hl.get_active_monitor(),
workspace = active_workspace(),
}
hl.exec_cmd(def.command)
return
end
local any_visible = false
for _, window in ipairs(windows) do
if scratchpad_is_visible(window) then
any_visible = true
break
end
end
if any_visible then
for _, window in ipairs(windows) do
hide_scratchpad_window(name, window)
end
else
hide_active_scratchpads(name)
local workspace = active_workspace()
local target_monitor = hl.get_active_monitor()
for _, window in ipairs(windows) do
show_scratchpad_window(name, window, workspace, target_monitor)
end
end
end
ctx.lower_contains = lower_contains
ctx.lower_contains_any = lower_contains_any
ctx.scratchpad_window_matches = scratchpad_window_matches
ctx.is_scratchpad_window = is_scratchpad_window
ctx.matching_scratchpad_name = matching_scratchpad_name
ctx.scratchpad_workspace = scratchpad_workspace
ctx.as_number = as_number
ctx.logical_monitor_dimension = logical_monitor_dimension
ctx.split_tsv = split_tsv
ctx.monitor_from_reserved_fields = monitor_from_reserved_fields
ctx.monitor_from_reserved_lines = monitor_from_reserved_lines
ctx.monitor_from_reserved_cache = monitor_from_reserved_cache
ctx.refresh_monitor_reserved_cache = refresh_monitor_reserved_cache
ctx.monitor_workarea = monitor_workarea
ctx.scratchpad_geometry = scratchpad_geometry
ctx.matching_scratchpad_windows = matching_scratchpad_windows
ctx.apply_scratchpad_geometry = apply_scratchpad_geometry
ctx.schedule_scratchpad_geometry = schedule_scratchpad_geometry
ctx.dropdown_spring_progress = dropdown_spring_progress
ctx.animate_dropdown_scratchpad_down = animate_dropdown_scratchpad_down
ctx.hide_scratchpad_window = hide_scratchpad_window
ctx.show_scratchpad_window = show_scratchpad_window
ctx.scratchpad_is_visible = scratchpad_is_visible
ctx.active_scratchpad_windows = active_scratchpad_windows
ctx.hide_active_scratchpads = hide_active_scratchpads
ctx.refresh_active_scratchpad_geometries = refresh_active_scratchpad_geometries
ctx.refresh_active_scratchpad_geometries_later = refresh_active_scratchpad_geometries_later
ctx.refresh_shell_workarea_and_scratchpads = refresh_shell_workarea_and_scratchpads
ctx.adopt_matching_scratchpad_window = adopt_matching_scratchpad_window
ctx.toggle_scratchpad = toggle_scratchpad
end
return M

View File

@@ -0,0 +1,422 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local file_chooser_title_rule = "^(Open File|Open Files|Save File|Save Files|Save As|Select File|Select Files|Choose File|Choose Files|File Upload|Upload File|Upload Files|Select Folder|Choose Folder|Open Folder|Save Folder)$"
local function lower_string(value)
return string.lower(tostring(value or ""))
end
local function title_indicates_file_chooser(title)
title = lower_string(title)
if title == "" then
return false
end
for _, exact in ipairs({
"open file",
"open files",
"save file",
"save files",
"save as",
"select file",
"select files",
"choose file",
"choose files",
"file upload",
"upload file",
"upload files",
"select folder",
"choose folder",
"open folder",
"save folder",
}) do
if title == exact then
return true
end
end
return title:find("file chooser", 1, true) ~= nil
or title:find("file picker", 1, true) ~= nil
end
local function is_file_chooser_window(window)
return window
and (title_indicates_file_chooser(window.title) or title_indicates_file_chooser(window.initial_title))
end
local function raise_file_chooser_window(window)
if verify_config or not is_file_chooser_window(window) then
return
end
local selector = window_selector(window)
if not selector then
return
end
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.center({ window = selector }))
dispatch(hl.dsp.focus({ window = selector }))
dispatch(hl.dsp.window.bring_to_top({ window = selector }))
end
local function raise_file_chooser_window_later(window, timeout)
hl.timer(function()
local refreshed = window and window.address and hl.get_window(window_selector(window)) or window
raise_file_chooser_window(refreshed)
end, { timeout = timeout or 50, type = "oneshot" })
end
if enable_nstack and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so")
end
if enable_hyprexpo and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
end
if enable_hyprwinview and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwinview.so")
end
if enable_workspace_history and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhypr-workspace-history.so")
end
if enable_hyprwobbly and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwobbly.so")
end
if enable_hyprglass and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/hyprglass.so")
end
hl.env("XCURSOR_SIZE", "24")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("HYPR_MAX_WORKSPACE", "9")
hl.config({
input = {
kb_layout = "us",
kb_variant = "",
kb_model = "",
kb_options = "",
kb_rules = "",
follow_mouse = 1,
sensitivity = 0,
touchpad = {
natural_scroll = false,
},
},
cursor = {
persistent_warps = true,
},
general = {
gaps_in = 5,
gaps_out = 10,
border_size = 2,
col = {
active_border = { colors = { "rgba(3b82f6ee)", "rgba(33ccffee)" }, angle = 45 },
inactive_border = "rgba(00000000)",
},
layout = columns_layout,
allow_tearing = false,
},
decoration = {
rounding = 5,
blur = {
enabled = true,
size = 7,
passes = 3,
},
active_opacity = 1.0,
inactive_opacity = 0.65,
},
animations = {
enabled = true,
},
binds = {
allow_workspace_cycles = true,
workspace_back_and_forth = true,
},
group = {
group_on_movetoworkspace = false,
col = {
border_active = "rgba(edb443ff)",
border_inactive = "rgba(091f2eff)",
},
groupbar = {
enabled = true,
blur = true,
font_size = 13,
gradients = true,
height = 26,
indicator_gap = 0,
indicator_height = 1,
rounding = 5,
gradient_rounding = 5,
text_padding = 8,
col = {
active = "rgba(edb443ff)",
inactive = "rgba(101820f2)",
},
text_color = "rgba(091018ff)",
text_color_inactive = "rgba(f2f5f7ff)",
},
},
misc = {
force_default_wallpaper = 0,
disable_hyprland_logo = true,
exit_window_retains_fullscreen = true,
},
})
hl.curve("overshoot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
hl.curve("smoothOut", { type = "bezier", points = { { 0.36, 1 }, { 0.3, 1 } } })
hl.curve("smoothInOut", { type = "bezier", points = { { 0.42, 0 }, { 0.58, 1 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
local spring_time_scale = 5
local function spring_curve(mass, stiffness, dampening)
return {
type = "spring",
mass = mass,
stiffness = stiffness * spring_time_scale * spring_time_scale,
dampening = dampening * spring_time_scale,
}
end
hl.curve("workspaceSpring", spring_curve(2.4, 38, 8))
hl.curve("windowSpring", spring_curve(2.5, 40, 10))
local animations = {
{ leaf = "global", enabled = true, speed = 8, bezier = "default" },
{ leaf = "windows", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
{ leaf = "windowsIn", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
{ leaf = "windowsOut", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
{ leaf = "windowsMove", enabled = true, speed = 8, spring = "windowSpring" },
{ leaf = "border", enabled = false },
{ leaf = "borderangle", enabled = false },
{ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeSwitch", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeShadow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeGlow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayers", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopups", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDpms", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "layers", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersIn", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersOut", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "workspaces", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
{ leaf = "workspacesIn", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
{ leaf = "workspacesOut", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
{ leaf = "specialWorkspace", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
{ leaf = "specialWorkspaceIn", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
{ leaf = "specialWorkspaceOut", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
{ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "smoothOut" },
-- Disabled for now: Hyprland 0.54.0 can crash while damaging a monitor
-- from this startup animation's update callback during output discovery.
-- { leaf = "monitorAdded", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "monitorAdded", enabled = false, speed = 5, bezier = "smoothOut" },
}
for _, animation in ipairs(animations) do
hl.animation(animation)
end
local function apply_hyprglass_config()
if verify_config or not enable_hyprglass then
return
end
hl.config({
plugin = {
hyprglass = {
enabled = 0,
default_theme = "dark",
default_preset = "default",
},
},
})
end
local function apply_hyprwobbly_config()
if verify_config or not enable_hyprwobbly then
return
end
hl.config({
plugin = {
hyprwobbly = {
enabled = hypr_visual_performance_mode and 0 or 1,
mode = "always",
grid_width = 4,
grid_height = 4,
tiles_x = 12,
tiles_y = 12,
spring_k = 18.0,
friction = 8.0,
mass = 12.0,
move_factor = 0.65,
resize_factor = 0.45,
max_warp = 140.0,
},
},
})
end
local function apply_visual_performance_mode()
if verify_config then
return
end
local visual_effects_enabled = not hypr_visual_performance_mode
hl.config({
decoration = {
blur = {
enabled = visual_effects_enabled,
},
},
animations = {
enabled = visual_effects_enabled,
},
})
if enable_hyprwobbly then
hl.config({
plugin = {
hyprwobbly = {
enabled = visual_effects_enabled and 1 or 0,
},
},
})
end
end
local function toggle_visual_performance_mode()
hypr_visual_performance_mode = not hypr_visual_performance_mode
apply_visual_performance_mode()
hl.notification.create({
text = "Hyprland performance mode: " .. (hypr_visual_performance_mode and "on" or "off"),
duration = 1800,
icon = hypr_visual_performance_mode and notification_icons.warning or notification_icons.ok,
color = hypr_visual_performance_mode and "rgba(edb443ff)" or "rgba(33ccffee)",
font_size = 13,
})
end
local function apply_rules()
if verify_config then
return
end
hl.workspace_rule({ workspace = "w[tv1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.workspace_rule({ workspace = "f[1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.window_rule({ match = { class = "^()$", title = "^()$" }, float = true })
hl.window_rule({ match = { title = "^(Picture-in-Picture)$" }, float = true })
hl.window_rule({
name = "rofi-glass-window",
match = { class = "^(rofi)$" },
float = true,
center = true,
decorate = false,
no_shadow = true,
xray = false,
})
hl.layer_rule({
name = "rofi-glass-layer",
match = { namespace = "^(rofi)$" },
blur = true,
ignore_alpha = 0.05,
xray = false,
})
hl.window_rule({
name = "file-chooser-dialogs",
match = { title = file_chooser_title_rule },
float = true,
center = true,
focus_on_activate = true,
stay_focused = true,
})
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
for index, match in ipairs({
{ class = "^(flameshot)$" },
{ title = "^(flameshot)$" },
}) do
hl.window_rule({
name = "flameshot-overlay-" .. tostring(index),
match = match,
float = true,
no_anim = true,
suppress_event = "fullscreen",
})
end
hl.layer_rule({
name = "flameshot-layer-overlay",
match = { namespace = "^(flameshot)$" },
no_anim = true,
})
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
no_anim = true,
})
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
tag = "+hyprglass_enabled",
})
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
tag = "+hyprglass_theme_light",
})
hl.window_rule({
match = { class = "^(.*[Rr]umno.*)$" },
float = true,
pin = true,
center = true,
decorate = false,
no_shadow = true,
})
hl.window_rule({
match = { title = "^(.*[Rr]umno.*)$" },
float = true,
pin = true,
center = true,
decorate = false,
no_shadow = true,
})
hl.window_rule({
name = "subtle-pinned-window-border",
match = { pin = true },
border_size = 2,
border_color = "rgba(edb443ff) rgba(ff4d5dcc)",
})
hl.window_rule({
match = { tag = inactive_opacity_override_tag },
opacity = "1.0 override 1.0 override 1.0 override",
})
end
ctx.apply_rules = apply_rules
ctx.apply_hyprglass_config = apply_hyprglass_config
ctx.apply_hyprwobbly_config = apply_hyprwobbly_config
ctx.apply_visual_performance_mode = apply_visual_performance_mode
ctx.is_file_chooser_window = is_file_chooser_window
ctx.raise_file_chooser_window = raise_file_chooser_window
ctx.raise_file_chooser_window_later = raise_file_chooser_window_later
ctx.toggle_visual_performance_mode = toggle_visual_performance_mode
end
return M

View File

@@ -0,0 +1,63 @@
local shell_ui_command = "hypr_shell_ui"
local columns_layout = "nStack"
local large_main_layout = "master"
local grid_layout = "grid"
local monocle_layout = "monocle"
return {
main_mod = "SUPER",
mod_alt = "SUPER + ALT",
hyper = "SUPER + CTRL + ALT",
terminal = "ghostty --gtk-single-instance=false",
shell_ui_command = shell_ui_command,
launcher_command = shell_ui_command .. " launcher",
run_menu = shell_ui_command .. " run",
-- Hyprland shadows ordinary keybinds after one fires; without transparent,
-- the first overview chord after a focus-moving bind can be skipped.
overview_bind_opts = { dont_inhibit = true, transparent = true },
overview_trace_enabled_path = "/tmp/hypr-overview-bind.enable",
overview_trace_path = "/tmp/hypr-overview-bind.log",
notification_icons = {
warning = 0,
info = 1,
hint = 2,
error = 3,
confused = 4,
ok = 5,
none = 6,
},
max_workspace = 9,
columns_layout = columns_layout,
large_main_layout = large_main_layout,
grid_layout = grid_layout,
monocle_layout = monocle_layout,
layout_cycle = { columns_layout, large_main_layout, grid_layout },
layout_names = {
[columns_layout] = "Columns",
[large_main_layout] = "Large main",
[grid_layout] = "Grid",
[monocle_layout] = "Monocle",
},
minimized_workspace = "special:minimized",
inactive_opacity_override_tag = "no-inactive-opacity",
tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-",
current_layout = columns_layout,
enable_nstack = true,
enable_hyprexpo = true,
enable_hyprwinview = true,
enable_workspace_history = true,
enable_hyprwobbly = true,
enable_hyprglass = false,
hypr_visual_performance_mode = false,
configure_nstack_plugin_from_lua = false,
workspace_layouts = {},
minimized_windows = {},
tabbed_workspace_groups = {},
window_picker_mode = nil,
window_picker_candidates = {},
stack_update_timer = nil,
monocle_notice = nil,
}

View File

@@ -0,0 +1,502 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function same_class_windows(class_name)
local windows = {}
if not class_name or class_name == "" then
return windows
end
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class == class_name then
windows[#windows + 1] = window
end
end
return windows
end
local function short_text(value, limit)
value = tostring(value or "")
value = value:gsub("[%c\t\r\n]", " ")
if #value <= limit then
return value
end
return value:sub(1, limit - 3) .. "..."
end
local function normal_windows()
local windows = {}
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) then
windows[#windows + 1] = window
end
end
table.sort(windows, function(left, right)
local left_workspace = left.workspace and left.workspace.id or max_workspace + 1
local right_workspace = right.workspace and right.workspace.id or max_workspace + 1
if left_workspace ~= right_workspace then
return left_workspace < right_workspace
end
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
return windows
end
local function window_picker_entry(index, window)
local workspace = window.workspace and window.workspace.id or "?"
local class = short_text(window.class, 18)
local title = short_text(window.title, 48)
return tostring(index) .. " [" .. tostring(workspace) .. "] " .. class .. " " .. title
end
local function remove_minimized_window(target)
local remaining = {}
local target_address = target and target.address
for _, window in ipairs(minimized_windows) do
if window and window.address ~= target_address then
remaining[#remaining + 1] = window
end
end
minimized_windows = remaining
end
local function add_minimized_window(window)
if not window or not window.address then
return
end
remove_minimized_window(window)
minimized_windows[#minimized_windows + 1] = window
end
local function hydrate_minimized_windows()
local by_address = {}
local current_by_address = {}
local hydrated = {}
for _, window in ipairs(hl.get_windows()) do
if window and window.address then
current_by_address[window.address] = window
end
end
for _, window in ipairs(minimized_windows) do
local current = window and window.address and current_by_address[window.address]
if current and is_minimized_window(current) and not by_address[current.address] then
by_address[current.address] = true
hydrated[#hydrated + 1] = current
end
end
for _, window in pairs(current_by_address) do
if window and window.address and is_minimized_window(window) and not by_address[window.address] then
by_address[window.address] = true
hydrated[#hydrated + 1] = window
end
end
minimized_windows = hydrated
end
local function float_active_window_preserving_tiled_geometry()
local geometry = tiled_window_geometry(hl.get_active_window())
dispatch(hl.dsp.window.float({ action = "enable", window = geometry and geometry.selector or nil }))
if geometry then
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = geometry.selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = geometry.selector }))
end
return geometry
end
local function float_and_drag_active_window()
float_active_window_preserving_tiled_geometry()
dispatch(hl.dsp.window.drag())
end
local function float_and_resize_active_window()
float_active_window_preserving_tiled_geometry()
dispatch(hl.dsp.window.resize())
end
local function toggle_pinned_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
if not window or not selector then
return
end
if window.pinned then
dispatch(hl.dsp.window.pin({ action = "disable", window = selector }))
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
return
end
if not window.floating then
float_active_window_preserving_tiled_geometry()
end
dispatch(hl.dsp.window.pin({ action = "enable", window = selector }))
end
local function current_minimized_windows()
hydrate_minimized_windows()
local windows = {}
for _, window in ipairs(minimized_windows) do
if window and window.address and is_minimized_window(window) then
windows[#windows + 1] = window
end
end
minimized_windows = windows
return windows
end
local function restore_minimized_window(window, workspace)
if not window or not workspace then
return false
end
move_window_to_workspace(workspace.id, false, window)
return true
end
local function window_picker_candidates_for(mode)
if mode == "minimized" then
return current_minimized_windows()
end
local focused = hl.get_active_window()
local workspace = active_workspace()
local candidates = {}
for _, window in ipairs(normal_windows()) do
local include = true
if mode == "bring" and workspace and window.workspace == workspace then
include = false
elseif mode == "replace" and focused and window == focused then
include = false
end
if include then
candidates[#candidates + 1] = window
end
end
return candidates
end
local function activate_window_picker_candidate(index)
local window = window_picker_candidates[index]
local mode = window_picker_mode
window_picker_mode = nil
window_picker_candidates = {}
dispatch(hl.dsp.submap("reset"))
if not window then
return
end
if mode == "go" then
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
local workspace = active_workspace()
if mode == "bring" and workspace then
move_window_to_workspace(workspace.id, false, window)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
if mode == "minimized" and workspace then
remove_minimized_window(window)
restore_minimized_window(window, workspace)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
if mode == "replace" then
local focused = hl.get_active_window()
if focused and focused ~= window then
dispatch(hl.dsp.window.swap({ target = window_selector(window), window = window_selector(focused) }))
dispatch(hl.dsp.focus({ window = window_selector(window) }))
end
end
end
local function enter_window_picker(mode)
window_picker_mode = mode
window_picker_candidates = window_picker_candidates_for(mode)
if #window_picker_candidates == 0 then
local empty_text = "No windows available"
if mode == "minimized" then
empty_text = "No minimized windows"
end
hl.notification.create({
text = empty_text,
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
local lines = {}
local count = math.min(#window_picker_candidates, 9)
for i = 1, count do
lines[#lines + 1] = window_picker_entry(i, window_picker_candidates[i])
end
hl.notification.create({
text = table.concat(lines, "\n"),
duration = 5000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 11,
})
dispatch(hl.dsp.submap("window-picker"))
end
local function gather_focused_class()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace or not focused.class or focused.class == "" then
return
end
local count = 0
for _, window in ipairs(same_class_windows(focused.class)) do
if window ~= focused and window.workspace ~= workspace then
move_window_to_workspace(workspace.id, false, window)
count = count + 1
end
end
hl.notification.create({
text = "Gathered " .. tostring(count) .. " " .. focused.class .. " windows",
duration = 1600,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function focus_next_class()
local focused = hl.get_active_window()
if not focused or not focused.class or focused.class == "" then
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
return
end
local classes = {}
local first_by_class = {}
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and window.class ~= "" and not first_by_class[window.class] then
first_by_class[window.class] = window
classes[#classes + 1] = window.class
end
end
table.sort(classes)
if #classes <= 1 then
return
end
local current_index = 1
for index, class_name in ipairs(classes) do
if class_name == focused.class then
current_index = index
break
end
end
local next_class = classes[(current_index % #classes) + 1]
local target = first_by_class[next_class]
if target then
dispatch(hl.dsp.focus({ window = window_selector(target) }))
end
end
local function show_active_window_info()
local window = hl.get_active_window()
if not window then
hl.notification.create({
text = "No active window",
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
local workspace = window.workspace and (window.workspace.name or window.workspace.id) or "?"
local lines = {
"Class: " .. tostring(window.class or ""),
"Title: " .. tostring(window.title or ""),
"Workspace: " .. tostring(workspace),
"Pinned: " .. tostring(window.pinned or false),
"Address: " .. tostring(window.address or ""),
"PID: " .. tostring(window.pid or ""),
}
hl.notification.create({
text = table.concat(lines, "\n"),
duration = 5000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 11,
})
end
local function window_has_tag(window, tag)
for _, value in ipairs((window and window.tags) or {}) do
if tostring(value):gsub("%*$", "") == tag then
return true
end
end
return false
end
local function toggle_inactive_opacity_for_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
if not selector then
return
end
local disabling_reduction = not window_has_tag(window, inactive_opacity_override_tag)
dispatch(hl.dsp.window.tag({ tag = inactive_opacity_override_tag, window = selector }))
hl.notification.create({
text = "Inactive opacity reduction: " .. (disabling_reduction and "off for window" or "on for window"),
duration = 1600,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function raise_or_spawn(class_fragment, command)
local fragment = string.lower(class_fragment)
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and string.find(string.lower(window.class), fragment, 1, true) then
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
end
hl.exec_cmd(command)
end
local function minimize_active_window()
local window = hl.get_active_window()
if not window then
return
end
add_minimized_window(window)
move_window_to_workspace(minimized_workspace, false, window)
end
local function restore_last_minimized()
local workspace = active_workspace()
if not workspace then
return
end
hydrate_minimized_windows()
while #minimized_windows > 0 do
local window = table.remove(minimized_windows)
if window and window.address and is_minimized_window(window) then
restore_minimized_window(window, workspace)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
end
end
local function restore_all_minimized()
local workspace = active_workspace()
if not workspace then
return
end
hydrate_minimized_windows()
while #minimized_windows > 0 do
restore_minimized_window(table.remove(minimized_windows), workspace)
end
end
local function minimize_other_classes()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace then
return
end
for _, window in ipairs(tiled_windows(workspace)) do
if window ~= focused and window.class ~= focused.class then
add_minimized_window(window)
move_window_to_workspace(minimized_workspace, false, window)
end
end
end
local function restore_focused_class()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace or not focused.class then
return
end
hydrate_minimized_windows()
local remaining = {}
for _, window in ipairs(minimized_windows) do
if window and window.class == focused.class and is_minimized_window(window) then
restore_minimized_window(window, workspace)
else
remaining[#remaining + 1] = window
end
end
minimized_windows = remaining
end
ctx.same_class_windows = same_class_windows
ctx.short_text = short_text
ctx.normal_windows = normal_windows
ctx.window_picker_entry = window_picker_entry
ctx.remove_minimized_window = remove_minimized_window
ctx.add_minimized_window = add_minimized_window
ctx.hydrate_minimized_windows = hydrate_minimized_windows
ctx.float_active_window_preserving_tiled_geometry = float_active_window_preserving_tiled_geometry
ctx.float_and_drag_active_window = float_and_drag_active_window
ctx.float_and_resize_active_window = float_and_resize_active_window
ctx.toggle_pinned_active_window = toggle_pinned_active_window
ctx.current_minimized_windows = current_minimized_windows
ctx.restore_minimized_window = restore_minimized_window
ctx.window_picker_candidates_for = window_picker_candidates_for
ctx.activate_window_picker_candidate = activate_window_picker_candidate
ctx.enter_window_picker = enter_window_picker
ctx.gather_focused_class = gather_focused_class
ctx.focus_next_class = focus_next_class
ctx.show_active_window_info = show_active_window_info
ctx.toggle_inactive_opacity_for_active_window = toggle_inactive_opacity_for_active_window
ctx.raise_or_spawn = raise_or_spawn
ctx.minimize_active_window = minimize_active_window
ctx.restore_last_minimized = restore_last_minimized
ctx.restore_all_minimized = restore_all_minimized
ctx.minimize_other_classes = minimize_other_classes
ctx.restore_focused_class = restore_focused_class
end
return M

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
default {
shader /run/current-system/sw/share/neowall/shaders/train_journey_optimized.glsl
shader_speed 0.7
shader_fps 30
mode fill
duration 0
}

View File

@@ -0,0 +1,7 @@
default {
shader /run/current-system/sw/share/neowall/shaders/matrix_rain.glsl
shader_speed 0.85
shader_fps 30
mode fill
duration 0
}

View File

@@ -0,0 +1,780 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import Control.Concurrent (forkIO)
import Data.Bits ((.&.), complement)
import Data.Char (toLower)
import Data.Function (on)
import Data.List (find, foldl', isInfixOf, isPrefixOf, minimumBy)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Typeable (Typeable)
import Data.Word (Word32)
import Graphics.X11.ExtraTypes.XF86
import System.Exit (ExitCode(..))
import System.IO (hFlush, stdout)
import System.Process (readCreateProcessWithExitCode, shell, spawnCommand, waitForProcess)
import XMonad
import qualified XMonad.Layout.Renamed as RN
import XMonad.River.WindowManager
import XMonad.River.WindowManager.Wayland
import qualified XMonad.StackSet as W
data Direction = DirectionUp | DirectionDown | DirectionLeft | DirectionRight
deriving (Eq, Show)
data EqualColumns a = EqualColumns
deriving (Read, Show, Typeable)
instance LayoutClass EqualColumns a where
description _ = "Columns"
pureLayout _ rect stack =
zip windows (equalColumnRects rect (length windows))
where
windows = W.integrate stack
main :: IO ()
main = do
let bindings = keyBindings
configLog $ "starting imalison-river-xmonad with keybindings=" ++ show (length bindings)
initialState <- initialRiverWMState riverConfig
runRiverWMWaylandConfig
RiverWMWaylandConfig
{ riverWMWaylandInitialState = initialState
, riverWMWaylandKeyBindings = bindings
}
riverLayouts =
renamed "Columns" EqualColumns
||| Full
where
renamed name = RN.renamed [RN.Replace name]
riverConfig =
(defaultRiverWMConfig riverLayouts)
{ riverWMWorkspaces = ordinaryWorkspaces ++ specialWorkspaces
, riverWMMouseFollowsFocus = True
, riverWMBorderWidth = 2
, riverWMFocusedBorderColor = rgba8 0xed 0xb4 0x43 0xee
, riverWMUnfocusedBorderColor = rgba8 0x59 0x59 0x59 0xaa
}
rgba8 :: Word32 -> Word32 -> Word32 -> Word32 -> RiverWMColor
rgba8 red green blue alpha =
RiverWMColor (wide red) (wide green) (wide blue) (wide alpha)
where
wide component = component * 0x01010101
keyBindings
:: (LayoutClass l Window, Read (l Window))
=> [RiverWMWaylandKeyBinding l]
keyBindings =
addHyperChordBindings hyper hyperChord $
concat
[ directionalBindings super directionalFocus
, directionalBindings (super .|. shift) directionalSwap
, directionalBindings (super .|. ctrl) (shiftFocusedToDirectionalScreen False)
, directionalBindings (super .|. ctrl .|. shift) shiftFocusedToEmptyWorkspaceOnDirectionalScreen
, directionalBindings hyper focusDirectionalScreen
, directionalBindings (hyper .|. shift) (shiftFocusedToDirectionalScreen True)
, workspaceBindings
, layoutBindings
, spawnBindings
, mediaBindings
]
directionalBindings
:: RiverWMWaylandModifiers
-> (Direction -> RiverWMWaylandAction l)
-> [RiverWMWaylandKeyBinding l]
directionalBindings mods command =
[ key mods xK_w (command DirectionUp)
, key mods xK_s (command DirectionDown)
, key mods xK_a (command DirectionLeft)
, key mods xK_d (command DirectionRight)
]
workspaceBindings
:: [RiverWMWaylandKeyBinding l]
workspaceBindings =
[ key (mods .|. super) keysym (action $ command workspace)
| (workspace, keysym) <- zip (map show [(1 :: Int) .. 9]) [xK_1 .. xK_9]
, (command, mods, action) <-
[ (W.greedyView, noMods, stackAction)
, (W.shift, shift, stackAction)
, (\workspaceId stackSet -> W.greedyView workspaceId (W.shift workspaceId stackSet), ctrl, stackActionWarpPointer)
]
]
layoutBindings
:: (LayoutClass l Window, Read (l Window))
=> [RiverWMWaylandKeyBinding l]
layoutBindings =
[ key super xK_space (layoutAction NextLayout)
, key (super .|. shift) xK_space (layoutAction (JumpToLayout "Columns"))
, key (super .|. ctrl) xK_space (layoutAction (JumpToLayout "Full"))
, key super xK_bracketleft (layoutAction Shrink)
, key super xK_bracketright (layoutAction Expand)
, key super xK_comma (layoutAction (IncMasterN 1))
, key super xK_period (layoutAction (IncMasterN (-1)))
]
spawnBindings
:: [RiverWMWaylandKeyBinding l]
spawnBindings =
[ key super xK_Return (spawnAction "ghostty --gtk-single-instance=false")
, key (super .|. shift) xK_Return (spawnAction "ghostty --gtk-single-instance=false")
, key super xK_p (spawnAction "rofi -show drun -show-icons")
, key (super .|. shift) xK_p (spawnAction "rofi -show run")
, key super xK_Tab (selectWindowAction "windows" focusSelectedWindow)
, key super xK_g (selectWindowAction "go to window" focusSelectedWindow)
, key super xK_b (selectWindowAction "bring window" bringSelectedWindow)
, key (super .|. shift) xK_b (selectWindowAction "replace window" replaceSelectedWindow)
, key super xK_m minimizeFocusedWindow
, key (super .|. shift) xK_m restoreLastMinimizedWindow
, key super xK_q (spawnAction "river-xmonad-restart")
, key (super .|. shift) xK_c closeFocusedWindow
, key (super .|. shift) xK_q (spawnAction "riverctl exit")
, key (super .|. alt) xK_e (toggleScratchpad "element")
, key (super .|. alt) xK_h (toggleScratchpad "htop")
, key (super .|. alt) xK_k (toggleScratchpad "slack")
, key (super .|. alt) xK_s (toggleScratchpad "spotify")
, key (super .|. alt) xK_t (toggleScratchpad "transmission")
, key (super .|. alt) xK_v (toggleScratchpad "volume")
, key (super .|. alt) xK_c (spawnAction "google-chrome-stable")
, key super xK_e (spawnAction "emacsclient --eval '(emacs-everywhere)'")
, key (super .|. ctrl) xK_e (shiftFocusedToNextEmptyWorkspace False)
, key (super .|. shift) xK_e (shiftFocusedToNextEmptyWorkspace True)
, key super xK_v (spawnAction "wl-paste | wtype -")
, key super xK_x (spawnAction "rofi_command.sh")
, key hyper xK_e viewNextEmptyWorkspace
, key hyper xK_v (spawnAction "rofi -modi 'clipboard:greenclip print' -show clipboard")
, key hyper xK_p (spawnAction "rofi-pass")
, key noMods xK_Print (spawnAction "flameshot gui")
, key hyper xK_h (spawnAction "flameshot gui")
, key hyper xK_c (spawnAction "shell_command.sh")
, key hyper xK_g gatherFocusedAppId
, key (hyper .|. shift) xK_l (spawnAction "loginctl lock-session")
, key hyper xK_k (spawnAction "rofi_kill_process.sh")
, key (hyper .|. shift) xK_k (spawnAction "rofi_kill_all.sh")
, key hyper xK_r (spawnAction "rofi_systemd_mono")
, key hyper xK_9 (spawnAction "start_synergy.sh")
, key hyper xK_backslash (spawnAction "$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")
, key hyper xK_i (spawnAction "rofi_select_input.hs")
, key hyper xK_o (spawnAction "rofi_paswitch")
, key hyper xK_comma (spawnAction "rofi_wallpaper.sh")
, key hyper xK_slash (spawnAction "toggle_taffybar")
, key hyper xK_y (spawnAction "rofi_agentic_skill")
]
mediaBindings
:: [RiverWMWaylandKeyBinding l]
mediaBindings =
[ key super xK_semicolon (spawnAction "playerctl play-pause")
, key noMods xF86XK_AudioPause (spawnAction "playerctl play-pause")
, key noMods xF86XK_AudioPlay (spawnAction "playerctl play-pause")
, key super xK_l (spawnAction "playerctl next")
, key noMods xF86XK_AudioNext (spawnAction "playerctl next")
, key super xK_j (spawnAction "playerctl previous")
, key noMods xF86XK_AudioPrev (spawnAction "playerctl previous")
, key noMods xF86XK_AudioRaiseVolume (spawnAction "set_volume --unmute --change-volume +5")
, key noMods xF86XK_AudioLowerVolume (spawnAction "set_volume --unmute --change-volume -5")
, key noMods xF86XK_AudioMute (spawnAction "set_volume --toggle-mute")
, key super xK_i (spawnAction "set_volume --unmute --change-volume +5")
, key super xK_k (spawnAction "set_volume --unmute --change-volume -5")
, key super xK_u (spawnAction "set_volume --toggle-mute")
, key (hyper .|. shift) xK_q (spawnAction "toggle_mute_current_window.sh")
, key (hyper .|. ctrl) xK_q (spawnAction "toggle_mute_current_window.sh only")
, key noMods xF86XK_MonBrightnessUp (spawnAction "brightness.sh up")
, key noMods xF86XK_MonBrightnessDown (spawnAction "brightness.sh down")
]
key
:: RiverWMWaylandModifiers
-> KeySym
-> RiverWMWaylandAction l
-> RiverWMWaylandKeyBinding l
key modifiers keysym action =
RiverWMWaylandKeyBinding
{ riverWMWaylandKeyModifiers = modifiers
, riverWMWaylandKeyKeysym = fromIntegral keysym
, riverWMWaylandKeyAction = action
}
spawnAction :: String -> RiverWMWaylandAction l
spawnAction command state = do
configLog $ "spawn start: " ++ command
process <- spawnCommand (riverSpawnPrelude ++ command)
_ <- forkIO $ do
exitCode <- waitForProcess process
configLog $ "spawn exit: " ++ command ++ " -> " ++ show exitCode
pure ()
pure ([], state)
riverSpawnPrelude :: String
riverSpawnPrelude =
"XDG_RUNTIME_DIR=\"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}\"; "
++ "export XDG_RUNTIME_DIR; "
++ "if [ -z \"${WAYLAND_DISPLAY:-}\" ]; then "
++ "for socket in \"$XDG_RUNTIME_DIR\"/wayland-*; do "
++ "[ -S \"$socket\" ] || continue; "
++ "WAYLAND_DISPLAY=\"$(basename \"$socket\")\"; "
++ "break; "
++ "done; "
++ "fi; "
++ "export WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:-wayland-1}\"; "
++ "export XDG_CURRENT_DESKTOP=river; "
++ "export XDG_SESSION_DESKTOP=river-xmonad; "
++ "export XDG_SESSION_TYPE=wayland; "
++ "export IMALISON_SESSION_TYPE=wayland; "
++ "export IMALISON_WINDOW_MANAGER=river-xmonad; "
configLog :: String -> IO ()
configLog message = do
putStrLn $ "imalison-river-xmonad: " ++ message
hFlush stdout
layoutAction
:: (LayoutClass l Window, Read (l Window), Message message)
=> message
-> RiverWMWaylandAction l
layoutAction = handleRiverWMLayoutMessage
stackAction
:: (W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail
-> W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail)
-> RiverWMWaylandAction l
stackAction f state =
pure $ modifyRiverWMStackSet f state
stackActionWarpPointer
:: (W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail
-> W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail)
-> RiverWMWaylandAction l
stackActionWarpPointer f state =
pure $ modifyRiverWMStackSetAndWarpPointer f state
data ScratchpadDefinition = ScratchpadDefinition
{ scratchpadName :: !String
, scratchpadCommand :: !String
, scratchpadMatches :: !(RiverWMWindowState -> Bool)
}
ordinaryWorkspaces :: [WorkspaceId]
ordinaryWorkspaces = map show [(1 :: Int) .. 9]
minimizedWorkspace :: WorkspaceId
minimizedWorkspace = "__minimized"
specialWorkspaces :: [WorkspaceId]
specialWorkspaces =
minimizedWorkspace : map (scratchpadWorkspace . scratchpadName) scratchpadDefinitions
scratchpadWorkspace :: String -> WorkspaceId
scratchpadWorkspace name = "__scratchpad:" ++ name
isSpecialWorkspace :: WorkspaceId -> Bool
isSpecialWorkspace workspace =
workspace == minimizedWorkspace || "__scratchpad:" `isPrefixOf` workspace
scratchpadDefinitions :: [ScratchpadDefinition]
scratchpadDefinitions =
[ ScratchpadDefinition "element" "element-desktop" $
anyMatcher [appIdMatches "Element", appIdMatches "element"]
, ScratchpadDefinition "htop" "ghostty --title=htop -e htop" $
titleContains "htop"
, ScratchpadDefinition "slack" "slack" $
anyMatcher [appIdMatches "Slack", appIdMatches "slack"]
, ScratchpadDefinition "spotify" "spotify" $
anyMatcher [appIdMatches "Spotify", appIdMatches "spotify"]
, ScratchpadDefinition "transmission" "transmission-gtk" $
anyMatcher [titleContains "Transmission", appIdContains "transmission"]
, ScratchpadDefinition "volume" "pavucontrol" $
anyMatcher [appIdMatches "Pavucontrol", appIdContains "pavucontrol"]
]
anyMatcher :: [RiverWMWindowState -> Bool] -> RiverWMWindowState -> Bool
anyMatcher matchers windowState =
any ($ windowState) matchers
appIdMatches :: String -> RiverWMWindowState -> Bool
appIdMatches expected windowState =
lower expected == maybe "" lower (riverWMWindowAppId windowState)
appIdContains :: String -> RiverWMWindowState -> Bool
appIdContains needle windowState =
lower needle `isInfixOf` maybe "" lower (riverWMWindowAppId windowState)
titleContains :: String -> RiverWMWindowState -> Bool
titleContains needle windowState =
lower needle `isInfixOf` maybe "" lower (riverWMWindowTitle windowState)
lower :: String -> String
lower = map toLower
closeFocusedWindow :: RiverWMWaylandAction l
closeFocusedWindow state@RiverWMState{riverWMStackSet, riverWMWindowIds} =
pure
( maybe [] ((: []) . RiverWMCloseWindow) $
W.peek riverWMStackSet >>= (`M.lookup` riverWMWindowIds)
, state
)
minimizeFocusedWindow :: RiverWMWaylandAction l
minimizeFocusedWindow =
stackAction $ W.shift minimizedWorkspace
restoreLastMinimizedWindow :: RiverWMWaylandAction l
restoreLastMinimizedWindow =
stackActionWarpPointer $ \stackSet ->
case workspaceFocusedWindow minimizedWorkspace stackSet of
Nothing -> stackSet
Just window ->
let currentTag = W.currentTag stackSet
in W.focusWindow window (W.shiftWin currentTag window stackSet)
toggleScratchpad :: String -> RiverWMWaylandAction l
toggleScratchpad name state@RiverWMState{riverWMStackSet} =
case find ((== name) . scratchpadName) scratchpadDefinitions of
Nothing ->
pure ([], state)
Just scratchpad ->
case W.peek riverWMStackSet of
Just focused | focused `elem` matchingWindows ->
pure $ modifyRiverWMStackSet (W.shift $ scratchpadWorkspace name) state
_ ->
case matchingWindows of
window : _ ->
pure $ modifyRiverWMStackSetAndWarpPointer (showScratchpadWindow window) state
[] ->
spawnAction (scratchpadCommand scratchpad) state
where
matchingWindows = scratchpadWindows scratchpad state
showScratchpadWindow window stackSet =
let currentTag = W.currentTag stackSet
in W.float window nearFullScratchpadRect $
W.focusWindow window (W.shiftWin currentTag window stackSet)
nearFullScratchpadRect :: W.RationalRect
nearFullScratchpadRect =
W.RationalRect left top width height
where
width = 0.9
height = 0.9
left = 0.95 - width
top = 0.95 - height
scratchpadWindows :: ScratchpadDefinition -> RiverWMState l -> [Window]
scratchpadWindows ScratchpadDefinition{scratchpadMatches} RiverWMState{riverWMWindows} =
[ riverWMWindowXWindow windowState
| windowState <- M.elems riverWMWindows
, scratchpadMatches windowState
]
selectWindowAction
:: String
-> (Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l))
-> RiverWMWaylandAction l
selectWindowAction prompt action state = do
selected <- rofiSelectWindow prompt state
pure $ maybe ([], state) (`action` state) selected
focusSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
focusSelectedWindow window state =
modifyRiverWMStackSetAndWarpPointer (focusWindowEverywhere window) state
bringSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
bringSelectedWindow window state =
modifyRiverWMStackSetAndWarpPointer (bringWindowToCurrentWorkspace window) state
replaceSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
replaceSelectedWindow selected state =
modifyRiverWMStackSetAndWarpPointer replaceWindow state
where
replaceWindow stackSet =
case (W.peek stackSet, W.findTag selected stackSet) of
(Just focused, Just selectedWorkspace)
| focused /= selected ->
W.focusWindow selected $
W.shiftWin selectedWorkspace focused $
W.shiftWin (W.currentTag stackSet) selected stackSet
_ -> stackSet
gatherFocusedAppId :: RiverWMWaylandAction l
gatherFocusedAppId state@RiverWMState{riverWMStackSet, riverWMWindowIds, riverWMWindows} =
pure $ modifyRiverWMStackSet gatherMatching state
where
focusedAppId = do
focused <- W.peek riverWMStackSet
windowId <- M.lookup focused riverWMWindowIds
riverWMWindowAppId =<< M.lookup windowId riverWMWindows
matchingWindows =
[ riverWMWindowXWindow windowState
| windowState <- M.elems riverWMWindows
, riverWMWindowAppId windowState == focusedAppId
]
gatherMatching stackSet =
case focusedAppId of
Nothing -> stackSet
Just _ ->
foldl' (\acc window -> W.shiftWin (W.currentTag acc) window acc) stackSet matchingWindows
rofiSelectWindow :: String -> RiverWMState l -> IO (Maybe Window)
rofiSelectWindow prompt state =
case windowEntries state of
[] ->
pure Nothing
entries -> do
(exitCode, selected, _stderr) <-
readCreateProcessWithExitCode
(shell $ "rofi -dmenu -i -show-icons -p " ++ shellQuote prompt)
(concatMap formatWindowEntry entries)
pure $ case exitCode of
ExitSuccess -> parseSelectedWindow selected
_ -> Nothing
data WindowEntry = WindowEntry
{ windowEntryWindow :: !Window
, windowEntryWorkspace :: !WorkspaceId
, windowEntryAppId :: !String
, windowEntryTitle :: !String
}
windowEntries :: RiverWMState l -> [WindowEntry]
windowEntries RiverWMState{riverWMStackSet, riverWMWindowIds, riverWMWindows} =
[ WindowEntry window (W.tag workspace) appId title
| workspace <- W.workspaces riverWMStackSet
, not (isSpecialWorkspace $ W.tag workspace)
, window <- W.integrate' (W.stack workspace)
, let windowId = M.lookup window riverWMWindowIds
, Just windowState <- [windowId >>= (`M.lookup` riverWMWindows)]
, let appId = fromMaybe "window" (riverWMWindowAppId windowState)
title = fromMaybe "" (riverWMWindowTitle windowState)
]
formatWindowEntry :: WindowEntry -> String
formatWindowEntry WindowEntry{..} =
visibleLabel ++ "\0icon\x1f" ++ iconName ++ "\n"
where
visibleLabel =
show windowEntryWindow
++ "\t["
++ windowEntryWorkspace
++ "] "
++ if null windowEntryTitle
then windowEntryAppId
else windowEntryAppId ++ " - " ++ windowEntryTitle
iconName = if null windowEntryAppId then "application-x-executable" else windowEntryAppId
parseSelectedWindow :: String -> Maybe Window
parseSelectedWindow selected =
case reads (takeWhile (/= '\t') $ takeWhile (/= '\0') selected) of
(window, _) : _ -> Just window
[] -> Nothing
focusWindowEverywhere
:: Eq sid
=> Window
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
focusWindowEverywhere window stackSet =
maybe stackSet (\workspace -> W.focusWindow window (W.greedyView workspace stackSet)) $
W.findTag window stackSet
bringWindowToCurrentWorkspace
:: Eq sid
=> Window
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
bringWindowToCurrentWorkspace window stackSet =
W.focusWindow window (W.shiftWin (W.currentTag stackSet) window stackSet)
workspaceFocusedWindow :: WorkspaceId -> W.StackSet WorkspaceId l Window sid sd -> Maybe Window
workspaceFocusedWindow workspace stackSet =
W.focus <$> (W.stack =<< find ((== workspace) . W.tag) (W.workspaces stackSet))
shellQuote :: String -> String
shellQuote value =
"'" ++ concatMap quoteChar value ++ "'"
where
quoteChar '\'' = "'\\''"
quoteChar char = [char]
viewNextEmptyWorkspace :: RiverWMWaylandAction l
viewNextEmptyWorkspace =
stackAction $ \stackSet ->
maybe stackSet (`W.greedyView` stackSet) (nextEmptyWorkspace stackSet)
shiftFocusedToNextEmptyWorkspace :: Bool -> RiverWMWaylandAction l
shiftFocusedToNextEmptyWorkspace follow =
(if follow then stackActionWarpPointer else stackAction) $ \stackSet ->
maybe stackSet (`shiftFocusedToWorkspace` stackSet) (nextEmptyWorkspace stackSet)
where
shiftFocusedToWorkspace workspace stackSet =
let shifted = W.shift workspace stackSet
in if follow then W.greedyView workspace shifted else shifted
nextEmptyWorkspace
:: W.StackSet WorkspaceId l Window sid sd
-> Maybe WorkspaceId
nextEmptyWorkspace stackSet =
find (`workspaceIsEmpty` stackSet) candidates
where
currentTag = W.currentTag stackSet
candidates =
case break (== currentTag) ordinaryWorkspaces of
(_before, []) -> ordinaryWorkspaces
(before, _current : after) -> after ++ before
workspaceIsEmpty
:: WorkspaceId
-> W.StackSet WorkspaceId l Window sid sd
-> Bool
workspaceIsEmpty workspace stackSet =
maybe False (null . W.integrate' . W.stack) $
find ((== workspace) . W.tag) (W.workspaces stackSet)
directionalSwap :: Direction -> RiverWMWaylandAction l
directionalSwap direction state@RiverWMState{riverWMStackSet} =
pure $ modifyRiverWMStackSet swapTarget state
where
target = directionalTargetAmong (W.index riverWMStackSet) direction state
swapTarget stackSet =
maybe (fallbackDirectionalSwap direction stackSet) (`swapFocusedWithWindow` stackSet) target
fallbackDirectionalSwap
:: Direction
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
fallbackDirectionalSwap DirectionUp = W.swapUp
fallbackDirectionalSwap DirectionLeft = W.swapUp
fallbackDirectionalSwap DirectionDown = W.swapDown
fallbackDirectionalSwap DirectionRight = W.swapDown
swapFocusedWithWindow
:: Window
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
swapFocusedWithWindow target stackSet =
case W.peek stackSet of
Just focused | focused /= target ->
W.modify' (swapStackOrder focused target) stackSet
_ -> stackSet
swapStackOrder :: Eq a => a -> a -> W.Stack a -> W.Stack a
swapStackOrder focused target stack =
stackFromListFocused stack focused $
map swapWindow (W.integrate stack)
where
swapWindow window
| window == focused = target
| window == target = focused
| otherwise = window
stackFromListFocused :: Eq a => W.Stack a -> a -> [a] -> W.Stack a
stackFromListFocused fallback focused windows =
case break (== focused) windows of
(before, _focused : after) -> W.Stack focused (reverse before) after
_ -> fallback
focusDirectionalScreen :: Direction -> RiverWMWaylandAction l
focusDirectionalScreen direction =
stackAction $ \stackSet ->
maybe stackSet ((`W.view` stackSet) . W.tag . W.workspace) $
directionalScreenTarget direction stackSet
shiftFocusedToDirectionalScreen :: Bool -> Direction -> RiverWMWaylandAction l
shiftFocusedToDirectionalScreen follow direction =
(if follow then stackActionWarpPointer else stackAction) $ \stackSet ->
maybe stackSet (shiftToScreen stackSet) $
directionalScreenTarget direction stackSet
where
shiftToScreen stackSet screen =
let workspace = W.tag (W.workspace screen)
shifted = W.shift workspace stackSet
in if follow then W.view workspace shifted else shifted
shiftFocusedToEmptyWorkspaceOnDirectionalScreen :: Direction -> RiverWMWaylandAction l
shiftFocusedToEmptyWorkspaceOnDirectionalScreen direction =
stackActionWarpPointer $ \stackSet ->
maybe stackSet (shiftToEmptyWorkspaceOnScreen stackSet) $
directionalScreenTarget direction stackSet
where
shiftToEmptyWorkspaceOnScreen stackSet screen =
let workspace = W.tag (W.workspace screen)
onDestination = W.view workspace (W.shift workspace stackSet)
in maybe onDestination
(\emptyWorkspace -> W.greedyView emptyWorkspace (W.shift emptyWorkspace onDestination))
(nextEmptyWorkspace onDestination)
directionalFocus :: Direction -> RiverWMWaylandAction l
directionalFocus direction state =
pure $ modifyRiverWMStackSet focusDirectionalWindow state
where
focusDirectionalWindow stackSet =
maybe (fallbackDirectionalFocus direction stackSet) (`W.focusWindow` stackSet) $
directionalTarget direction state
fallbackDirectionalFocus
:: Direction
-> W.StackSet WorkspaceId l Window sid sd
-> W.StackSet WorkspaceId l Window sid sd
fallbackDirectionalFocus DirectionUp = W.focusUp
fallbackDirectionalFocus DirectionLeft = W.focusUp
fallbackDirectionalFocus DirectionDown = W.focusDown
fallbackDirectionalFocus DirectionRight = W.focusDown
directionalTarget :: Direction -> RiverWMState l -> Maybe Window
directionalTarget direction state@RiverWMState{riverWMStackSet} =
directionalTargetAmong (W.index riverWMStackSet) direction state
directionalTargetAmong :: [Window] -> Direction -> RiverWMState l -> Maybe Window
directionalTargetAmong allowed direction RiverWMState{riverWMStackSet, riverWMWindows, riverWMWindowIds} = do
focused <- W.peek riverWMStackSet
focusedId <- M.lookup focused riverWMWindowIds
focusedRect <- riverWMWindowDesired =<< M.lookup focusedId riverWMWindows
let focusedCenter = rectCenter focusedRect
candidates =
[ (window, directionScore direction focusedCenter (rectCenter rect))
| (windowId, RiverWMWindowState{riverWMWindowXWindow = window, riverWMWindowDesired = Just rect}) <-
M.toList riverWMWindows
, windowId /= focusedId
, window `elem` allowed
]
viable = mapMaybe sequenceCandidate candidates
fst <$> minimumMaybeBy (compare `on` snd) viable
directionalScreenTarget
:: Direction
-> W.StackSet WorkspaceId l Window sid ScreenDetail
-> Maybe (W.Screen WorkspaceId l Window sid ScreenDetail)
directionalScreenTarget direction stackSet =
fst <$> minimumMaybeBy (compare `on` snd) viable
where
focusedCenter = screenCenter (W.current stackSet)
candidates =
[ (screen, directionScore direction focusedCenter (screenCenter screen))
| screen <- W.visible stackSet
]
viable = mapMaybe sequenceCandidate candidates
screenCenter :: W.Screen WorkspaceId l Window sid ScreenDetail -> (Double, Double)
screenCenter = rectCenter . screenRect . W.screenDetail
equalColumnRects :: Rectangle -> Int -> [Rectangle]
equalColumnRects _ count | count <= 0 = []
equalColumnRects rect 1 = [rect]
equalColumnRects (Rectangle x y width height) count =
[ Rectangle
(x + fromIntegral riverOuterGap + fromIntegral (columnOffset index))
(y + fromIntegral riverOuterGap)
(fromIntegral (columnWidth index))
contentHeight
| index <- [0 .. count - 1]
]
where
totalWidth = max 0 (fromIntegral width - 2 * riverOuterGap - riverInnerGap * (count - 1))
contentHeight = fromIntegral (max 1 (fromIntegral height - 2 * riverOuterGap :: Int))
baseWidth = totalWidth `div` count
extraPixels = totalWidth `mod` count
columnWidth index = baseWidth + if index < extraPixels then 1 else 0
columnOffset index = index * baseWidth + min index extraPixels + index * riverInnerGap
riverOuterGap :: Int
riverOuterGap = 10
riverInnerGap :: Int
riverInnerGap = 5
sequenceCandidate :: (a, Maybe b) -> Maybe (a, b)
sequenceCandidate (value, Just score) = Just (value, score)
sequenceCandidate (_, Nothing) = Nothing
rectCenter :: Rectangle -> (Double, Double)
rectCenter (Rectangle x y width height) =
( fromIntegral x + fromIntegral width / 2
, fromIntegral y + fromIntegral height / 2
)
directionScore :: Direction -> (Double, Double) -> (Double, Double) -> Maybe (Double, Double)
directionScore direction (fx, fy) (cx, cy) =
case direction of
DirectionUp | cy < fy -> Just (fy - cy, abs (cx - fx))
DirectionDown | cy > fy -> Just (cy - fy, abs (cx - fx))
DirectionLeft | cx < fx -> Just (fx - cx, abs (cy - fy))
DirectionRight | cx > fx -> Just (cx - fx, abs (cy - fy))
_ -> Nothing
minimumMaybeBy :: (a -> a -> Ordering) -> [a] -> Maybe a
minimumMaybeBy _ [] = Nothing
minimumMaybeBy compareFn xs = Just (minimumBy compareFn xs)
addHyperChordBindings
:: RiverWMWaylandModifiers
-> RiverWMWaylandModifiers
-> [RiverWMWaylandKeyBinding l]
-> [RiverWMWaylandKeyBinding l]
addHyperChordBindings hyperMask chordMask bindings =
bindings ++ M.elems chosen
where
existingKeys =
M.fromList
[ ((riverWMWaylandKeyModifiers binding, riverWMWaylandKeyKeysym binding), ())
| binding <- bindings
]
chordBinding binding@RiverWMWaylandKeyBinding{riverWMWaylandKeyModifiers} =
binding
{ riverWMWaylandKeyModifiers =
(riverWMWaylandKeyModifiers .&. complement hyperMask) .|. chordMask
}
candidates =
[ ( (riverWMWaylandKeyModifiers chorded, riverWMWaylandKeyKeysym chorded)
, (score (riverWMWaylandKeyModifiers binding), chorded)
)
| binding <- bindings
, riverWMWaylandKeyModifiers binding .&. hyperMask /= 0
, let chorded = chordBinding binding
, M.notMember (riverWMWaylandKeyModifiers chorded, riverWMWaylandKeyKeysym chorded) existingKeys
]
chosen =
fmap snd $
foldl' keepBest M.empty candidates
keepBest selected (bindingKey, candidate@(candidateScore, _binding)) =
case M.lookup bindingKey selected of
Nothing -> M.insert bindingKey candidate selected
Just (bestScore, _) ->
if candidateScore < bestScore
then M.insert bindingKey candidate selected
else selected
score modifiers =
length $
filter (/= 0)
[ modifiers .&. shift
, modifiers .&. ctrl
, modifiers .&. alt
, modifiers .&. hyper
, modifiers .&. super
, modifiers .&. riverWMWaylandModifierMod5
]
noMods, shift, ctrl, alt, hyper, super, hyperChord :: RiverWMWaylandModifiers
noMods = riverWMWaylandModifierNone
shift = riverWMWaylandModifierShift
ctrl = riverWMWaylandModifierCtrl
alt = riverWMWaylandModifierAlt
hyper = riverWMWaylandModifierHyper
super = riverWMWaylandModifierSuper
hyperChord = ctrl .|. alt .|. super

View File

@@ -0,0 +1,18 @@
cabal-version: 2.4
name: imalison-river-xmonad
version: 0.1.0.0
license: BSD-3-Clause
author: Ivan Malison
maintainer: IvanMalison@gmail.com
build-type: Simple
executable imalison-river-xmonad
main-is: Main.hs
build-depends: base >= 4.12 && < 5
, containers
, process
, X11
, xmonad
, xmonad-contrib
ghc-options: -threaded -Wall -Wno-unused-do-bind -Wno-deprecations -Wno-missing-signatures -Wno-name-shadowing
default-language: Haskell2010

View File

@@ -0,0 +1,13 @@
_: pkgs: {
haskellPackages = pkgs.haskellPackages.override (old: {
overrides = pkgs.lib.composeExtensions (old.overrides or (_: _: {})) (self: _super: {
imalison-river-xmonad = self.callCabal2nix "imalison-river-xmonad" (
pkgs.lib.sourceByRegex ./.
[
"Main.hs"
"imalison-river-xmonad.cabal"
]
) { };
});
});
}

View File

@@ -0,0 +1,138 @@
configuration {
font: "Roboto 12";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Search";
display-run: "Run";
display-window: "Windows";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
* {
bg: #00000000;
backdrop: #0b102026;
panel: #00000000;
control: #ffffffe0;
candidate: #18203372;
candidate-active:#2430489c;
text: #111827ff;
text-muted: #667085ff;
text-on-dark: #f8fafcff;
text-dark-muted: #d0d6e0ff;
accent: #007affff;
accent-soft: #d8eaffcc;
border: #ffffff96;
hairline: #cfd6df70;
}
window {
transparency: "real";
location: center;
anchor: center;
width: 72%;
height: 78%;
background-color: @backdrop;
text-color: @text;
border: 1px;
border-color: @border;
border-radius: 18px;
}
mainbox {
background-color: @panel;
children: [ inputbar, listview ];
spacing: 10px;
padding: 88px 136px;
margin: 0px;
border: 0px;
border-radius: 0px;
}
inputbar {
background-color: @control;
text-color: @text;
children: [ prompt, entry ];
border: 1px;
border-color: @hairline;
border-radius: 18px;
padding: 13px 15px;
spacing: 8px;
}
prompt {
enabled: true;
background-color: @bg;
text-color: @accent;
font: "Roboto 12";
}
entry {
background-color: @bg;
text-color: @text;
placeholder-color: @text-muted;
placeholder: "";
cursor: text;
expand: true;
}
listview {
background-color: @bg;
columns: 1;
lines: 10;
spacing: 0px;
border: 1px;
border-color: @border;
border-radius: 14px;
cycle: false;
dynamic: true;
layout: vertical;
scrollbar: false;
}
element {
background-color: @bg;
text-color: @text-on-dark;
orientation: horizontal;
border: 0px 0px 1px 0px;
border-color: @hairline;
border-radius: 0px;
padding: 11px 11px;
spacing: 10px;
}
element-icon {
background-color: @bg;
text-color: inherit;
size: 24px;
vertical-align: 0.5;
}
element-text {
background-color: @bg;
text-color: inherit;
vertical-align: 0.5;
horizontal-align: 0;
}
element selected {
background-color: @candidate;
text-color: @text-on-dark;
border-color: @border;
}
element selected element-text {
text-color: @text-on-dark;
}
message {
background-color: @candidate;
border-radius: 14px;
padding: 10px;
}
textbox {
background-color: @bg;
text-color: @text-dark-muted;
}

View File

@@ -1,9 +0,0 @@
/* colors */
* {
al: #00000000;
bg: #000000ff;
se: #101010ff;
fg: #FFFFFFff;
ac: #EC7875ff;
}

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env bash
## Author : Aditya Shakya
## Mail : adi1090x@gmail.com
## Github : @adi1090x
## Twitter : @adi1090x
# Available Styles
# >> Created and tested on : rofi 1.6.0-1
#
# style_1 style_2 style_3 style_4 style_5 style_6
# style_7 style_8 style_9 style_10 style_11 style_12
theme="style_1"
dir="$HOME/.config/rofi/launchers/colorful"
# dark
ALPHA="#00000000"
BG="#000000ff"
FG="#FFFFFFff"
SELECT="#101010ff"
# light
#ALPHA="#00000000"
#BG="#FFFFFFff"
#FG="#000000ff"
#SELECT="#f3f3f3ff"
# accent colors
COLORS=('#EC7875' '#61C766' '#FDD835' '#42A5F5' '#BA68C8' '#4DD0E1' '#00B19F' \
'#FBC02D' '#E57C46' '#AC8476' '#6D8895' '#EC407A' '#B9C244' '#6C77BB')
ACCENT="${COLORS[$(( $RANDOM % 14 ))]}ff"
# overwrite colors file
cat > $dir/colors.rasi <<- EOF
/* colors */
* {
al: $ALPHA;
bg: $BG;
se: $SELECT;
fg: $FG;
ac: $ACCENT;
}
EOF
# comment these lines to disable random style
themes=($(ls -p --hide="launcher.sh" --hide="colors.rasi" $dir))
theme="${themes[$(( $RANDOM % 12 ))]}"
rofi -no-lazy-grab -show drun -modi drun -theme $dir/"$theme"

View File

@@ -1,119 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 12px;
width: 35%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px;
columns: 5;
lines: 3;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 2% 0% 2% 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 64px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.5% -0.5% 0.5%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 12px;
border-color: @bg;
}

View File

@@ -1,123 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 80%;
height: 80%;
}
prompt {
enabled: true;
padding: 1% 0.75% 1% 0.75%;
background-color: @ac;
text-color: @fg;
border-radius: 100%;
font: "Iosevka Nerd Font 12";
}
textbox-prompt-colon {
padding: 1% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search...";
padding: 1.15% 0.5% 1% 0.5%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0% 0.2% 0.3% 0%;
border-radius: 100%;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 3;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 20% 15% 20% 15%;
}
element {
background-color: @se;
text-color: @fg;
orientation: horizontal;
border-radius: 100%;
padding: 1% 0.5% 1% 0.75%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @ac;
border: 0% 0% 0.3% 0.2%;
border-radius: 100%;
border-color: @ac;
}

View File

@@ -1,129 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 25px;
width: 50%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 1.25% 0.75% 1.25% 0.75%;
background-color: @ac;
text-color: @fg;
font: "Iosevka Nerd Font 12";
border-radius: 100%;
}
textbox-prompt-colon {
padding: 1.40% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 1.5% 0.5% 1% 0%;
blink: true;
}
inputbar {
children: [ prompt, textbox-prompt-colon, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 100px;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 3;
lines: 8;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 4% 2% 4% 2%;
}
element {
background-color: @bg;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 1%;
border-color: @ac;
border-radius: 15px;
background-color: @ac;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @ac;
border: 0% 0% 0% 0%;
border-radius: 15px;
border-color: @ac;
}

View File

@@ -1,132 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: " Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 50px;
width: 50%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 1.25% 0.75% 1.25% 0.75%;
background-color: @ac;
text-color: @fg;
font: "Iosevka Nerd Font 12";
border-radius: 100%;
}
textbox-prompt-colon {
padding: 1.40% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 1.5% 0.5% 1% 0%;
blink: true;
}
inputbar {
children: [ prompt, textbox-prompt-colon, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0%;
border-radius: 100%;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 6;
lines: 3;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 10px 0px 10px 0px;
border-radius: 50px;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 4% 2% 2% 2%;
}
element {
background-color: @bg;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 64px;
border: 1%;
border-color: @se;
border-radius: 15px;
background-color: @se;
padding: 2% 1% 2% 1%;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.25% 0.5% 0.25%;
padding: 1% 0.5% 1% 0.5%;
}
element-text selected {
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
background-color: @ac;
text-color: @bg;
border-radius: 100%;
}

View File

@@ -1,119 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 12px;
width: 18%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 0px;
columns: 1;
lines: 5;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 32px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -1,120 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
height: 100%;
width: 18%;
location: west;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 0px;
columns: 1;
lines: 5;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 32px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -1,119 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
height: 100%;
width: 19%;
location: east;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px 10px 0px 10px;
columns: 3;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 2% 0% 2% 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 48px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.5% -0.5% 0.5%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -1,119 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 35%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @fg;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px;
columns: 2;
lines: 10;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @ac;
text-color: @bg;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @bg;
}

View File

@@ -1,116 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 100%;
height: 100%;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @bg;
font: "FantasqueSansMono Nerd Font 12";
}
entry {
background-color: @al;
text-color: @bg;
placeholder-color: @bg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @ac;
text-color: @bg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 100%;
border-color: @ac;
margin: 0% 54.5% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 0px;
columns: 10;
spacing: 0%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2.5%;
padding: 20% 5% 20% 5%;
}
element {
background-color: @al;
text-color: @fg;
orientation: vertical;
border-radius: 0%;
padding: 4% 0% 4% 0%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 80px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0.5;
vertical-align: 0.5;
margin: 0.5% 0.5% -0.5% 0.5%;
}
element selected {
background-color: @se;
text-color: @fg;
border: 0% 0% 0.5% 0%;
border-radius: 25px;
border-color: @ac;
}

View File

@@ -1,118 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Fira Code 10";
show-icons: true;
display-drun: "";
drun-display-format: "{name} {generic}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 12px;
width: 88%;
height: 78%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 0.30% 1% 0% -0.5%;
background-color: @al;
text-color: @fg;
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search";
padding: 0.10% 0% 0% 0%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @bg;
text-color: @fg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
margin: 0% 0% 0% 0%;
padding: 1.5%;
}
listview {
background-color: @al;
padding: 10px;
columns: 1;
lines: 18;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 0%;
padding: 0%;
}
element {
background-color: @al;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 0.5% 0.5% 0.5% 0.5%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @ac;
text-color: @bg;
border: 0% 0% 0% 0%;
border-radius: 12px;
border-color: @bg;
}

View File

@@ -1,125 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 0px;
width: 35%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 1% 0.75% 1% 0.75%;
background-color: @ac;
text-color: @fg;
font: "Iosevka Nerd Font 12";
}
textbox-prompt-colon {
padding: 1% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search...";
padding: 1.15% 0.5% 1% 0.5%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 0px;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 2;
lines: 7;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 4% 2% 4% 2%;
}
element {
background-color: @se;
text-color: @fg;
orientation: horizontal;
border-radius: 0%;
padding: 1% 0.5% 1% 0.75%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @ac;
border: 0% 0% 0% 0.3%;
border-radius: 0px;
border-color: @ac;
}

View File

@@ -1,126 +0,0 @@
/*
*
* Author : Aditya Shakya
* Mail : adi1090x@gmail.com
* Github : @adi1090x
* Twitter : @adi1090x
*
*/
configuration {
font: "Iosevka Nerd Font 10";
show-icons: true;
icon-theme: "Papirus";
display-drun: "Applications";
drun-display-format: "{name}";
disable-history: false;
sidebar-mode: false;
}
@import "colors.rasi"
window {
transparency: "real";
background-color: @bg;
text-color: @fg;
border: 0px;
border-color: @ac;
border-radius: 15px;
width: 35%;
location: center;
x-offset: 0;
y-offset: 0;
}
prompt {
enabled: true;
padding: 1% 0.75% 1% 0.75%;
background-color: @ac;
text-color: @fg;
border-radius: 10px;
font: "Iosevka Nerd Font 12";
}
textbox-prompt-colon {
padding: 1% 0% 1% 0%;
background-color: @se;
text-color: @fg;
expand: false;
str: " :: ";
}
entry {
background-color: @al;
text-color: @fg;
placeholder-color: @fg;
expand: true;
horizontal-align: 0;
placeholder: "Search...";
padding: 1.15% 0.5% 1% 0.5%;
blink: true;
}
inputbar {
children: [ prompt, entry ];
background-color: @se;
text-color: @fg;
expand: false;
border: 0% 0% 0% 0%;
border-radius: 10px;
border-color: @ac;
}
listview {
background-color: @al;
padding: 0px;
columns: 2;
lines: 7;
spacing: 1%;
cycle: false;
dynamic: true;
layout: vertical;
}
mainbox {
background-color: @al;
border: 0% 0% 0% 0%;
border-radius: 0% 0% 0% 0%;
border-color: @ac;
children: [ inputbar, listview ];
spacing: 2%;
padding: 4% 2% 4% 2%;
}
element {
background-color: @se;
text-color: @fg;
orientation: horizontal;
border-radius: 12px;
padding: 1% 0.5% 1% 0.75%;
}
element-icon {
background-color: inherit;
text-color: inherit;
horizontal-align: 0.5;
vertical-align: 0.5;
size: 24px;
border: 0px;
}
element-text {
background-color: @al;
text-color: inherit;
expand: true;
horizontal-align: 0;
vertical-align: 0.5;
margin: 0% 0.25% 0% 0.25%;
}
element selected {
background-color: @se;
text-color: @ac;
border: 0% 0.3% 0% 0.3%;
border-radius: 12px;
border-color: @ac;
}

View File

@@ -1,12 +1,12 @@
configuration {
bw: 0;
padding: 50;
padding: 0;
show-icons: true;
terminal: "alacritty";
sidebar-mode: false;
fullscreen: true;
fullscreen: false;
/* Let rofi auto-detect DPI under Wayland/Xwayland to avoid blurry scaling. */
dpi: 0;
}
@theme "colorful/style_7.rasi"
@theme "apple-frost.rasi"

View File

@@ -7,14 +7,16 @@
## Multiplexer session titling
- If the `TMUX` or `ZELLIJ` environment variable is set, treat this chat as the controller for the current tmux or zellij session.
- Use `set_multiplexer_title '<project> - <task>'` to update the title. The command detects tmux vs. zellij internally, prefers tmux when both are present, and no-ops outside a multiplexer.
- Maintain a session/window/pane title that updates when the task focus changes substantially.
- Prefer automatic titling: infer a concise <task> from the current user request and context without asking.
- Maintain a session/window/pane title that describes the durable purpose of the overall exchange.
- Prefer automatic titling: infer a concise <task> from the current user request and the existing chat context without asking.
- Choose holistic titles over granular turn summaries. The title should answer "what has this chat been for?" rather than describe the latest command, substep, clarification, or follow-up message.
- Preserve the existing <task> when the new user turn is a continuation, status check, refinement, or implementation detail within the same broader objective.
- Title format: "<project> - <task>".
- <project> is the basename of the current project directory.
- Prefer git repo root basename if available; otherwise use basename of the current working directory.
- <task> is a short, user-friendly description of what we are doing.
- Ask for a short descriptive <task> only when the task is ambiguous or you are not confident in an inferred title.
- When the task changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
- When the broader objective changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
- When a title is provided or updated, immediately run `set_multiplexer_title '<project> - <task>'`; do not call raw tmux or zellij rename commands unless debugging the helper itself.
## Pane usage

View File

@@ -0,0 +1,320 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module TaffybarConfig.ChromeFavicons
( ChromeFaviconOverlayMode (..),
ChromeFaviconConfig (..),
defaultChromeFaviconConfig,
chromeFaviconIconGetter,
)
where
import Control.Exception (IOException, try)
import Control.Monad (unless, when)
import Control.Monad.IO.Class (liftIO)
import Data.Char (isAlphaNum)
import Data.Int (Int32)
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified GI.GdkPixbuf.Enums as GdkPixbuf
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import System.Directory
( createDirectoryIfMissing,
doesFileExist,
getFileSize,
renameFile,
)
import System.Environment.XDG.BaseDir (getUserCacheDir)
import System.Exit (ExitCode (ExitSuccess))
import System.FilePath ((</>))
import System.Process (readProcessWithExitCode)
import qualified System.Taffybar.Information.Workspaces.Model as WorkspaceModel
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import System.Taffybar.Widget.Util (loadPixbufByName)
data ChromeFaviconOverlayMode
= FaviconWithChromeOverlay
| ChromeWithFaviconOverlay
deriving (Eq, Show)
data ChromeFaviconConfig = ChromeFaviconConfig
{ chromeFaviconOverlayMode :: ChromeFaviconOverlayMode,
chromeFaviconOverlayRatio :: Double,
chromeFaviconEnabled :: Bool
}
deriving (Eq, Show)
defaultChromeFaviconConfig :: ChromeFaviconConfig
defaultChromeFaviconConfig =
ChromeFaviconConfig
{ chromeFaviconOverlayMode = FaviconWithChromeOverlay,
chromeFaviconOverlayRatio = 0.45,
chromeFaviconEnabled = True
}
data BridgePayload = BridgePayload
{ payloadMappedWindowId :: Text,
payloadFaviconURL :: Text
}
deriving (Eq, Show)
chromeFaviconIconGetter :: ChromeFaviconConfig -> Workspaces.WindowIconPixbufGetter
chromeFaviconIconGetter cfg =
Workspaces.handleIconGetterException $ \size windowInfo ->
if chromeFaviconEnabled cfg && isChromeWindow windowInfo
then liftIO $ chromeFaviconPixbuf cfg size windowInfo
else pure Nothing
chromeFaviconPixbuf ::
ChromeFaviconConfig ->
Int32 ->
WorkspaceModel.WindowInfo ->
IO (Maybe Gdk.Pixbuf)
chromeFaviconPixbuf cfg size windowInfo = do
payload <- getChromeWindowInfoPayload windowInfo
case payload of
Just p
| payloadMatchesWindow windowInfo p,
validFaviconURL (payloadFaviconURL p) -> do
mFavicon <- loadCachedFavicon size (payloadFaviconURL p)
mChrome <- runChromeIconGetter size windowInfo
case (mFavicon, mChrome) of
(Just favicon, Just chromeIcon) ->
Just <$> composeChromeFavicon cfg size favicon chromeIcon
(Just favicon, Nothing) -> Just <$> scalePixbuf size favicon
_ -> pure Nothing
_ -> pure Nothing
runChromeIconGetter :: Int32 -> WorkspaceModel.WindowInfo -> IO (Maybe Gdk.Pixbuf)
runChromeIconGetter size _ =
loadPixbufByName size "google-chrome"
payloadMatchesWindow :: WorkspaceModel.WindowInfo -> BridgePayload -> Bool
payloadMatchesWindow windowInfo payload =
normalizedHyprlandWindowId windowInfo == Just (normalizeAddress (payloadMappedWindowId payload))
normalizedHyprlandWindowId :: WorkspaceModel.WindowInfo -> Maybe Text
normalizedHyprlandWindowId windowInfo =
case WorkspaceModel.windowIdentity windowInfo of
WorkspaceModel.HyprlandWindowIdentity address -> Just (normalizeAddress address)
WorkspaceModel.X11WindowIdentity _ -> Nothing
isChromeWindow :: WorkspaceModel.WindowInfo -> Bool
isChromeWindow windowInfo =
any looksLikeChrome (WorkspaceModel.windowClassHints windowInfo)
looksLikeChrome :: Text -> Bool
looksLikeChrome raw =
let lowered = T.toLower raw
in any (`T.isInfixOf` lowered) ["chrome", "chromium", "brave", "edge", "vivaldi"]
normalizeAddress :: Text -> Text
normalizeAddress address =
let trimmed = T.strip address
in if "0x" `T.isPrefixOf` trimmed || T.null trimmed
then trimmed
else "0x" <> trimmed
validFaviconURL :: Text -> Bool
validFaviconURL url =
any (`T.isPrefixOf` url) ["https://", "http://"]
getChromeWindowInfoPayload :: WorkspaceModel.WindowInfo -> IO (Maybe BridgePayload)
getChromeWindowInfoPayload windowInfo =
case normalizedHyprlandWindowId windowInfo of
Just windowId -> do
payloads <- getBridgeString "GetWindowPayloads"
payload <- case payloads of
Just payloadText -> extractBridgePayloadForWindow windowId payloadText
Nothing -> pure Nothing
case payload of
Just value -> pure (Just value)
Nothing -> getLastChromeWindowInfoPayload
Nothing -> getLastChromeWindowInfoPayload
getLastChromeWindowInfoPayload :: IO (Maybe BridgePayload)
getLastChromeWindowInfoPayload = do
payload <- getBridgeString "GetLastPayload"
case payload of
Just payloadText -> extractBridgePayload payloadText
Nothing -> pure Nothing
getBridgeString :: String -> IO (Maybe String)
getBridgeString method = do
result <-
try @IOException $
readProcessWithExitCode
"busctl"
[ "--user",
"call",
"org.imalison.ChromeWindowInfo",
"/org/imalison/ChromeWindowInfo",
"org.imalison.ChromeWindowInfo",
method
]
""
case result of
Right (ExitSuccess, stdoutText, _) ->
pure (parseBusctlString stdoutText)
_ -> pure Nothing
parseBusctlString :: String -> Maybe String
parseBusctlString output = do
rest <- T.stripPrefix "s " (T.strip (T.pack output))
decodeQuotedString (T.unpack rest)
decodeQuotedString :: String -> Maybe String
decodeQuotedString raw =
case reads raw of
[(decoded, trailing)] | all (`elem` (" \n\t\r" :: String)) trailing -> Just decoded
_ -> Nothing
extractBridgePayload :: String -> IO (Maybe BridgePayload)
extractBridgePayload payload = do
let jqFilter = "[.bridge.mapped_window.window_id // \"\", .tab.favicon_url // \"\"] | @tsv"
(code, stdoutText, _) <- readProcessWithExitCode "jq" ["-r", jqFilter] payload
pure $
case (code, T.splitOn "\t" (T.strip (T.pack stdoutText))) of
(ExitSuccess, [mappedWindowId, faviconURL])
| not (T.null mappedWindowId),
not (T.null faviconURL) ->
Just (BridgePayload mappedWindowId faviconURL)
_ -> Nothing
extractBridgePayloadForWindow :: Text -> String -> IO (Maybe BridgePayload)
extractBridgePayloadForWindow windowId payloads = do
let jqFilter = ".[$window_id] // empty | [.bridge.mapped_window.window_id // \"\", .tab.favicon_url // \"\"] | @tsv"
(code, stdoutText, _) <-
readProcessWithExitCode
"jq"
["-r", "--arg", "window_id", T.unpack windowId, jqFilter]
payloads
pure $
case (code, T.splitOn "\t" (T.strip (T.pack stdoutText))) of
(ExitSuccess, [mappedWindowId, faviconURL])
| not (T.null mappedWindowId),
not (T.null faviconURL) ->
Just (BridgePayload mappedWindowId faviconURL)
_ -> Nothing
loadCachedFavicon :: Int32 -> Text -> IO (Maybe Gdk.Pixbuf)
loadCachedFavicon size url = do
path <- ensureCachedFavicon url
case path of
Just faviconPath ->
try @IOException (Gdk.pixbufNewFromFileAtScale faviconPath size size True) >>= \case
Right (Just pixbuf) -> pure (Just pixbuf)
Right Nothing -> pure Nothing
Left _ -> pure Nothing
Nothing -> pure Nothing
ensureCachedFavicon :: Text -> IO (Maybe FilePath)
ensureCachedFavicon url = do
cacheRoot <- getUserCacheDir "taffybar/chrome-favicons"
let rawDir = cacheRoot </> "raw"
createDirectoryIfMissing True rawDir
hash <- hashText url
let path = rawDir </> (hash <> faviconExtension url)
cached <- nonEmptyFileExists path
unless cached $
downloadFavicon url path
exists <- nonEmptyFileExists path
pure $ if exists then Just path else Nothing
hashText :: Text -> IO String
hashText value = do
(code, stdoutText, _) <-
readProcessWithExitCode "sha256sum" [] (T.unpack value)
pure $
if code == ExitSuccess
then takeWhile (/= ' ') stdoutText
else safeFileComponent value
safeFileComponent :: Text -> String
safeFileComponent =
take 96 . map normalizeChar . T.unpack
where
normalizeChar c
| isAlphaNum c = c
| otherwise = '-'
faviconExtension :: Text -> String
faviconExtension url =
fromMaybe ".img" $
listToMaybe
[ T.unpack ext
| ext <- [".svg", ".png", ".ico", ".jpg", ".jpeg", ".webp", ".gif"] :: [Text],
ext `T.isSuffixOf` T.toLower pathOnly
]
where
pathOnly = T.takeWhile (/= '?') url
downloadFavicon :: Text -> FilePath -> IO ()
downloadFavicon url path = do
let tmp = path <> ".tmp"
(code, _, _) <-
readProcessWithExitCode
"curl"
[ "-fsSL",
"--max-time",
"10",
"--retry",
"1",
"-o",
tmp,
T.unpack url
]
""
when (code == ExitSuccess) $
renameFile tmp path
nonEmptyFileExists :: FilePath -> IO Bool
nonEmptyFileExists path = do
exists <- doesFileExist path
if exists
then (> 0) <$> getFileSize path
else pure False
composeChromeFavicon ::
ChromeFaviconConfig ->
Int32 ->
Gdk.Pixbuf ->
Gdk.Pixbuf ->
IO Gdk.Pixbuf
composeChromeFavicon cfg size favicon chromeIcon = do
let (baseSource, overlaySource) =
case chromeFaviconOverlayMode cfg of
FaviconWithChromeOverlay -> (favicon, chromeIcon)
ChromeWithFaviconOverlay -> (chromeIcon, favicon)
base <- scalePixbuf size baseSource
result <- fromMaybe base <$> Gdk.pixbufCopy base
baseWidth <- Gdk.pixbufGetWidth result
baseHeight <- Gdk.pixbufGetHeight result
let baseSize = max 1 (min baseWidth baseHeight)
overlaySize =
max 1 $
min baseSize $
round (fromIntegral baseSize * chromeFaviconOverlayRatio cfg)
overlayX = baseWidth - overlaySize
overlayY = baseHeight - overlaySize
overlay <- scalePixbuf overlaySize overlaySource
Gdk.pixbufComposite
overlay
result
overlayX
overlayY
overlaySize
overlaySize
(fromIntegral overlayX)
(fromIntegral overlayY)
1
1
GdkPixbuf.InterpTypeBilinear
255
pure result
scalePixbuf :: Int32 -> Gdk.Pixbuf -> IO Gdk.Pixbuf
scalePixbuf size pixbuf =
fromMaybe pixbuf <$> Gdk.pixbufScaleSimple pixbuf size size GdkPixbuf.InterpTypeBilinear

View File

@@ -0,0 +1,35 @@
module TaffybarConfig.Config
( mkSimpleTaffyConfig,
)
where
import TaffybarConfig.Host (compactBarHosts, smallBarHosts)
import TaffybarConfig.Widgets (clockWidget, endWidgetsForHost, startWidgetsForHostAndBackend)
import System.Taffybar.Context (Backend)
import System.Taffybar.SimpleConfig
mkSimpleTaffyConfig :: String -> Backend -> [FilePath] -> SimpleTaffyConfig
mkSimpleTaffyConfig hostName backend cssFiles =
defaultSimpleTaffyConfig
{ startWidgets = startWidgetsForHostAndBackend hostName backend,
centerWidgets = [clockWidget],
endWidgets = endWidgetsForHost hostName,
barLevels = Nothing,
barPosition = Top,
widgetSpacing = 0,
barPadding =
if hostName `elem` smallBarHosts
then 1
else
if hostName `elem` compactBarHosts
then 2
else 4,
barHeight =
if hostName `elem` smallBarHosts
then ScreenRatio $ 1 / 48
else
if hostName `elem` compactBarHosts
then ScreenRatio $ 1 / 40
else ScreenRatio $ 1 / 33,
cssPaths = cssFiles
}

View File

@@ -0,0 +1,41 @@
module TaffybarConfig.Host
( compactBarHosts,
cssFilesForHost,
laptopHosts,
smallBarHosts,
)
where
import Data.Maybe (fromMaybe)
-- NOTE: Keep `cssPaths` to a single entrypoint file per host. GTK's
-- `cssProviderLoadFromPath` clears the provider before loading, so handing
-- Taffybar multiple files here causes only the last file to take effect.
defaultCssFiles :: [FilePath]
defaultCssFiles = ["taffybar.css"]
cssFilesByHostname :: [(String, [FilePath])]
cssFilesByHostname =
[ ("ryzen-shine", ["ryzen-shine.css"]),
("strixi-minaj", ["strixi-minaj.css"])
]
compactBarHosts :: [String]
compactBarHosts =
["ryzen-shine"]
smallBarHosts :: [String]
smallBarHosts =
["strixi-minaj"]
laptopHosts :: [String]
laptopHosts =
[ "adell",
"stevie-nixos",
"strixi-minaj",
"jay-lenovo"
]
cssFilesForHost :: String -> [FilePath]
cssFilesForHost hostName =
fromMaybe defaultCssFiles $ lookup hostName cssFilesByHostname

View File

@@ -0,0 +1,80 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module TaffybarConfig.WidgetUtil
( decorateWithClassAndBox,
decorateWithClassAndBoxM,
setFixedLabelWidth,
setLabelAlignmentRecursively,
stackInPill,
usageLogoWidget,
)
where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Foldable (for_)
import Data.GI.Base (castTo)
import Data.Int (Int32)
import Data.Text (Text)
import qualified GI.Gtk as Gtk
import qualified GI.Pango as Pango
import System.Environment.XDG.BaseDir (getUserConfigFile)
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Widget.Util
( buildContentsBox,
pixbufNewFromFileAtScaleByHeight,
widgetSetClassGI,
)
-- | Wrap the widget in a "TaffyBox" (via 'buildContentsBox') and add a CSS class.
decorateWithClassAndBox :: (MonadIO m) => Text -> Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBox klass widget = do
boxed <- buildContentsBox widget
widgetSetClassGI boxed klass
decorateWithClassAndBoxM :: (MonadIO m) => Text -> m Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBoxM klass builder =
builder >>= decorateWithClassAndBox klass
forEachLabelRecursively :: Gtk.Widget -> (Gtk.Label -> IO ()) -> IO ()
forEachLabelRecursively widget action = do
maybeLabel <- castTo Gtk.Label widget
for_ maybeLabel action
maybeContainer <- castTo Gtk.Container widget
case maybeContainer of
Just container ->
Gtk.containerGetChildren container >>= mapM_ (`forEachLabelRecursively` action)
Nothing -> pure ()
setLabelAlignmentRecursively :: Float -> Gtk.Justification -> Gtk.Widget -> IO ()
setLabelAlignmentRecursively xalign justify widget =
forEachLabelRecursively widget $ \label -> do
Gtk.labelSetXalign label xalign
Gtk.labelSetJustify label justify
setFixedLabelWidth :: Int32 -> Gtk.Label -> IO ()
setFixedLabelWidth width label = do
Gtk.labelSetWidthChars label width
Gtk.labelSetMaxWidthChars label width
Gtk.labelSetEllipsize label Pango.EllipsizeModeEnd
stackInPill :: Text -> [TaffyIO Gtk.Widget] -> TaffyIO Gtk.Widget
stackInPill klass builders =
decorateWithClassAndBoxM klass $ do
widgets <- sequence builders
liftIO $ do
box <- Gtk.boxNew Gtk.OrientationVertical 0
mapM_ (\w -> Gtk.boxPackStart box w False False 0) widgets
Gtk.widgetShowAll box
Gtk.toWidget box
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"

View File

@@ -0,0 +1,469 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module TaffybarConfig.Widgets
( clockWidget,
endWidgetsForHost,
startWidgetsForBackend,
startWidgetsForHostAndBackend,
)
where
import Control.Concurrent (threadDelay)
import Control.Monad.IO.Class (liftIO)
import Data.Char (toLower)
import Data.Maybe (fromMaybe)
import Data.Ratio ((%))
import Data.Text (Text)
import qualified Data.Text as T
import qualified GI.Gtk as Gtk
import qualified StatusNotifier.Tray as SNITray
import System.Environment (lookupEnv)
import System.Environment.XDG.BaseDir (getUserConfigFile)
import System.Taffybar.Context
( Backend (BackendWayland, BackendX11),
TaffyIO,
)
import System.Taffybar.Information.Memory (MemoryInfo (..), parseMeminfo)
import qualified System.Taffybar.Information.Workspaces.Hyprland as HyprlandWorkspaces
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget
import qualified System.Taffybar.Widget.ASUS as ASUS
import qualified System.Taffybar.Widget.Audio as Audio
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
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
OpenAIUsageStackConfig (..),
defaultOpenAIUsageStackConfig,
openAIUsageSectionNewWith,
)
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
import System.Taffybar.Widget.SNITray
( CollapsibleSNITrayParams (..),
SNITrayConfig (..),
defaultCollapsibleSNITrayParams,
defaultSNITrayConfig,
)
import System.Taffybar.Widget.SNITray.PrioritizedCollapsible
( PrioritizedCollapsibleSNITrayParams (..),
defaultPrioritizedCollapsibleSNITrayParams,
sniTrayPrioritizedCollapsibleNewFromParams,
)
import qualified System.Taffybar.Widget.ScreenLock as ScreenLock
import System.Taffybar.Widget.Util
( backgroundLoop,
buildIconLabelBox,
pixbufNewFromFileAtScaleByHeight,
widgetSetClassGI,
)
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import TaffybarConfig.Host (laptopHosts)
import TaffybarConfig.WidgetUtil
( decorateWithClassAndBox,
decorateWithClassAndBoxM,
setFixedLabelWidth,
setLabelAlignmentRecursively,
stackInPill,
usageLogoWidget,
)
import TaffybarConfig.Workspaces (workspaceLabelSetter, workspaceShowPredicate, workspaceWindowIconGetter)
import Text.Printf (printf)
import Text.Read (readMaybe)
audioWidget :: TaffyIO Gtk.Widget
audioWidget =
decorateWithClassAndBoxM "audio" Audio.audioNew
networkInnerWidget :: TaffyIO Gtk.Widget
networkInnerWidget =
withNmAppletMenu NetworkManager.networkManagerWifiIconLabelNew
>>= flip widgetSetClassGI "network"
networkWidget :: TaffyIO Gtk.Widget
networkWidget =
decorateWithClassAndBoxM "network" networkInnerWidget
layoutWidget :: TaffyIO Gtk.Widget
layoutWidget =
decorateWithClassAndBoxM "layout" (layoutNew defaultLayoutConfig)
windowsWidget :: TaffyIO Gtk.Widget
windowsWidget =
decorateWithClassAndBoxM
"windows"
( windowsNew
defaultWindowsConfig
{ getActiveLabel = truncatedGetActiveLabel 28,
configureActiveLabel = liftIO . setFixedLabelWidth 28
}
)
workspacesWidget :: TaffyIO Gtk.Widget
workspacesWidget =
Workspaces.workspacesNew cfg
where
cfg =
Workspaces.defaultWorkspacesConfig
{ Workspaces.widgetGap = 0,
Workspaces.minIcons = 1,
Workspaces.getWindowIconPixbuf = workspaceWindowIconGetter,
Workspaces.hyprlandWorkspaceProviderConfig =
HyprlandWorkspaces.defaultHyprlandWorkspaceProviderConfig
{ HyprlandWorkspaces.specialWorkspaceWindowTarget =
HyprlandWorkspaces.specialWorkspaceWindowsToMinimized
},
Workspaces.labelSetter = workspaceLabelSetter,
Workspaces.showWorkspaceFn = workspaceShowPredicate
}
clockWidget :: TaffyIO Gtk.Widget
clockWidget = do
clock <-
textClockNewWith
defaultClockConfig
{ clockUpdateStrategy = RoundedTargetInterval 60 0.0,
clockFormatString = "%a %b %_d\n%I:%M %p"
}
liftIO $ setLabelAlignmentRecursively 0.5 Gtk.JustificationCenter clock
decorateWithClassAndBox "clock" clock
singleLineMprisLabel :: Text -> Text
singleLineMprisLabel =
T.replace "\n" " " . T.replace "\r" " "
stackedMprisLabel :: Text -> Text
stackedMprisLabel raw =
let normalized = singleLineMprisLabel raw
(top, rest) = T.breakOn " - " normalized
in if T.null rest
then normalized
else top <> "\n" <> T.drop 3 rest
mprisWidget :: TaffyIO Gtk.Widget
mprisWidget =
mpris2NewWithConfig
MPRIS2Config
{ mprisWidgetWrapper = decorateWithClassAndBox "mpris",
updatePlayerWidget =
simplePlayerWidget
defaultPlayerConfig
{ setNowPlayingLabel =
fmap stackedMprisLabel . playingText 20 20,
setupPlayerLabel = setFixedLabelWidth 20
}
}
batteryInnerWidget :: TaffyIO Gtk.Widget
batteryInnerWidget = do
iconWidget <- batteryTextIconNew
labelWidget <- textBatteryNew "$percentage$%"
liftIO (buildIconLabelBox iconWidget labelWidget) >>= flip widgetSetClassGI "battery"
batteryWidget :: TaffyIO Gtk.Widget
batteryWidget =
decorateWithClassAndBoxM "battery" batteryInnerWidget
backlightWidget :: TaffyIO Gtk.Widget
backlightWidget =
decorateWithClassAndBoxM
"backlight"
( backlightLabelNewChanWith
defaultBacklightWidgetConfig
{ backlightFormat = "☀ $percent$%",
backlightUnknownFormat = "☀ n/a",
backlightTooltipFormat =
Just "Device: $device$\nBrightness: $brightness$/$max$ ($percent$%)"
}
)
diskUsageInnerWidget :: TaffyIO Gtk.Widget
diskUsageInnerWidget =
diskUsageNew >>= flip widgetSetClassGI "disk-usage"
diskUsageWidget :: TaffyIO Gtk.Widget
diskUsageWidget =
decorateWithClassAndBoxM "disk-usage" diskUsageInnerWidget
meminfoPercentRowWidget ::
Text ->
Text ->
(MemoryInfo -> Maybe Double) ->
(MemoryInfo -> T.Text) ->
TaffyIO Gtk.Widget
meminfoPercentRowWidget rowClass iconText getRatio tooltipText =
liftIO $ do
iconW <- Gtk.toWidget =<< Gtk.labelNew (Just iconText)
valueLabel <- Gtk.labelNew (Just "")
valueW <- Gtk.toWidget valueLabel
row <- buildIconLabelBox iconW valueW
_ <- widgetSetClassGI row rowClass
let fmtPercent :: Double -> T.Text
fmtPercent r = T.pack (printf "%.0f%%" (max 0 r * 100))
updateOnce :: IO ()
updateOnce = do
info <- parseMeminfo
let valueText = maybe "n/a" fmtPercent (getRatio info)
postGUIASync $ do
Gtk.labelSetText valueLabel valueText
Gtk.widgetSetTooltipText row (Just (tooltipText info))
threadDelay (2 * 1000000)
_ <- Gtk.onWidgetRealize row $ backgroundLoop updateOnce
pure row
ramRowWidget :: TaffyIO Gtk.Widget
ramRowWidget =
meminfoPercentRowWidget
"ram-row"
"\xF538" -- Font Awesome: memory
(Just . memoryUsedRatio)
(\info -> "RAM " <> showMemoryInfo "$used$/$total$" 2 info)
swapRowWidget :: TaffyIO Gtk.Widget
swapRowWidget =
meminfoPercentRowWidget
"swap-row"
"\xF0EC" -- Font Awesome: exchange (swap-ish)
(\info -> if memorySwapTotal info <= 0 then Nothing else Just (memorySwapUsedRatio info))
(\info -> "SWAP " <> showMemoryInfo "$swapUsed$/$swapTotal$" 2 info)
ramSwapWidget :: TaffyIO Gtk.Widget
ramSwapWidget =
stackInPill "ram-swap" [ramRowWidget, swapRowWidget]
audioBacklightWidget :: TaffyIO Gtk.Widget
audioBacklightWidget =
stackInPill
"audio-backlight"
[ Audio.audioNew,
backlightNewChanWith
defaultBacklightWidgetConfig
{ backlightFormat = "$percent$%",
backlightUnknownFormat = "n/a",
backlightTooltipFormat =
Just "Device: $device$\nBrightness: $brightness$/$max$ ($percent$%)"
}
]
asusInnerWidget :: TaffyIO Gtk.Widget
asusInnerWidget = ASUS.asusWidgetNew
asusWidget :: TaffyIO Gtk.Widget
asusWidget =
decorateWithClassAndBoxM "asus-profile" asusInnerWidget
batteryNetworkWidget :: TaffyIO Gtk.Widget
batteryNetworkWidget =
stackInPill "battery-network" [batteryInnerWidget, networkInnerWidget]
asusDiskUsageWidget :: TaffyIO Gtk.Widget
asusDiskUsageWidget =
stackInPill "asus-disk-usage" [diskUsageInnerWidget, asusInnerWidget]
screenLockWidget :: TaffyIO Gtk.Widget
screenLockWidget =
decorateWithClassAndBoxM "screen-lock" $
ScreenLock.screenLockNewWithConfig
ScreenLock.defaultScreenLockConfig
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock"
}
wlsunsetWidget :: TaffyIO Gtk.Widget
wlsunsetWidget =
decorateWithClassAndBoxM "wlsunset" $
Wlsunset.wlsunsetNewWithConfig
Wlsunset.defaultWlsunsetWidgetConfig
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun"
}
simplifiedScreenLockWidget :: TaffyIO Gtk.Widget
simplifiedScreenLockWidget =
-- Inner widget: no extra pill wrapping (the combiner provides that).
ScreenLock.screenLockNewWithConfig
ScreenLock.defaultScreenLockConfig
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock"
}
simplifiedWlsunsetWidget :: TaffyIO Gtk.Widget
simplifiedWlsunsetWidget =
-- Inner widget: no extra pill wrapping (the combiner provides that).
Wlsunset.wlsunsetNewWithConfig
Wlsunset.defaultWlsunsetWidgetConfig
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun"
}
sunLockWidget :: TaffyIO Gtk.Widget
sunLockWidget =
stackInPill "sun-lock" [simplifiedWlsunsetWidget, simplifiedScreenLockWidget]
cpuWidget :: TaffyIO Gtk.Widget
cpuWidget =
decorateWithClassAndBoxM "cpu" $
cpuMonitorNew
defaultGraphConfig
{ graphDataColors = [(0, 1, 0.5, 0.8), (1, 0, 0, 0.5)],
graphBackgroundColor = (0, 0, 0, 0),
graphBorderWidth = 0,
graphLabel = Just "CPU",
graphWidth = 50,
graphDirection = LEFT_TO_RIGHT
}
1.0
"cpu"
wakeupDebugWidget :: TaffyIO Gtk.Widget
wakeupDebugWidget =
decorateWithClassAndBoxM "wakeup-debug" wakeupDebugWidgetNew
omniMenuItem :: Text -> Text -> Text -> OmniMenuItem
omniMenuItem label iconName command =
OmniMenuItem
{ omniMenuItemLabel = label,
omniMenuItemCommand = command,
omniMenuItemIcon = Just iconName,
omniMenuItemTooltip = Just command
}
omniMenuWidget :: TaffyIO Gtk.Widget
omniMenuWidget =
decorateWithClassAndBoxM "omni-menu" $ do
icon <-
liftIO $ do
iconPath <- getUserConfigFile "taffybar" "icons/nix-snowflake.svg"
pixbufNewFromFileAtScaleByHeight 18 iconPath >>= \case
Right pixbuf -> Gtk.toWidget =<< Gtk.imageNewFromPixbuf (Just pixbuf)
Left _ ->
Gtk.imageNewFromIconName
(Just "system-run")
(fromIntegral $ fromEnum Gtk.IconSizeMenu)
>>= Gtk.toWidget
omniMenuNewWithConfig
(defaultOmniMenuConfig icon)
{ omniMenuIncludeApplications = True,
omniMenuSections =
[ OmniMenuSection
"Launch"
[ omniMenuItem "App launcher" "view-app-grid-symbolic" "hypr_shell_ui launcher",
omniMenuItem "Run command" "system-run" "hypr_shell_ui run",
omniMenuItem "Terminal" "utilities-terminal" "ghostty --gtk-single-instance=false",
omniMenuItem "Window picker" "preferences-system-windows" "hypr_shell_ui window go"
],
OmniMenuSection
"System"
[ omniMenuItem "Lock" "system-lock-screen" "loginctl lock-session",
omniMenuItem "Toggle screensaver" "video-display" "/home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver toggle",
omniMenuItem "Reload WM" "view-refresh" "sh -lc 'hyprctl reload || xmonad --restart || river-xmonad-restart'",
omniMenuItem "Restart taffybar" "view-refresh-symbolic" "/home/imalison/dotfiles/dotfiles/config/taffybar/scripts/taffybar-restart",
omniMenuItem "Logout" "system-log-out" "sh -lc 'hyprctl dispatch exit || riverctl exit'",
omniMenuItem "Suspend" "media-playback-pause" "systemctl suspend",
omniMenuItem "Reboot" "system-reboot" "systemctl reboot",
omniMenuItem "Power off" "system-shutdown" "systemctl poweroff"
]
]
}
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 = do
iconWidget <- liftIO $ usageLogoWidget "openai-symbol.svg" "OpenAI usage"
decorateWithClassAndBoxM "openai-usage" $
openAIUsageSectionNewWith
iconWidget
defaultOpenAIUsageStackConfig
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
}
sniPriorityVisibilityThresholdDefault :: Int
sniPriorityVisibilityThresholdDefault = 0
sniTrayWidget :: TaffyIO Gtk.Widget
sniTrayWidget = do
-- If the Haskell backend regresses, flip at runtime:
-- TAFFYBAR_SNI_MENU_BACKEND=lib
backendEnv <- liftIO (lookupEnv "TAFFYBAR_SNI_MENU_BACKEND")
thresholdEnv <- liftIO (lookupEnv "TAFFYBAR_SNI_PRIORITY_THRESHOLD")
let menuBackend =
case fmap (map toLower) backendEnv of
Just "lib" -> SNITray.LibDBusMenu
_ -> SNITray.HaskellDBusMenu
visibilityThreshold =
fromMaybe
sniPriorityVisibilityThresholdDefault
(thresholdEnv >>= readMaybe)
trayParams =
SNITray.defaultTrayParams
{ SNITray.trayMenuBackend = menuBackend,
SNITray.trayOverlayScale = 1 % 3,
SNITray.trayEventHooks = SNITray.defaultTrayEventHooks
}
sniTrayConfig =
defaultSNITrayConfig
{ sniTrayTrayParams = trayParams
}
collapsibleParams =
defaultCollapsibleSNITrayParams
{ collapsibleSNITrayConfig = sniTrayConfig
}
prioritizedParams =
defaultPrioritizedCollapsibleSNITrayParams
{ prioritizedCollapsibleSNITrayParams = collapsibleParams,
prioritizedCollapsibleSNITrayVisibilityThreshold = Just visibilityThreshold,
prioritizedCollapsibleSNITrayHoverExpand = True
}
decorateWithClassAndBoxM
"sni-tray"
(sniTrayPrioritizedCollapsibleNewFromParams prioritizedParams)
startWidgetsForBackend :: Backend -> [TaffyIO Gtk.Widget]
startWidgetsForBackend backend =
case backend of
BackendX11 -> [omniMenuWidget, workspacesWidget, layoutWidget]
-- These Wayland widgets are Hyprland-specific.
BackendWayland -> [omniMenuWidget, workspacesWidget]
startWidgetsForHostAndBackend :: String -> Backend -> [TaffyIO Gtk.Widget]
startWidgetsForHostAndBackend _hostName = startWidgetsForBackend
endWidgetsForHost :: String -> [TaffyIO Gtk.Widget]
endWidgetsForHost hostName =
-- NOTE: end widgets are packed with Gtk.boxPackEnd, so the list order is
-- right-to-left on screen. Make the tray appear at the far right by placing
-- it first in the list. (On laptops: the battery/wifi stack is far right,
-- tray immediately left of it.)
let baseEndWidgets =
[ sniTrayWidget,
audioWidget,
openAIUsageWidget,
cpuWidget,
ramSwapWidget,
diskUsageWidget,
networkWidget,
sunLockWidget,
mprisWidget
]
laptopEndWidgets =
[ batteryNetworkWidget,
sniTrayWidget,
asusDiskUsageWidget,
audioBacklightWidget,
openAIUsageWidget,
cpuWidget,
ramSwapWidget,
sunLockWidget,
mprisWidget
]
in if hostName `elem` laptopHosts
then laptopEndWidgets
else baseEndWidgets

View File

@@ -0,0 +1,196 @@
{-# LANGUAGE OverloadedStrings #-}
module TaffybarConfig.Workspaces
( workspaceLabelSetter,
workspaceShowPredicate,
workspaceWindowIconGetter,
)
where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Reader (asks)
import Data.Int (Int32)
import Data.List (nub)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import System.Taffybar.Context
( Backend (BackendX11),
TaffyIO,
backend,
runX11Def,
)
import System.Taffybar.Information.EWMHDesktopInfo (WorkspaceId (..))
import qualified System.Taffybar.Information.Workspaces.Model as WorkspaceModel
import System.Taffybar.Information.X11DesktopInfo
import System.Taffybar.Util (getPixbufFromFilePath, maybeTCombine, (<|||>))
import System.Taffybar.Widget.Util (loadPixbufByName)
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import System.Taffybar.WindowIcon (pixBufFromColor)
import TaffybarConfig.ChromeFavicons
( ChromeFaviconConfig (..),
ChromeFaviconOverlayMode (..),
chromeFaviconIconGetter,
defaultChromeFaviconConfig,
)
chromeFaviconConfig :: ChromeFaviconConfig
chromeFaviconConfig =
defaultChromeFaviconConfig
{ chromeFaviconOverlayMode = FaviconWithChromeOverlay,
chromeFaviconOverlayRatio = 0.45,
chromeFaviconEnabled = True
}
x11FullWorkspaceNames :: X11Property [(WorkspaceId, String)]
x11FullWorkspaceNames =
go <$> readAsListOfString Nothing "_NET_DESKTOP_FULL_NAMES"
where
go = zip [WorkspaceId i | i <- [0 ..]]
remapNSP :: String -> String
remapNSP "NSP" = "S"
remapNSP n = n
workspaceIsMinimizedBucket :: WorkspaceModel.WorkspaceInfo -> Bool
workspaceIsMinimizedBucket workspace =
let name =
T.toLower $
WorkspaceModel.workspaceName $
WorkspaceModel.workspaceIdentity workspace
in name == "minimized" || name == "special:minimized"
workspaceShowPredicate :: WorkspaceModel.WorkspaceInfo -> Bool
workspaceShowPredicate workspace =
Workspaces.hideEmpty workspace
&& (not (WorkspaceModel.workspaceIsSpecial workspace) || workspaceIsMinimizedBucket workspace)
workspaceLabelSetter :: WorkspaceModel.WorkspaceInfo -> TaffyIO String
workspaceLabelSetter workspace = do
backendType <- asks backend
let identity = WorkspaceModel.workspaceIdentity workspace
fallbackLabel = remapNSP $ T.unpack (WorkspaceModel.workspaceName identity)
if workspaceIsMinimizedBucket workspace
then return "M"
else case (backendType, WorkspaceModel.workspaceNumericId identity) of
(BackendX11, Just workspaceId) -> do
fullNames <- runX11Def [] x11FullWorkspaceNames
return $ remapNSP $ fromMaybe fallbackLabel (lookup (WorkspaceId workspaceId) fullNames)
_ -> return fallbackLabel
iconRemap :: [(Text, [Text])]
iconRemap =
[ ("spotify", ["spotify-client", "spotify"])
]
iconRemapMap :: M.Map Text [Text]
iconRemapMap =
M.fromList [(T.toLower k, v) | (k, v) <- iconRemap]
lookupIconRemap :: Text -> [Text]
lookupIconRemap name = fromMaybe [] $ M.lookup (T.toLower name) iconRemapMap
iconNameVariants :: Text -> [Text]
iconNameVariants raw =
let lower = T.toLower raw
stripped = fromMaybe lower (T.stripSuffix ".desktop" lower)
suffixes = ["-gtk", "-client", "-desktop"]
stripSuffixes name =
let variants = mapMaybe (`T.stripSuffix` name) suffixes
in nub $ variants ++ [name]
baseNames = stripSuffixes stripped ++ [raw]
toDash c
| c == ' ' || c == '_' || c == '.' || c == '/' = '-'
| otherwise = c
toUnderscore c
| c == ' ' || c == '-' || c == '.' || c == '/' = '_'
| otherwise = c
variantsFor name =
let dotted =
case T.splitOn "." name of
[] -> name
xs -> last xs
dashed = T.map toDash name
dashedDotted = T.map toDash dotted
underscored = T.map toUnderscore name
underscoredDotted = T.map toUnderscore dotted
in [dotted, dashed, dashedDotted, underscored, underscoredDotted, name]
in nub $ concatMap variantsFor baseNames
workspaceIconCandidates :: WorkspaceModel.WindowInfo -> [Text]
workspaceIconCandidates windowData =
let baseNames = WorkspaceModel.windowClassHints windowData
remapped = concatMap lookupIconRemap baseNames
remappedExpanded = concatMap iconNameVariants remapped
baseExpanded = concatMap iconNameVariants baseNames
in nub (remappedExpanded ++ baseExpanded)
isPathCandidate :: Text -> Bool
isPathCandidate name =
T.isInfixOf "/" name
|| any (`T.isSuffixOf` name) [".png", ".svg", ".xpm"]
workspaceCandidateInfo :: Text -> WorkspaceModel.WindowInfo
workspaceCandidateInfo name =
WorkspaceModel.WindowInfo
{ WorkspaceModel.windowIdentity = WorkspaceModel.HyprlandWindowIdentity "",
WorkspaceModel.windowUpdateRevision = 0,
WorkspaceModel.windowTitle = "",
WorkspaceModel.windowClassHints = [name],
WorkspaceModel.windowPosition = Nothing,
WorkspaceModel.windowUrgent = False,
WorkspaceModel.windowActive = False,
WorkspaceModel.windowMinimized = False,
WorkspaceModel.windowPinned = False
}
workspaceIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe Gdk.Pixbuf)
workspaceIconFromCandidate size name
| isPathCandidate name =
liftIO $ getPixbufFromFilePath (T.unpack name)
| otherwise =
maybeTCombine
(Workspaces.getWindowIconPixbufFromDesktopEntry size (workspaceCandidateInfo name))
(liftIO $ loadPixbufByName size name)
workspaceManualIconGetter :: Workspaces.WindowIconPixbufGetter
workspaceManualIconGetter =
Workspaces.handleIconGetterException $ \size windowData ->
foldl maybeTCombine (return Nothing) $
map (workspaceIconFromCandidate size) (workspaceIconCandidates windowData)
fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe Gdk.Pixbuf)
fallbackIconPixbuf size = do
let fallbackNames =
[ "application-x-executable",
"application",
"image-missing",
"gtk-missing-image",
"dialog-question",
"utilities-terminal",
"system-run",
"window"
]
tryNames =
foldl
maybeTCombine
(return Nothing)
(map (liftIO . loadPixbufByName size) fallbackNames)
result <- tryNames
case result of
Just _ -> return result
Nothing -> Just <$> pixBufFromColor size 0x5f5f5fff
workspaceFallbackIcon :: Workspaces.WindowIconPixbufGetter
workspaceFallbackIcon size _ =
fallbackIconPixbuf size
workspaceWindowIconGetter :: Workspaces.WindowIconPixbufGetter
workspaceWindowIconGetter =
chromeFaviconIconGetter chromeFaviconConfig
<|||> workspaceManualIconGetter
<|||> Workspaces.getWindowIconPixbufFromChrome
<|||> Workspaces.defaultGetWindowIconPixbuf
<|||> workspaceFallbackIcon

View File

@@ -7,3 +7,4 @@ packages:
taffybar/packages/status-notifier-item
taffybar/packages/dbus-menu
taffybar/packages/dbus-hslogger
taffybar/packages/gi-wireplumber

View File

@@ -136,17 +136,17 @@
"xmonad-contrib": "xmonad-contrib"
},
"locked": {
"lastModified": 1777319252,
"narHash": "sha256-mPft6i8ReJAvW2LdylFI6FF6NFGa1HMa3RNbisfAsbc=",
"ref": "refs/heads/codex/fix-gdk-backend-strut-detection",
"rev": "c2cee23fc57384cd322d589944129e6c31d4f0fd",
"revCount": 2288,
"type": "git",
"url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"
"lastModified": 1778673962,
"narHash": "sha256-GmHRMdrUIQpMf6k5gRjP9Mvx2WO0FvIEF1SPlxEpnas=",
"owner": "taffybar",
"repo": "taffybar",
"rev": "08125b267c03232c560fce6259264cc9283d582e",
"type": "github"
},
"original": {
"type": "git",
"url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"
"owner": "taffybar",
"repo": "taffybar",
"type": "github"
}
},
"weeder-nix": {

View File

@@ -1,10 +1,9 @@
{
inputs = {
taffybar = {
# Use the local git checkout, not a raw path snapshot, so gitignored
# build artifacts like dist-newstyle/.worktrees/.direnv don't get copied
# into flake-input store sources.
url = "git+file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar";
# Keep the default source usable in CI. Local iteration uses
# IMALISON_TAFFYBAR_LIVE_CHECKOUT below via `just switch-local-taffybar`.
url = "github:taffybar/taffybar";
inputs.weeder-nix.inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
};
# Follow the vendored taffybar flake's pins so the config shell and the
@@ -108,6 +107,13 @@
{ })
(_: { doCheck = false; doHaddock = false; });
gi-wireplumber =
pkgs.haskell.lib.overrideCabal
(hself.callCabal2nix "gi-wireplumber"
(localTaffybarSubdir "packages/gi-wireplumber")
{ })
(_: { doCheck = false; doHaddock = false; });
dbus-hslogger =
hself.callCabal2nix "dbus-hslogger"
(localTaffybarSubdir "packages/dbus-hslogger")
@@ -117,41 +123,59 @@
# modules (e.g. System.Taffybar.Widget.ASUS) used by this config.
taffybar = pkgs.haskell.lib.overrideCabal
(pkgs.haskell.lib.disableStaticLibraries
(hself.callCabal2nix "taffybar" cleanedTaffybarSource { inherit (pkgs) gtk3; }))
(hself.callCabal2nix "taffybar" cleanedTaffybarSource {
inherit (pkgs) gtk3;
}))
(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 and gi-wireplumber introspection data.
librarySystemDepends = (oa.librarySystemDepends or []) ++ [
pkgs.gtk-layer-shell
pkgs.wireplumber
];
shellHook = ''
${oa.shellHook or ""}
export PKG_CONFIG_PATH="${pkgs.wireplumber.dev}/lib/pkgconfig:${pkgs.pipewire.dev}/lib/pkgconfig:''${PKG_CONFIG_PATH:-}"
export GI_GIR_PATH="${pkgs.wireplumber.dev}/share/gir-1.0:''${GI_GIR_PATH:-}"
export GI_TYPELIB_PATH="${pkgs.wireplumber}/lib/girepository-1.0:${pkgs.glib.out}/lib/girepository-1.0:''${GI_TYPELIB_PATH:-}"
export XDG_DATA_DIRS="${pkgs.wireplumber.dev}/share:''${XDG_DATA_DIRS:-}"
'';
# Needed for gi-gtk-layer-shell (introspection data).
librarySystemDepends = (oa.librarySystemDepends or []) ++ [ pkgs.gtk-layer-shell ];
});
# gi-gtk-hs patching is now handled by taffybar's fixVersionNamePackages overlay
imalison-taffybar = pkgs.haskell.lib.addPkgconfigDepends (
hself.callCabal2nix "imalison-taffybar"
(pkgs.lib.sourceByRegex ./. [ "taffybar.hs" "imalison-taffybar.cabal" ])
{ }
) [
pkgs.util-linux.dev
pkgs.pcre2
pkgs.pcre
pkgs.libselinux.dev
pkgs.libsepol.dev
pkgs.fribidi.out
pkgs.fribidi.dev
pkgs.libthai.dev
pkgs.libdatrie.dev
pkgs.libxdmcp.dev
pkgs.libxkbcommon.dev
pkgs.libepoxy.dev
pkgs.libxtst.out
];
imalison-taffybar = pkgs.haskell.lib.overrideCabal
(pkgs.haskell.lib.addPkgconfigDepends (
hself.callCabal2nix "imalison-taffybar"
(pkgs.lib.sourceByRegex ./. [
"taffybar.hs"
"imalison-taffybar.cabal"
"TaffybarConfig"
"TaffybarConfig/.*"
])
{ }
) [
pkgs.util-linux.dev
pkgs.pcre2
pkgs.pcre
pkgs.libselinux.dev
pkgs.libsepol.dev
pkgs.fribidi.out
pkgs.fribidi.dev
pkgs.libthai.dev
pkgs.libdatrie.dev
pkgs.libxdmcp.dev
pkgs.libxkbcommon.dev
pkgs.libepoxy.dev
pkgs.libxtst.out
])
(oa: {
configureFlags = (oa.configureFlags or []) ++ [
"--ghc-option=-optl-fuse-ld=bfd"
"--ld-option=-fuse-ld=bfd"
"--with-ld=ld.bfd"
];
});
};
# Avoid depending on xmonad.lib's helper functions, since parent flakes
@@ -179,6 +203,7 @@
pkgs.librsvg
];
shellHook = ''
${hpkgs.taffybar.env.shellHook or ""}
# GHCi loads package DLL dependencies via the runtime linker, so it
# needs zlib on LD_LIBRARY_PATH in addition to the build-time -L flags.
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ pkgs.zlib ]}:''${LD_LIBRARY_PATH:-}"

View File

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

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="535"
height="535"
viewBox="0 0 501.56251 501.56249"
id="svg2"
version="1.1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="nix-snowflake-colours.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient5562">
<stop
style="stop-color:#699ad7;stop-opacity:1"
offset="0"
id="stop5564" />
<stop
id="stop5566"
offset="0.24345198"
style="stop-color:#7eb1dd;stop-opacity:1" />
<stop
style="stop-color:#7ebae4;stop-opacity:1"
offset="1"
id="stop5568" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5053">
<stop
style="stop-color:#415e9a;stop-opacity:1"
offset="0"
id="stop5055" />
<stop
id="stop5057"
offset="0.23168644"
style="stop-color:#4a6baf;stop-opacity:1" />
<stop
style="stop-color:#5277c3;stop-opacity:1"
offset="1"
id="stop5059" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5562"
id="linearGradient4328"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(70.650339,-1055.1511)"
x1="200.59668"
y1="351.41116"
x2="290.08701"
y2="506.18814" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5053"
id="linearGradient4330"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(864.69589,-1491.3405)"
x1="-584.19934"
y1="782.33563"
x2="-496.29703"
y2="937.71399" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.70904368"
inkscape:cx="99.429699"
inkscape:cy="195.33352"
inkscape:document-units="px"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1050"
inkscape:window-x="1920"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:snap-global="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="gradient-logo"
style="display:inline;opacity:1"
transform="translate(-156.41121,933.30685)">
<g
id="g2"
transform="matrix(0.99994059,0,0,0.99994059,-0.06321798,33.188377)"
style="stroke-width:1.00006">
<path
sodipodi:nodetypes="cccccccccc"
inkscape:connector-curvature="0"
id="path3336-6"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z"
style="opacity:1;fill:url(#linearGradient4328);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.00018;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<use
height="100%"
width="100%"
transform="rotate(60,407.11155,-715.78724)"
id="use3439-6"
inkscape:transform-center-y="151.59082"
inkscape:transform-center-x="124.43045"
xlink:href="#path3336-6"
y="0"
x="0"
style="stroke-width:1.00006" />
<use
height="100%"
width="100%"
transform="rotate(-60,407.31177,-715.70016)"
id="use3445-0"
inkscape:transform-center-y="75.573958"
inkscape:transform-center-x="-168.20651"
xlink:href="#path3336-6"
y="0"
x="0"
style="stroke-width:1.00006" />
<use
height="100%"
width="100%"
transform="rotate(180,407.41868,-715.7565)"
id="use3449-5"
inkscape:transform-center-y="-139.94592"
inkscape:transform-center-x="59.669705"
xlink:href="#path3336-6"
y="0"
x="0"
style="stroke-width:1.00006" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient4330);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.00018;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8256 z"
id="path4260-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<use
height="100%"
width="100%"
transform="rotate(120,407.33916,-716.08356)"
id="use4354-5"
xlink:href="#path4260-0"
y="0"
x="0"
style="display:inline;stroke-width:1.00006" />
<use
height="100%"
width="100%"
transform="rotate(-120,407.28823,-715.86995)"
id="use4362-2"
xlink:href="#path4260-0"
y="0"
x="0"
style="display:inline;stroke-width:1.00006" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -13,6 +13,12 @@ cabal-version: >=1.10
executable taffybar
hs-source-dirs: .
main-is: taffybar.hs
other-modules: TaffybarConfig.Config
, TaffybarConfig.ChromeFavicons
, TaffybarConfig.Host
, TaffybarConfig.Widgets
, TaffybarConfig.WidgetUtil
, TaffybarConfig.Workspaces
ghc-options: -threaded -rtsopts -with-rtsopts=-N
ghc-prof-options: -fprof-auto
build-depends: base

View File

@@ -7,20 +7,24 @@ pkill -u "$USER" -x taffybar || true
cd "$root"
# Hyprland can restart and change the instance signature, leaving old shells with
# a stale HYPRLAND_INSTANCE_SIGNATURE. Fix it before launching taffybar so any
# `hyprctl` calls inside the bar work.
# Hyprland can restart and change the instance signature, and controller shells
# can retain stale X11 session variables. Prefer the live Hyprland instance when
# one is available so taffybar starts on the Wayland backend.
if command -v hyprctl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
if ! hyprctl monitors -j >/dev/null 2>&1; then
instances_json="$(hyprctl instances -j 2>/dev/null || true)"
instances_json="$(hyprctl instances -j 2>/dev/null || true)"
if [[ -n "${instances_json:-}" ]]; then
if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then
inst="$(printf '%s\n' "$instances_json" | jq -r --arg sock "$WAYLAND_DISPLAY" '.[] | select(.wl_socket == $sock) | .instance' 2>/dev/null | head -n1 || true)"
inst_row="$(printf '%s\n' "$instances_json" | jq -r --arg sock "$WAYLAND_DISPLAY" '.[] | select(.instance and .wl_socket and .wl_socket == $sock) | [.instance, .wl_socket] | @tsv' 2>/dev/null | head -n1 || true)"
else
inst="$(printf '%s\n' "$instances_json" | jq -r '.[0].instance // empty' 2>/dev/null || true)"
inst_row="$(printf '%s\n' "$instances_json" | jq -r '.[] | select(.instance and .wl_socket) | [.instance, .wl_socket] | @tsv' 2>/dev/null | head -n1 || true)"
fi
if [[ -n "${inst:-}" ]]; then
if [[ -n "${inst_row:-}" ]]; then
read -r inst wl_socket <<<"$inst_row"
export HYPRLAND_INSTANCE_SIGNATURE="$inst"
export WAYLAND_DISPLAY="$wl_socket"
export XDG_SESSION_TYPE=wayland
export GDK_BACKEND=wayland
fi
fi
fi

View File

@@ -4,20 +4,24 @@ set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root"
# Hyprland can restart and change the instance signature, leaving old shells with
# a stale HYPRLAND_INSTANCE_SIGNATURE. Fix it before launching taffybar so any
# `hyprctl` calls inside the bar work.
# Hyprland can restart and change the instance signature, and controller shells
# can retain stale X11 session variables. Prefer the live Hyprland instance when
# one is available so taffybar starts on the Wayland backend.
if command -v hyprctl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
if ! hyprctl monitors -j >/dev/null 2>&1; then
instances_json="$(hyprctl instances -j 2>/dev/null || true)"
instances_json="$(hyprctl instances -j 2>/dev/null || true)"
if [[ -n "${instances_json:-}" ]]; then
if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then
inst="$(printf '%s\n' "$instances_json" | jq -r --arg sock "$WAYLAND_DISPLAY" '.[] | select(.wl_socket == $sock) | .instance' 2>/dev/null | head -n1 || true)"
inst_row="$(printf '%s\n' "$instances_json" | jq -r --arg sock "$WAYLAND_DISPLAY" '.[] | select(.instance and .wl_socket and .wl_socket == $sock) | [.instance, .wl_socket] | @tsv' 2>/dev/null | head -n1 || true)"
else
inst="$(printf '%s\n' "$instances_json" | jq -r '.[0].instance // empty' 2>/dev/null || true)"
inst_row="$(printf '%s\n' "$instances_json" | jq -r '.[] | select(.instance and .wl_socket) | [.instance, .wl_socket] | @tsv' 2>/dev/null | head -n1 || true)"
fi
if [[ -n "${inst:-}" ]]; then
if [[ -n "${inst_row:-}" ]]; then
read -r inst wl_socket <<<"$inst_row"
export HYPRLAND_INSTANCE_SIGNATURE="$inst"
export WAYLAND_DISPLAY="$wl_socket"
export XDG_SESSION_TYPE=wayland
export GDK_BACKEND=wayland
fi
fi
fi

View File

@@ -3,18 +3,20 @@ max_visible_icons: 0
priorities:
- key: item-id:nm-applet
priority: 3
- key: icon-name:gitea
priority: 2
- key: icon-name:github
priority: 2
- key: icon-name:gitea
priority: 1
- key: icon-name:gmail
priority: 2
- key: icon-name:password
priority: 1
- key: icon-name:text-org
priority: 1
- key: item-id:git-sync-rs
priority: 1
- key: process:slack
priority: 1
- key: icon-name:blueman-tray
priority: 0
- key: icon-name:kdeconnectindicatordark
priority: 0
- key: item-id:flameshot
@@ -23,8 +25,12 @@ priorities:
priority: 0
- key: item-id:udiskie
priority: 0
- key: icon-name::1.89
priority: -1
- key: icon-name:audio-volume-low
priority: -1
- key: icon-name:blueman-tray
priority: -1
- key: item-id:blueman
priority: -1
- key: item-id:chrome_status_icon_1

View File

@@ -0,0 +1,42 @@
@import url("taffybar.css");
/* Host-specific density tweaks for strixi-minaj. */
.taffy-box {
font-size: 9.5pt;
border-radius: 8px;
}
.outer-pad,
.workspaces .outer-pad {
border-radius: 8px;
margin: 4px 3px;
}
.inner-pad,
.workspaces .inner-pad {
border-radius: 7px;
padding-top: 2px;
padding-bottom: 2px;
}
.workspaces .contents {
border-radius: 7px;
padding: 0px 3px;
}
.workspaces .overlay-box .workspace-label {
padding: 1px 4px 4px 10px;
}
.visible .contents,
.workspaces .window-icon-container,
.workspaces .window-icon-container.active {
padding-top: 1px;
padding-bottom: 1px;
}
.auto-size-image,
.sni-tray {
padding-top: 2px;
padding-bottom: 2px;
}

View File

@@ -40,6 +40,16 @@
-GtkLabel-justify: left;
}
/* Compact logo column for stacked AI usage sections. */
.usage-section.icon-label > .icon {
min-width: 22px;
padding-right: 8px;
}
.usage-section.icon-label > .label {
padding-right: 0px;
}
/* Compact two-line RAM/SWAP widget: reduce icon padding a bit. */
.ram-swap .icon-label > .icon {
/* Different glyphs have different visual widths; fix the icon column width
@@ -119,6 +129,15 @@
padding: 0px;
}
/* This must outrank the end-widget nth-last-child color rotation. Otherwise
the collapsed MPRIS end widget can still render as a thin blue strip. */
.taffy-box > .outer-pad.end-widget.mpris.no-visible-children {
background-color: @transparent;
background-image: none;
box-shadow: none;
border-width: 0px;
}
/* Workspaces styling */
/* Reset workspace .outer-pad pills to default styling so the nth-child color
@@ -230,16 +249,27 @@
/* Don't give each window icon its own background/border; the workspace
squircle is the background. */
background-color: transparent;
border: 0px;
border: 1px solid transparent;
box-shadow: none;
padding: 0px 2px;
padding: 2px 4px;
}
.workspaces .window-icon-container.active {
background-color: rgba(255, 255, 255, 0.10);
border: 1px solid rgba(255, 255, 255, 0.5);
border-color: rgba(59, 130, 246, 0.76);
border-radius: 7px;
padding: 2px 4px;
}
.workspaces .window-icon-container.pinned {
border-color: rgba(255, 77, 93, 0.74);
box-shadow: inset 0 -2px 0 rgba(255, 77, 93, 0.72);
}
.workspaces .window-icon-container.pinned.active {
border-color: rgba(237, 180, 67, 0.95);
box-shadow:
inset 0 -2px 0 rgba(237, 180, 67, 0.9),
0 0 0 1px rgba(237, 180, 67, 0.22);
}
.workspaces .active .contents,

View File

@@ -1,667 +1,17 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Control.Concurrent (threadDelay)
import Control.Monad (void, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Reader (asks)
import Data.Char (toLower)
import Data.Foldable (for_)
import Data.GI.Base (castTo)
import Data.Int (Int32)
import Data.List (nub)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Ratio ((%))
import Data.Text (Text)
import qualified Data.Text as T
import qualified GI.Gdk as Gdk
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import qualified GI.Gtk as Gtk
import qualified GI.Pango as Pango
import Network.HostName (getHostName)
import qualified StatusNotifier.Tray as SNITray
import System.Environment (lookupEnv)
import System.Environment.XDG.BaseDir (getUserConfigFile)
import System.Log.Logger (Priority (WARNING), rootLoggerName, setLevel, updateGlobalLogger)
import System.Process (spawnCommand)
import System.Taffybar (startTaffybar)
import System.Taffybar.Context
( Backend (BackendWayland, BackendX11),
TaffyIO,
backend,
detectBackend,
runX11Def,
)
import System.Taffybar.Context (appendHook, detectBackend)
import System.Taffybar.DBus
import System.Taffybar.DBus.Toggle
import System.Taffybar.Hooks (withLogLevels)
import System.Taffybar.Information.EWMHDesktopInfo (WorkspaceId (..))
import System.Taffybar.Information.Memory (MemoryInfo (..), parseMeminfo)
import qualified System.Taffybar.Information.Workspaces.Model as WorkspaceModel
import System.Taffybar.Information.X11DesktopInfo
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.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 qualified System.Taffybar.Widget.PulseAudio as PulseAudio
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
import System.Taffybar.Widget.SNITray
( CollapsibleSNITrayParams (..),
SNITrayConfig (..),
defaultCollapsibleSNITrayParams,
defaultSNITrayConfig,
)
import System.Taffybar.Widget.SNITray.PrioritizedCollapsible
( PrioritizedCollapsibleSNITrayParams (..),
defaultPrioritizedCollapsibleSNITrayParams,
sniTrayPrioritizedCollapsibleNewFromParams,
)
import qualified System.Taffybar.Widget.ScreenLock as ScreenLock
import System.Taffybar.Widget.Util (backgroundLoop, buildContentsBox, buildIconLabelBox, loadPixbufByName, widgetSetClassGI)
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import System.Taffybar.WindowIcon (pixBufFromColor)
import Text.Printf (printf)
import Text.Read (readMaybe)
-- | Wrap the widget in a "TaffyBox" (via 'buildContentsBox') and add a CSS class.
decorateWithClassAndBox :: (MonadIO m) => Text -> Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBox klass widget = do
boxed <- buildContentsBox widget
widgetSetClassGI boxed klass
decorateWithClassAndBoxM :: (MonadIO m) => Text -> m Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBoxM klass builder =
builder >>= decorateWithClassAndBox klass
forEachLabelRecursively :: Gtk.Widget -> (Gtk.Label -> IO ()) -> IO ()
forEachLabelRecursively widget action = do
maybeLabel <- castTo Gtk.Label widget
for_ maybeLabel action
maybeContainer <- castTo Gtk.Container widget
case maybeContainer of
Just container ->
Gtk.containerGetChildren container >>= mapM_ (`forEachLabelRecursively` action)
Nothing -> pure ()
setLabelAlignmentRecursively :: Float -> Gtk.Justification -> Gtk.Widget -> IO ()
setLabelAlignmentRecursively xalign justify widget =
forEachLabelRecursively widget $ \label -> do
Gtk.labelSetXalign label xalign
Gtk.labelSetJustify label justify
setFixedLabelWidth :: Int32 -> Gtk.Label -> IO ()
setFixedLabelWidth width label = do
Gtk.labelSetWidthChars label width
Gtk.labelSetMaxWidthChars label width
Gtk.labelSetEllipsize label Pango.EllipsizeModeEnd
-- ** X11 Workspaces
x11FullWorkspaceNames :: X11Property [(WorkspaceId, String)]
x11FullWorkspaceNames =
go <$> readAsListOfString Nothing "_NET_DESKTOP_FULL_NAMES"
where
go = zip [WorkspaceId i | i <- [0 ..]]
remapNSP :: String -> String
remapNSP "NSP" = "S"
remapNSP n = n
workspaceLabelSetter :: WorkspaceModel.WorkspaceInfo -> TaffyIO String
workspaceLabelSetter workspace = do
backendType <- asks backend
let identity = WorkspaceModel.workspaceIdentity workspace
fallbackLabel = remapNSP $ T.unpack (WorkspaceModel.workspaceName identity)
case (backendType, WorkspaceModel.workspaceNumericId identity) of
(BackendX11, Just workspaceId) -> do
fullNames <- runX11Def [] x11FullWorkspaceNames
return $ remapNSP $ fromMaybe fallbackLabel (lookup (WorkspaceId workspaceId) fullNames)
_ -> return fallbackLabel
-- ** Logging
-- ** Hyprland Icon Finding
iconRemap :: [(Text, [Text])]
iconRemap =
[ ("spotify", ["spotify-client", "spotify"])
]
iconRemapMap :: M.Map Text [Text]
iconRemapMap =
M.fromList [(T.toLower k, v) | (k, v) <- iconRemap]
lookupIconRemap :: Text -> [Text]
lookupIconRemap name = fromMaybe [] $ M.lookup (T.toLower name) iconRemapMap
iconNameVariants :: Text -> [Text]
iconNameVariants raw =
let lower = T.toLower raw
stripped = fromMaybe lower (T.stripSuffix ".desktop" lower)
suffixes = ["-gtk", "-client", "-desktop"]
stripSuffixes name =
let variants = mapMaybe (`T.stripSuffix` name) suffixes
in nub $ variants ++ [name]
baseNames = stripSuffixes stripped ++ [raw]
toDash c
| c == ' ' || c == '_' || c == '.' || c == '/' = '-'
| otherwise = c
toUnderscore c
| c == ' ' || c == '-' || c == '.' || c == '/' = '_'
| otherwise = c
variantsFor name =
let dotted =
case T.splitOn "." name of
[] -> name
xs -> last xs
dashed = T.map toDash name
dashedDotted = T.map toDash dotted
underscored = T.map toUnderscore name
underscoredDotted = T.map toUnderscore dotted
in [dotted, dashed, dashedDotted, underscored, underscoredDotted, name]
in nub $ concatMap variantsFor baseNames
workspaceIconCandidates :: WorkspaceModel.WindowInfo -> [Text]
workspaceIconCandidates windowData =
let baseNames = WorkspaceModel.windowClassHints windowData
remapped = concatMap lookupIconRemap baseNames
remappedExpanded = concatMap iconNameVariants remapped
baseExpanded = concatMap iconNameVariants baseNames
in nub (remappedExpanded ++ baseExpanded)
isPathCandidate :: Text -> Bool
isPathCandidate name =
T.isInfixOf "/" name
|| any (`T.isSuffixOf` name) [".png", ".svg", ".xpm"]
workspaceCandidateInfo :: Text -> WorkspaceModel.WindowInfo
workspaceCandidateInfo name =
WorkspaceModel.WindowInfo
{ WorkspaceModel.windowIdentity = WorkspaceModel.HyprlandWindowIdentity "",
WorkspaceModel.windowTitle = "",
WorkspaceModel.windowClassHints = [name],
WorkspaceModel.windowPosition = Nothing,
WorkspaceModel.windowUrgent = False,
WorkspaceModel.windowActive = False,
WorkspaceModel.windowMinimized = False
}
workspaceIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe Gdk.Pixbuf)
workspaceIconFromCandidate size name
| isPathCandidate name =
liftIO $ getPixbufFromFilePath (T.unpack name)
| otherwise =
maybeTCombine
(Workspaces.getWindowIconPixbufFromDesktopEntry size (workspaceCandidateInfo name))
(liftIO $ loadPixbufByName size name)
workspaceManualIconGetter :: Workspaces.WindowIconPixbufGetter
workspaceManualIconGetter =
Workspaces.handleIconGetterException $ \size windowData ->
foldl maybeTCombine (return Nothing) $
map (workspaceIconFromCandidate size) (workspaceIconCandidates windowData)
fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe Gdk.Pixbuf)
fallbackIconPixbuf size = do
let fallbackNames =
[ "application-x-executable",
"application",
"image-missing",
"gtk-missing-image",
"dialog-question",
"utilities-terminal",
"system-run",
"window"
]
tryNames =
foldl
maybeTCombine
(return Nothing)
(map (liftIO . loadPixbufByName size) fallbackNames)
result <- tryNames
case result of
Just _ -> return result
Nothing -> Just <$> pixBufFromColor size 0x5f5f5fff
workspaceFallbackIcon :: Workspaces.WindowIconPixbufGetter
workspaceFallbackIcon size _ =
fallbackIconPixbuf size
workspaceWindowIconGetter :: Workspaces.WindowIconPixbufGetter
workspaceWindowIconGetter =
workspaceManualIconGetter
<|||> Workspaces.getWindowIconPixbufFromChrome
<|||> Workspaces.defaultGetWindowIconPixbuf
<|||> workspaceFallbackIcon
-- ** Host Overrides
-- NOTE: Keep `cssPaths` to a single entrypoint file per host. GTK's
-- `cssProviderLoadFromPath` clears the provider before loading, so handing
-- Taffybar multiple files here causes only the last file to take effect.
defaultCssFiles :: [FilePath]
defaultCssFiles = ["taffybar.css"]
cssFilesByHostname :: [(String, [FilePath])]
cssFilesByHostname =
[("ryzen-shine", ["ryzen-shine.css"])]
laptopHosts :: [String]
laptopHosts =
[ "adell",
"stevie-nixos",
"strixi-minaj",
"jay-lenovo"
]
cssFilesForHost :: String -> [FilePath]
cssFilesForHost hostName =
fromMaybe defaultCssFiles $ lookup hostName cssFilesByHostname
-- ** Widgets
audioWidget :: TaffyIO Gtk.Widget
audioWidget =
decorateWithClassAndBoxM "audio" PulseAudio.pulseAudioNew
networkInnerWidget :: TaffyIO Gtk.Widget
networkInnerWidget =
withNmAppletMenu NetworkManager.networkManagerWifiIconLabelNew
>>= flip widgetSetClassGI "network"
networkWidget :: TaffyIO Gtk.Widget
networkWidget =
decorateWithClassAndBoxM "network" networkInnerWidget
layoutWidget :: TaffyIO Gtk.Widget
layoutWidget =
decorateWithClassAndBoxM "layout" (layoutNew defaultLayoutConfig)
windowsWidget :: TaffyIO Gtk.Widget
windowsWidget =
decorateWithClassAndBoxM
"windows"
( windowsNew
defaultWindowsConfig
{ getActiveLabel = truncatedGetActiveLabel 28,
configureActiveLabel = liftIO . setFixedLabelWidth 28
}
)
workspacesWidget :: TaffyIO Gtk.Widget
workspacesWidget = Workspaces.workspacesNew cfg
where
cfg =
Workspaces.defaultWorkspacesConfig
{ Workspaces.widgetGap = 0,
Workspaces.minIcons = 1,
Workspaces.getWindowIconPixbuf = workspaceWindowIconGetter,
Workspaces.labelSetter = workspaceLabelSetter,
Workspaces.showWorkspaceFn =
\workspace ->
Workspaces.hideEmpty workspace
&& not (WorkspaceModel.workspaceIsSpecial workspace)
}
clockWidget :: TaffyIO Gtk.Widget
clockWidget = do
clock <-
textClockNewWith
defaultClockConfig
{ clockUpdateStrategy = RoundedTargetInterval 60 0.0,
clockFormatString = "%a %b %_d\n%I:%M %p"
}
liftIO $ setLabelAlignmentRecursively 0.5 Gtk.JustificationCenter clock
decorateWithClassAndBox "clock" clock
singleLineMprisLabel :: Text -> Text
singleLineMprisLabel =
T.replace "\n" " " . T.replace "\r" " "
stackedMprisLabel :: Text -> Text
stackedMprisLabel raw =
let normalized = singleLineMprisLabel raw
(top, rest) = T.breakOn " - " normalized
in if T.null rest
then normalized
else top <> "\n" <> T.drop 3 rest
mprisWidget :: TaffyIO Gtk.Widget
mprisWidget =
mpris2NewWithConfig
MPRIS2Config
{ mprisWidgetWrapper = decorateWithClassAndBox "mpris",
updatePlayerWidget =
simplePlayerWidget
defaultPlayerConfig
{ setNowPlayingLabel =
fmap stackedMprisLabel . playingText 20 20,
setupPlayerLabel = setFixedLabelWidth 20
}
}
batteryInnerWidget :: TaffyIO Gtk.Widget
batteryInnerWidget = do
iconWidget <- batteryTextIconNew
labelWidget <- textBatteryNew "$percentage$%"
liftIO (buildIconLabelBox iconWidget labelWidget) >>= flip widgetSetClassGI "battery"
batteryWidget :: TaffyIO Gtk.Widget
batteryWidget =
decorateWithClassAndBoxM "battery" batteryInnerWidget
backlightWidget :: TaffyIO Gtk.Widget
backlightWidget =
decorateWithClassAndBoxM
"backlight"
( backlightLabelNewChanWith
defaultBacklightWidgetConfig
{ backlightFormat = "☀ $percent$%",
backlightUnknownFormat = "☀ n/a",
backlightTooltipFormat =
Just "Device: $device$\nBrightness: $brightness$/$max$ ($percent$%)"
}
)
diskUsageInnerWidget :: TaffyIO Gtk.Widget
diskUsageInnerWidget =
diskUsageNew >>= flip widgetSetClassGI "disk-usage"
diskUsageWidget :: TaffyIO Gtk.Widget
diskUsageWidget =
decorateWithClassAndBoxM "disk-usage" diskUsageInnerWidget
stackInPill :: Text -> [TaffyIO Gtk.Widget] -> TaffyIO Gtk.Widget
stackInPill klass builders =
decorateWithClassAndBoxM klass $ do
widgets <- sequence builders
liftIO $ do
box <- Gtk.boxNew Gtk.OrientationVertical 0
mapM_ (\w -> Gtk.boxPackStart box w False False 0) widgets
Gtk.widgetShowAll box
Gtk.toWidget box
meminfoPercentRowWidget ::
Text ->
Text ->
(MemoryInfo -> Maybe Double) ->
(MemoryInfo -> T.Text) ->
TaffyIO Gtk.Widget
meminfoPercentRowWidget rowClass iconText getRatio tooltipText =
liftIO $ do
iconW <- Gtk.toWidget =<< Gtk.labelNew (Just iconText)
valueLabel <- Gtk.labelNew (Just "")
valueW <- Gtk.toWidget valueLabel
row <- buildIconLabelBox iconW valueW
_ <- widgetSetClassGI row rowClass
let fmtPercent :: Double -> T.Text
fmtPercent r = T.pack (printf "%.0f%%" (max 0 r * 100))
updateOnce :: IO ()
updateOnce = do
info <- parseMeminfo
let valueText = maybe "n/a" fmtPercent (getRatio info)
postGUIASync $ do
Gtk.labelSetText valueLabel valueText
Gtk.widgetSetTooltipText row (Just (tooltipText info))
threadDelay (2 * 1000000)
_ <- Gtk.onWidgetRealize row $ backgroundLoop updateOnce
pure row
ramRowWidget :: TaffyIO Gtk.Widget
ramRowWidget =
meminfoPercentRowWidget
"ram-row"
"\xF538" -- Font Awesome: memory
(Just . memoryUsedRatio)
(\info -> "RAM " <> showMemoryInfo "$used$/$total$" 2 info)
swapRowWidget :: TaffyIO Gtk.Widget
swapRowWidget =
meminfoPercentRowWidget
"swap-row"
"\xF0EC" -- Font Awesome: exchange (swap-ish)
(\info -> if memorySwapTotal info <= 0 then Nothing else Just (memorySwapUsedRatio info))
(\info -> "SWAP " <> showMemoryInfo "$swapUsed$/$swapTotal$" 2 info)
ramSwapWidget :: TaffyIO Gtk.Widget
ramSwapWidget =
stackInPill "ram-swap" [ramRowWidget, swapRowWidget]
audioBacklightWidget :: TaffyIO Gtk.Widget
audioBacklightWidget =
stackInPill
"audio-backlight"
[ PulseAudio.pulseAudioNew,
backlightNewChanWith
defaultBacklightWidgetConfig
{ backlightFormat = "$percent$%",
backlightUnknownFormat = "n/a",
backlightTooltipFormat =
Just "Device: $device$\nBrightness: $brightness$/$max$ ($percent$%)"
}
]
asusInnerWidget :: TaffyIO Gtk.Widget
asusInnerWidget = ASUS.asusWidgetNew
asusWidget :: TaffyIO Gtk.Widget
asusWidget =
decorateWithClassAndBoxM "asus-profile" asusInnerWidget
batteryNetworkWidget :: TaffyIO Gtk.Widget
batteryNetworkWidget =
stackInPill "battery-network" [batteryInnerWidget, networkInnerWidget]
asusDiskUsageWidget :: TaffyIO Gtk.Widget
asusDiskUsageWidget =
stackInPill "asus-disk-usage" [diskUsageInnerWidget, asusInnerWidget]
screenLockWidget :: TaffyIO Gtk.Widget
screenLockWidget =
decorateWithClassAndBoxM "screen-lock" $
ScreenLock.screenLockNewWithConfig
ScreenLock.defaultScreenLockConfig
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock"
}
wlsunsetWidget :: TaffyIO Gtk.Widget
wlsunsetWidget =
decorateWithClassAndBoxM "wlsunset" $
Wlsunset.wlsunsetNewWithConfig
Wlsunset.defaultWlsunsetWidgetConfig
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun"
}
simplifiedScreenLockWidget :: TaffyIO Gtk.Widget
simplifiedScreenLockWidget =
-- Inner widget: no extra pill wrapping (the combiner provides that).
ScreenLock.screenLockNewWithConfig
ScreenLock.defaultScreenLockConfig
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock"
}
simplifiedScreensaverWidget :: TaffyIO Gtk.Widget
simplifiedScreensaverWidget =
liftIO $ do
label <- Gtk.labelNew (Just (T.pack "\xF108" <> " Saver"))
ebox <- Gtk.eventBoxNew
Gtk.containerAdd ebox label
_ <- widgetSetClassGI ebox "screensaver"
Gtk.widgetSetTooltipText ebox (Just "Left click: toggle screensaver\nRight click: stop screensaver")
void $ Gtk.onWidgetButtonPressEvent ebox $ \event -> do
eventType <- Gdk.getEventButtonType event
button <- Gdk.getEventButtonButton event
if eventType /= Gdk.EventTypeButtonPress
then return False
else case button of
1 -> do
void $ spawnCommand "hypr-screensaver toggle >/dev/null 2>&1"
return True
3 -> do
void $ spawnCommand "hypr-screensaver stop >/dev/null 2>&1"
return True
_ -> return False
Gtk.widgetShowAll ebox
Gtk.toWidget ebox
screensaverWidget :: TaffyIO Gtk.Widget
screensaverWidget =
decorateWithClassAndBoxM "screensaver" simplifiedScreensaverWidget
simplifiedWlsunsetWidget :: TaffyIO Gtk.Widget
simplifiedWlsunsetWidget =
-- Inner widget: no extra pill wrapping (the combiner provides that).
Wlsunset.wlsunsetNewWithConfig
Wlsunset.defaultWlsunsetWidgetConfig
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun"
}
sunLockWidget :: TaffyIO Gtk.Widget
sunLockWidget =
stackInPill "sun-lock" [simplifiedWlsunsetWidget, simplifiedScreenLockWidget]
cpuWidget :: TaffyIO Gtk.Widget
cpuWidget =
decorateWithClassAndBoxM "cpu" $
cpuMonitorNew
defaultGraphConfig
{ graphDataColors = [(0, 1, 0.5, 0.8), (1, 0, 0, 0.5)],
graphBackgroundColor = (0, 0, 0, 0),
graphBorderWidth = 0,
graphLabel = Just "CPU",
graphWidth = 50,
graphDirection = LEFT_TO_RIGHT
}
1.0
"cpu"
wakeupDebugWidget :: TaffyIO Gtk.Widget
wakeupDebugWidget =
decorateWithClassAndBoxM "wakeup-debug" wakeupDebugWidgetNew
openAIUsageWidget :: TaffyIO Gtk.Widget
openAIUsageWidget =
decorateWithClassAndBoxM "openai-usage" openAIUsageStackNew
anthropicUsageWidget :: TaffyIO Gtk.Widget
anthropicUsageWidget =
decorateWithClassAndBoxM "anthropic-usage" anthropicUsageStackNew
sniPriorityVisibilityThresholdDefault :: Int
sniPriorityVisibilityThresholdDefault = 0
sniTrayWidget :: TaffyIO Gtk.Widget
sniTrayWidget = do
-- If the Haskell backend regresses, flip at runtime:
-- TAFFYBAR_SNI_MENU_BACKEND=lib
backendEnv <- liftIO (lookupEnv "TAFFYBAR_SNI_MENU_BACKEND")
thresholdEnv <- liftIO (lookupEnv "TAFFYBAR_SNI_PRIORITY_THRESHOLD")
let menuBackend =
case fmap (map toLower) backendEnv of
Just "lib" -> SNITray.LibDBusMenu
_ -> SNITray.HaskellDBusMenu
visibilityThreshold =
fromMaybe
sniPriorityVisibilityThresholdDefault
(thresholdEnv >>= readMaybe)
trayParams =
SNITray.defaultTrayParams
{ SNITray.trayMenuBackend = menuBackend,
SNITray.trayOverlayScale = 1 % 3,
SNITray.trayEventHooks = SNITray.defaultTrayEventHooks
}
sniTrayConfig =
defaultSNITrayConfig
{ sniTrayTrayParams = trayParams
}
collapsibleParams =
defaultCollapsibleSNITrayParams
{ collapsibleSNITrayConfig = sniTrayConfig
}
prioritizedParams =
defaultPrioritizedCollapsibleSNITrayParams
{ prioritizedCollapsibleSNITrayParams = collapsibleParams,
prioritizedCollapsibleSNITrayVisibilityThreshold = Just visibilityThreshold
}
decorateWithClassAndBoxM
"sni-tray"
(sniTrayPrioritizedCollapsibleNewFromParams prioritizedParams)
-- ** Layout
startWidgetsForBackend :: Backend -> [TaffyIO Gtk.Widget]
startWidgetsForBackend backend =
case backend of
BackendX11 -> [workspacesWidget, layoutWidget, windowsWidget]
-- These Wayland widgets are Hyprland-specific.
BackendWayland -> [workspacesWidget, windowsWidget]
endWidgetsForHost :: String -> [TaffyIO Gtk.Widget]
endWidgetsForHost hostName =
-- NOTE: end widgets are packed with Gtk.boxPackEnd, so the list order is
-- right-to-left on screen. Make the tray appear at the far right by placing
-- it first in the list. (On laptops: the battery/wifi stack is far right,
-- tray immediately left of it.)
let baseEndWidgets =
[ sniTrayWidget,
audioWidget,
anthropicUsageWidget,
openAIUsageWidget,
cpuWidget,
ramSwapWidget,
diskUsageWidget,
networkWidget,
screensaverWidget,
sunLockWidget,
mprisWidget
]
laptopEndWidgets =
[ batteryNetworkWidget,
sniTrayWidget,
asusDiskUsageWidget,
audioBacklightWidget,
anthropicUsageWidget,
openAIUsageWidget,
cpuWidget,
ramSwapWidget,
screensaverWidget,
sunLockWidget,
mprisWidget
]
in if hostName `elem` laptopHosts
then laptopEndWidgets
else baseEndWidgets
mkSimpleTaffyConfig :: String -> Backend -> [FilePath] -> SimpleTaffyConfig
mkSimpleTaffyConfig hostName backend cssFiles =
defaultSimpleTaffyConfig
{ startWidgets = startWidgetsForBackend backend,
centerWidgets = [clockWidget],
endWidgets = endWidgetsForHost hostName,
barLevels = Nothing,
barPosition = Top,
widgetSpacing = 0,
barPadding = if hostName == "ryzen-shine" then 2 else 4,
barHeight =
if hostName == "ryzen-shine"
then ScreenRatio $ 1 / 40
else ScreenRatio $ 1 / 33,
cssPaths = cssFiles
}
-- ** Entry Point
import System.Taffybar.Information.ChromeWindowInfo (registerChromeWindowInfoRefreshRequests)
import System.Taffybar.SimpleConfig (toTaffybarConfig)
import TaffybarConfig.Config (mkSimpleTaffyConfig)
import TaffybarConfig.Host (cssFilesForHost)
main :: IO ()
main = do
@@ -676,4 +26,5 @@ main = do
withLogServer $
withLogLevels $
withToggleServer $
toTaffybarConfig simpleTaffyConfig
appendHook registerChromeWindowInfoRefreshRequests $
toTaffybarConfig simpleTaffyConfig

View File

@@ -108,7 +108,7 @@ myConfig = def
, borderWidth = 0
, logHook
= updatePointer (0.5, 0.5) (0, 0)
<> toggleFadeInactiveLogHook 0.9
<> toggleFadeInactiveLogHook 0.65
<> workspaceHistoryHook
<> setWorkspaceNames
<> logHook def
@@ -232,40 +232,29 @@ 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
codexSelector = className =? "codex-desktop"
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")
]
-- Commands
chromeCommand = "google-chrome-stable"
codexCommand = "codex_desktop_scratchpad"
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"
@@ -812,10 +801,9 @@ nearFullFloat = customFloating $ W.RationalRect l t w h
scratchpads =
[ NS "element" elementCommand elementSelector nearFullFloat
, NS "gmail" gmailCommand gmailSelector nearFullFloat
[ NS "codex" codexCommand codexSelector nearFullFloat
, NS "element" elementCommand elementSelector 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,22 +1000,16 @@ 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
[ (modalt, xK_c, spawn chromeCommand, chromeSelector)
] ++
-- ScratchPads
[ ((modalt, xK_e), doScratchpad "element")
, ((modalt, xK_g), doScratchpad "gmail")
[ ((modalt, xK_c), doScratchpad "codex")
, ((modalt, xK_e), doScratchpad "element")
, ((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")
@@ -1047,7 +1029,7 @@ addKeys conf@XConfig { modMask = modm } =
, ((modm, xK_m), withFocused minimizeWindow)
, ((modm .|. shiftMask, xK_m),
deactivateFullOr $ withLastMinimized maximizeWindowAndFocus)
, ((modm, xK_x), addHiddenWorkspace "NSP" >> windows (W.shift "NSP"))
, ((modm, xK_x), spawn "rofi_command.sh")
, ((modalt, xK_space), deactivateFullOr restoreOrMinimizeOtherClasses)
, ((modalt, xK_Return), deactivateFullAnd restoreAllMinimized)
, ((hyper, xK_g), gatherThisClass)
@@ -1087,21 +1069,23 @@ addKeys conf@XConfig { modMask = modm } =
, ((modm, xK_v), spawn "xclip -o | xdotool type --file -")
, ((hyper, xK_v), spawn "rofi -modi 'clipboard:greenclip print' -show clipboard")
, ((hyper, xK_p), spawn "rofi-pass")
, ((hyper, xK_h), spawn "rofi_shutter")
, ((hyper, xK_c), spawn "shell_command.sh")
, ((hyper, xK_x), spawn "rofi_command.sh")
, ((0, xK_Print), spawn "flameshot gui")
, ((hyper, xK_h), spawn "flameshot gui")
, ((hyper, xK_c), spawn "rofi_tmcodex.sh")
, ((hyper .|. shiftMask, xK_c), spawn "rofi_tmcodex.sh resume")
, ((hyper .|. shiftMask, xK_l), spawn "dm-tool lock")
, ((hyper, xK_l), selectLayout)
, ((hyper, xK_k), spawn "rofi_kill_process.sh")
, ((hyper .|. shiftMask, xK_k), spawn "rofi_kill_all.sh")
, ((hyper, xK_r), spawn "rofi-systemd")
, ((hyper, xK_r), spawn "rofi_systemd_mono")
, ((hyper, xK_9), spawn "start_synergy.sh")
, ((hyper, xK_slash), spawn "toggle_taffybar")
, ((hyper, xK_backslash), spawn "$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")
, ((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 .|. shiftMask, xK_o), spawn "$HOME/dotfiles/dotfiles/lib/bin/kef-optical")
, ((hyper, xK_comma), spawn "rofi_wallpaper.sh")
, ((hyper, xK_y), spawn "rofi_agentic_skill")
, ((modm, xK_e), spawn "emacsclient --eval '(emacs-everywhere)'")

View File

@@ -0,0 +1,24 @@
keybinds {
// Keep Ctrl-p available for readline/history/up in shells and editors.
unbind "Ctrl p"
shared_except "locked" "pane" {
bind "Ctrl Space" { SwitchToMode "Pane"; }
}
pane {
bind "Ctrl Space" { SwitchToMode "Normal"; }
}
tmux {
// Ctrl-b C: start a Codex pane from the current zellij tab.
bind "C" {
Run "codex" "--dangerously-bypass-approvals-and-sandbox" {
name "codex"
}
SwitchToMode "Normal"
}
}
}
default_mode "locked"

View File

@@ -58,8 +58,8 @@ think its pretty awesome!
([[https://github.com/IvanMalison/emit#compose][README]])
+ [[Add a blacklist to a major mode]]
** Configuration of My Own Packages
- [[term-projectile][term-projectile]] and [[term-manager][term-manager]]
- [[org-projectile][org-projectile]]
- [[term-project][term-project]] and [[term-manager][term-manager]]
- [[org-project-capture][org-project-capture]]
- [[multi-line][multi-line]]
- [[github-search][github-search]]
- [[flimenu][flimenu]]
@@ -737,10 +737,15 @@ aren't visiting a file but are associated with a directory."
(imalison:copy-buffer-file-path-builder imalison:copy-buffer-file-path-full)
(imalison:copy-buffer-file-path-builder imalison:copy-buffer-file-name
file-name-nondirectory)
(imalison:copy-buffer-file-path-builder imalison:copy-buffer-file-path
car
projectile-make-relative-to-root
list)
(defun imalison:buffer-file-project-relative-name ()
"Return the current buffer file or directory relative to its project root."
(let ((filename (imalison:buffer-file-name-or-directory)))
(file-relative-name filename
(imalison:project-root
(file-name-directory filename)))))
(imalison:compose-copy-builder imalison:copy-buffer-file-path
imalison:buffer-file-project-relative-name)
#+END_SRC
*** Copy the current branch using magit
#+BEGIN_SRC emacs-lisp
@@ -986,7 +991,12 @@ I keep it around just in case I need it.
(shell-command (format "grownotify -t %s -m %s" title message)))
(defun notify-send (title message)
(shell-command (format "notify-send -u critical %s %s" title message)))
(when-let ((program (executable-find "notify-send")))
(let ((process-connection-type nil))
(start-process "notify-send" nil program
"-u" "critical"
(or title "No title")
(or message "No message")))))
(defvar notify-function
(cond ((eq system-type 'darwin) 'notification-center)
@@ -1315,6 +1325,7 @@ Paradox is a package.el extension. I have no use for it now that I use straight.
** gcmh
#+begin_src emacs-lisp
(use-package gcmh
:defer 5
:config (gcmh-mode 1))
#+end_src
** diminish
@@ -1333,7 +1344,33 @@ Paradox is a package.el extension. I have no use for it now that I use straight.
** emacs-everywhere
#+begin_src emacs-lisp
(use-package emacs-everywhere
:commands emacs-everywhere)
:commands emacs-everywhere
:config
(progn
(defun imalison:emacs-everywhere-app-info-hyprland ()
(require 'json)
(let* ((window (json-parse-string
(shell-command-to-string "hyprctl -j activewindow")
:object-type 'alist
:array-type 'list))
(address (alist-get 'address window))
(class (or (alist-get 'initialClass window)
(alist-get 'class window)
""))
(title (or (alist-get 'title window) "")))
(unless address
(user-error "Unable to determine active Hyprland window"))
(make-emacs-everywhere-app
:id address
:class class
:title title)))
(add-to-list 'emacs-everywhere-system-configs
'((wayland . Hyprland)
:focus-command ("hyprctl" "dispatch" "focuswindow" "address:%w")
:info-function imalison:emacs-everywhere-app-info-hyprland)
t)
(setq emacs-everywhere--system-configured nil)))
#+end_src
** atomic-chrome
#+BEGIN_SRC emacs-lisp
@@ -1383,8 +1420,10 @@ The file server file for this emacs instance no longer exists.")
(defun imalison:get-this-server-filepath ()
(let ((server-dir (if server-use-tcp server-auth-dir server-socket-dir)))
(expand-file-name server-name server-dir)))
(when (equal nil (server-running-p)) (server-start)
(imalison:make-main-emacs-server))))
(unless (daemonp)
(when (equal nil (server-running-p))
(server-start)
(imalison:make-main-emacs-server)))))
#+END_SRC
** list-environment
#+BEGIN_SRC emacs-lisp
@@ -1392,7 +1431,8 @@ The file server file for this emacs instance no longer exists.")
#+END_SRC
** bug-hunter
#+BEGIN_SRC emacs-lisp
(use-package bug-hunter)
(use-package bug-hunter
:defer t)
#+END_SRC
** shackle
#+BEGIN_SRC emacs-lisp
@@ -1630,7 +1670,7 @@ out how to detect that a buffer is a man mode buffer.
(defhydra imalison:hydra-yank
nil
"Yank text"
("p" imalison:copy-buffer-file-path "Projectile path")
("p" imalison:copy-buffer-file-path "Project path")
("b" imalison:copy-current-buffer-name "Buffer Name")
("f" imalison:copy-buffer-file-path-full "Full path")
("n" imalison:copy-buffer-file-name "File name")
@@ -1641,7 +1681,7 @@ out how to detect that a buffer is a man mode buffer.
#+BEGIN_SRC emacs-lisp
(defun imalison:make-test ()
(interactive)
(let ((default-directory (projectile-project-root)))
(let ((default-directory (imalison:project-root)))
(imalison:named-compile "make test")))
(defun imalison:glide-up ()
@@ -1738,71 +1778,41 @@ bind-key and global-set-key forms.
(setq zop-to-char-kill-keys '(?\C-k ?\C-w))
(setq zop-to-char-quit-at-point-keys '(?\r))))
#+END_SRC
** projectile
** project
#+BEGIN_SRC emacs-lisp
(use-package projectile
(use-package project
:ensure nil
:demand t
:bind (:map projectile-mode-map
("C-c p f" . imalison:projectile-find-file)
("C-c p" . projectile-command-map)
("C-c p s" . imalison:do-rg)
("C-c p f" . imalison:projectile-find-file))
:bind-keymap
("C-c p" . project-prefix-map)
:custom
((projectile-require-project-root nil)
(projectile-enable-caching nil)
(projectile-git-submodule-command nil)
(projectile-git-use-fd t)
(project-vc-merge-submodules nil)
(projectile-current-project-on-switch 'keep))
((project-vc-merge-submodules nil)
(project-vc-extra-root-markers '(".project" ".projectile"))
(project-switch-commands 'imalison:project-find-file))
:config
(progn
(defmacro imalison:projectile-do-in-project (project-dir &rest forms)
(defun imalison:project-root (&optional directory)
"Return DIRECTORY's project root, falling back to DIRECTORY itself."
(let* ((default-directory (file-name-as-directory
(expand-file-name
(or directory default-directory))))
(project (project-current nil default-directory)))
(file-name-as-directory
(expand-file-name
(if project
(project-root project)
default-directory)))))
(defmacro imalison:project-do-in-project (project-dir &rest forms)
`(imalison:with-default-directory ,project-dir
(noflet ((projectile-project-root (&rest args) ,project-dir))
,@forms)))
(let ((project-current-directory-override ,project-dir))
,@forms)))
(defmacro imalison:with-default-directory (directory &rest forms)
`(let ((default-directory ,directory))
,@forms))
(defvar imalison:projectile-find-ignore-file ".projectile-find-ignore"
"Project-local fd ignore file used only for Projectile file finding.")
(defun imalison:projectile-fd-ignore-file-arg ()
"Return a shell fragment that adds `imalison:projectile-find-ignore-file' when present."
(let ((ignore-file (shell-quote-argument imalison:projectile-find-ignore-file)))
(format "$(test -f %s && printf -- '--ignore-file %s')" ignore-file ignore-file)))
(defun imalison:projectile-fd-command (&optional pattern)
"Build an fd command for Projectile file finding.
The command includes ignored and hidden files by default, then applies
`imalison:projectile-find-ignore-file' when that file exists in the project
root. PATTERN is passed as fd's search pattern when non-nil."
(mapconcat
#'identity
(delq nil
(list projectile-fd-executable
pattern
"-H"
"--no-ignore"
(imalison:projectile-fd-ignore-file-arg)
"-0"
"-E .git"
"-tf"
"--strip-cwd-prefix"
"-c never"))
" "))
(when projectile-fd-executable
(setq projectile-git-fd-args
(replace-regexp-in-string
(concat "^" (regexp-quote projectile-fd-executable) " ")
""
(imalison:projectile-fd-command)))
(setq projectile-generic-command
(imalison:projectile-fd-command ".")))
(defun imalison:do-rg-default-directory (&rest args)
(interactive)
(let ((consult-ripgrep-args (concat consult-ripgrep-args " --no-ignore" " --hidden")))
@@ -1812,24 +1822,35 @@ root. PATTERN is passed as fd's search pattern when non-nil."
consult-ripgrep
imalison:do-rg-default-directory)
(emit-prefix-selector imalison:projectile-find-file
projectile-find-file
projectile-find-file-other-window)
(defun imalison:project-find-file ()
"Find a file in the current `project.el' project."
(interactive)
(let* ((project-root (file-name-as-directory
(expand-file-name
(or project-current-directory-override
(imalison:project-root)))))
(default-directory project-root)
(project-current-directory-override project-root))
(call-interactively #'project-find-file)))
(defun imalison:project-switch-project ()
"Switch projects using `project.el'."
(interactive)
(call-interactively #'project-switch-project))
(define-key project-prefix-map (kbd "f") #'imalison:project-find-file)
(define-key project-prefix-map (kbd "s") #'imalison:do-rg)
(imalison:let-around imalison:set-options-do-rg
imalison:do-rg)
(defun imalison:projectile-make-all-subdirs-projects (directory)
(defun imalison:project-make-all-subdirs-projects (directory)
(cl-loop for file-info in (directory-files-and-attributes directory)
do (when (nth 1 file-info)
(write-region "" nil
(expand-file-name
(concat directory "/"
(nth 0 file-info) "/.projectile")))))))
:config
(progn
(projectile-global-mode)
(diminish 'projectile-mode)))
(nth 0 file-info) "/.project"))))))))
#+END_SRC
** ido
#+BEGIN_SRC emacs-lisp
@@ -1984,37 +2005,38 @@ root. PATTERN is passed as fd's search pattern when non-nil."
:config
(progn
(setq embark-mixed-indicator-delay 1.0)
(defmacro imalison:embark-projectile-act-for-file (file &rest forms)
`(let ((default-directory (projectile-project-root ,file)))
(imalison:projectile-do-in-project default-directory ,@forms)))
(defmacro imalison:embark-project-act-for-file (file &rest forms)
`(let ((default-directory
(imalison:project-root (file-name-directory ,file))))
(imalison:project-do-in-project default-directory ,@forms)))
(defmacro imalison:build-embark-projectile-for-file (command)
`(defun ,(intern (concat "imalison:embark-projectile-file-" (symbol-name command))) (filepath)
(imalison:embark-projectile-act-for-file filepath (,command))))
(defmacro imalison:build-embark-project-for-file (command)
`(defun ,(intern (concat "imalison:embark-project-file-" (symbol-name command))) (filepath)
(imalison:embark-project-act-for-file filepath (,command))))
(imalison:build-embark-projectile-for-file term-projectile-switch)
(imalison:build-embark-projectile-for-file magit-status)
(imalison:build-embark-projectile-for-file consult-ripgrep)
(imalison:build-embark-project-for-file term-project-forward)
(imalison:build-embark-project-for-file magit-status)
(imalison:build-embark-project-for-file consult-ripgrep)
(setq embark-prompter #'embark-keymap-prompter)
(define-key embark-general-map (kbd "t")
#'imalison:embark-projectile-file-term-projectile-switch)
#'imalison:embark-project-file-term-project-forward)
(define-key embark-general-map (kbd "m")
#'imalison:embark-projectile-file-magit-status)
#'imalison:embark-project-file-magit-status)
(define-key embark-general-map (kbd "g")
#'imalison:embark-projectile-file-magit-status)
#'imalison:embark-project-file-magit-status)
(define-key embark-general-map (kbd "s")
#'imalison:embark-projectile-file-consult-ripgrep)
#'imalison:embark-project-file-consult-ripgrep)
(defvar-keymap imalison:projectile-embark-map
:doc "Keymap for actions on projectile projects"
"m" #'imalison:embark-projectile-file-magit-status
"t" #'imalison:embark-projectile-file-term-projectile-switch
"s" #'imalison:embark-projectile-file-consult-ripgrep)))
(defvar-keymap imalison:project-embark-map
:doc "Keymap for actions on projects"
"m" #'imalison:embark-project-file-magit-status
"t" #'imalison:embark-project-file-term-project-forward
"s" #'imalison:embark-project-file-consult-ripgrep)))
(use-package embark-consult
:hook
@@ -2029,7 +2051,7 @@ root. PATTERN is passed as fd's search pattern when non-nil."
("C-x C-i" . consult-imenu))
:config
(progn
(setq consult-project-function 'projectile-project-root)))
(setq consult-project-function #'imalison:project-root)))
#+end_src
** company
#+BEGIN_SRC emacs-lisp
@@ -2122,8 +2144,7 @@ root. PATTERN is passed as fd's search pattern when non-nil."
** multi-line
#+BEGIN_SRC emacs-lisp
(use-package multi-line
;; Demand multi-line to avoid failure to load mode specific strategies
:demand t
:commands multi-line
:bind ("C-c d" . multi-line)
:config
(progn
@@ -2188,11 +2209,10 @@ root. PATTERN is passed as fd's search pattern when non-nil."
** yasnippet
#+BEGIN_SRC emacs-lisp
(use-package yasnippet
:defer 5
:commands (yas-global-mode)
:commands (yas-expand yas-global-mode yas-insert-snippet yas-minor-mode)
:hook ((prog-mode text-mode conf-mode) . yas-minor-mode)
:config
(progn
(yas-global-mode)
(diminish 'yas-minor-mode)
(add-hook 'term-mode-hook (lambda() (yas-minor-mode -1)))
(setq yas-prompt-functions
@@ -2200,8 +2220,7 @@ root. PATTERN is passed as fd's search pattern when non-nil."
(cl-delete 'yas-ido-prompt yas-prompt-functions)))))
(use-package yasnippet-snippets
:after yasnippet
:demand t)
:after yasnippet)
#+END_SRC
** align
#+BEGIN_SRC emacs-lisp
@@ -2322,7 +2341,7 @@ root. PATTERN is passed as fd's search pattern when non-nil."
(defvar imalison:use-lsp-go t)
(defun imalison:glide-novendor ()
(projectile-with-default-dir (projectile-project-root)
(let ((default-directory (imalison:project-root)))
(shell-command-to-string "glide novendor")))
(defun imalison:go-mode-create-imenu-index ()
@@ -2347,7 +2366,7 @@ root. PATTERN is passed as fd's search pattern when non-nil."
(nconc type-index (list (cons "func" func-index)))))
(defun imalison:go-workspace-path ()
(file-relative-name (projectile-project-root)
(file-relative-name (imalison:project-root)
(concat (file-name-as-directory
(imalison:get-go-path)) "src")))
@@ -2380,12 +2399,10 @@ root. PATTERN is passed as fd's search pattern when non-nil."
(if (executable-find "goimports") "goimports" "gofmt"))
(setq-local imenu-create-index-function
#'imalison:go-mode-create-imenu-index)
(make-local-variable 'projectile-globally-ignored-files)
(add-hook 'after-save-hook 'imalison:install-current-go-project nil
'yes-do-local)
(add-hook 'before-save-hook 'gofmt-before-save nil 'yes-do-local)
(add-to-list 'projectile-globally-ignored-files
"vendor")
(setq-local project-vc-ignores (cons "vendor/" project-vc-ignores))
(when (and imalison:use-lsp-go
(fboundp 'lsp-deferred)
(or (executable-find "gopls")
@@ -2897,11 +2914,14 @@ The following is taken from [[https://github.com/syl20bnr/spacemacs/blob/a650877
#+END_SRC
*** swift
#+begin_src emacs-lisp
(use-package swift-mode)
(use-package swift-mode
:mode "\\.swift\\'")
#+end_src
*** groovy
#+begin_src emacs-lisp
(use-package groovy-mode)
(use-package groovy-mode
:mode (("\\.groovy\\'" . groovy-mode)
("\\.gradle\\'" . groovy-mode)))
#+end_src
*** vala
#+BEGIN_SRC emacs-lisp
@@ -2951,7 +2971,8 @@ The following is taken from [[https://github.com/syl20bnr/spacemacs/blob/a650877
#+END_SRC
*** graphql
#+begin_src emacs-lisp
(use-package graphql-mode)
(use-package graphql-mode
:mode "\\.graphql\\'")
#+end_src
*** json-mode
#+BEGIN_SRC emacs-lisp
@@ -3475,18 +3496,20 @@ in term-mode. This makes term-mode 1000% more useful
(advice-add
'term-manager-default-build-term :after 'imalison:set-escape-char)))
#+END_SRC
** term-projectile
** term-project
#+BEGIN_SRC emacs-lisp
(use-package term-projectile
(use-package term-project
:ensure (term-project :files ("term-project.el")
:host github :repo "colonelpanic8/term-manager")
:bind ("C-c 7" . imalison:term-hydra-global/body)
:commands
(term-projectile-backward
term-projectile-create-new
term-projectile-create-new-default-directory
term-projectile-default-directory-backward
term-projectile-default-directory-forward
term-projectile-forward
term-projectile-switch)
(term-project-backward
term-project-create-new
term-project-default-directory-backward
term-project-default-directory-create-new
term-project-default-directory-forward
term-project-forward
term-project-switch)
:config
(progn
(use-package term-manager-eat
@@ -3494,37 +3517,51 @@ in term-mode. This makes term-mode 1000% more useful
:ensure
(term-manager-eat :files ("term-manager-eat.el")
:host github :repo "colonelpanic8/term-manager"))
(setq term-projectile-term-manager (term-projectile :build-term 'term-manager-eat-build-term))
(require 'term-manager-eat)
(defun term-project-get-symbol-for-buffer (buffer)
"Get the project root symbol for BUFFER, falling back to its directory."
(term-project-maybe-intern
(with-current-buffer buffer
(if (or (derived-mode-p 'term-mode)
(derived-mode-p 'eat-mode))
default-directory
(imalison:project-root)))))
(setq term-project-term-manager
(term-project :build-term 'term-manager-eat-build-term))
(term-manager-enable-eat-buffer-renaming-and-reindexing
term-project-term-manager)
(emit-prefix-selector imalison:term
term-projectile-forward
term-projectile-create-new)
term-project-forward
term-project-create-new)
(defvar imalison:term-hydra-original-default-directory)
(defhydra imalison:term-hydra-default-directory
(:body-pre
(term-projectile-default-directory-forward-restored))
(term-project-default-directory-forward-restored))
"term - default-directory"
("s" term-projectile-switch-to "Switch to existing")
("f" term-projectile-default-directory-forward-restored "Forward for current directory terminals")
("b" term-projectile-default-directory-backward-restored "Backward for current directory terminals")
("c" term-projectile-default-directory-create-new-restored "Create new current directory terminal")
("d" term-projectile-default-directory-forward-restored "Switch/Create default directory terminal")
("s" term-project-switch-to "Switch to existing")
("f" term-project-default-directory-forward-restored "Forward for current directory terminals")
("b" term-project-default-directory-backward-restored "Backward for current directory terminals")
("c" term-project-default-directory-create-new-restored "Create new current directory terminal")
("d" term-project-default-directory-forward-restored "Switch/Create default directory terminal")
("g" imalison:term-hydra-global/body-restored "Switch/Create global terminal" :exit t)
("p" imalison:term-hydra-projectile/body-restored "Switch/Create project terminal" :exit t))
("p" imalison:term-hydra-project/body-restored "Switch/Create project terminal" :exit t))
(defhydra imalison:term-hydra-projectile
(defhydra imalison:term-hydra-project
(:body-pre
(progn
(term-projectile-forward-restored)))
"term - projectile"
("s" term-projectile-switch-to "Switch to existing")
("f" term-projectile-forward-restored "Forward for project terminals")
("b" term-projectile-backward-restored "Backward for project terminals")
("c" term-projectile-create-new-restored "Create new project terminal")
(term-project-forward-restored)))
"term - project"
("s" term-project-switch-to "Switch to existing")
("f" term-project-forward-restored "Forward for project terminals")
("b" term-project-backward-restored "Backward for project terminals")
("c" term-project-create-new-restored "Create new project terminal")
("d" imalison:term-hydra-default-directory/body-restored "Switch/Create default directory terminal" :exit t)
("g" imalison:term-hydra-global/body-restored "Switch/Create global terminal" :exit t)
("p" term-projectile-forward-restored "Switch/Create project terminal"))
("p" term-project-forward-restored "Switch/Create project terminal"))
(defhydra imalison:term-hydra-global
@@ -3532,31 +3569,31 @@ in term-mode. This makes term-mode 1000% more useful
(progn (setq imalison:term-hydra-original-default-directory
default-directory)))
"term - global"
("s" term-projectile-switch-to "Switch to existing")
("f" term-projectile-global-forward-restored "Forward for project terminals")
("b" term-projectile-global-backward-restored "Backward for project terminals")
("c" term-projectile-global-create-new-restored "Create new project terminal")
("s" term-project-switch-to "Switch to existing")
("f" term-project-global-forward-restored "Forward for project terminals")
("b" term-project-global-backward-restored "Backward for project terminals")
("c" term-project-global-create-new-restored "Create new project terminal")
("d" imalison:term-hydra-default-directory/body-restored "Switch/Create default directory terminal" :exit t)
("g" term-projectile-global-forward-restored "Switch/Create global terminal")
("p" imalison:term-hydra-projectile/body-restored "Switch/Create project terminal" :exit t))
("g" term-project-global-forward-restored "Switch/Create global terminal")
("p" imalison:term-hydra-project/body-restored "Switch/Create project terminal" :exit t))
(mapcar (lambda (term-projectile-function)
(defalias (imalison:concat-symbols term-projectile-function '-restored)
(mapcar (lambda (term-project-function)
(defalias (imalison:concat-symbols term-project-function '-restored)
(lambda (&rest args)
(interactive)
(let ((default-directory imalison:term-hydra-original-default-directory))
(apply term-projectile-function args)))))
'(term-projectile-default-directory-forward
term-projectile-default-directory-backward
term-projectile-default-directory-create-new
term-projectile-forward
term-projectile-backward
term-projectile-create-new
term-projectile-global-forward
term-projectile-global-backward
term-projectile-global-create-new
(apply term-project-function args)))))
'(term-project-default-directory-forward
term-project-default-directory-backward
term-project-default-directory-create-new
term-project-forward
term-project-backward
term-project-create-new
term-project-global-forward
term-project-global-backward
term-project-global-create-new
imalison:term-hydra-global/body
imalison:term-hydra-projectile/body
imalison:term-hydra-project/body
imalison:term-hydra-default-directory/body))))
#+END_SRC
** crux
@@ -3638,8 +3675,28 @@ I don't use iedit directly, but it is used by [[*emr][emr]] and I need to disabl
(use-package tramp
:ensure nil
:commands tramp
:init
;; Avoid TRAMP's GVFS backend entirely. It depends on D-Bus/GVFS desktop
;; services, while this config uses ordinary TRAMP methods.
(setq tramp-gvfs-methods nil)
:config
(setq tramp-default-method "scp"))
(setq tramp-default-method "scp")
(setq tramp-methods
(seq-remove
(lambda (method)
(member (car method)
'("afp" "dav" "davs" "gdrive" "mtp" "nextcloud" "sftp")))
tramp-methods))
(when (boundp 'tramp-foreign-file-name-handler-alist)
(setq tramp-foreign-file-name-handler-alist
(assq-delete-all 'tramp-gvfs-file-name-p
tramp-foreign-file-name-handler-alist))))
#+END_SRC
** dbus
#+BEGIN_SRC emacs-lisp
;; D-Bus is disabled by lisp/dbus.el, which shadows Emacs' built-in dbus.el.
;; Keep the event hook empty in case another environment loads the real library.
(setq dbus-event-error-functions nil)
#+END_SRC
** narrow-indirect
#+BEGIN_SRC emacs-lisp
@@ -3706,7 +3763,9 @@ I had to disable this mode because something that it does messes with coding set
:demand t
:config
(progn
(setq recentf-max-saved-items 1000
(setq recentf-initialize-file-name-history nil
recentf-auto-cleanup 'never
recentf-max-saved-items 1000
recentf-max-menu-items 1000)
(advice-add 'recentf-cleanup :around 'imalison:shut-up-around)
(recentf-mode +1)))
@@ -3728,8 +3787,8 @@ I have currently disabled key-chord because it may cause typing lag.
(key-chord-mode 1)
(advice-add 'imalison:avy :around 'imalison:disable-keychord-around)
(key-chord-define-global "tg" 'imalison:term-hydra/body)
(key-chord-define-global "pj" 'imalison:projectile-find-file)
(key-chord-define-global "p[" 'projectile-switch-project)
(key-chord-define-global "pj" 'imalison:project-find-file)
(key-chord-define-global "p[" 'imalison:project-switch-project)
(key-chord-define-global "fj" 'imalison:do-ag)
(key-chord-define-global "jh" 'imalison:avy)))
#+END_SRC
@@ -3831,13 +3890,36 @@ This is useful with server mode when editing gmail messages. I think that it is
(use-package alert
:defer t
:preface
(defun imalison:linux-notifications-available-p ()
(and (eq system-type 'gnu/linux)
(executable-find "notify-send")))
(defun imalison:notify-send-urgency (severity)
(pcase severity
((or 'urgent 'high) "critical")
('low "low")
(_ "normal")))
(defun imalison:alert-notify-send (info)
(when-let ((program (executable-find "notify-send")))
(let ((process-connection-type nil))
(start-process "alert-notify-send" nil program
"-u" (imalison:notify-send-urgency
(plist-get info :severity))
(or (plist-get info :title) "Emacs")
(or (plist-get info :message) "")))))
(defun imalison:windows-toast-notify (info)
(let ((message (plist-get info :message))
(title (plist-get info :title)))
(shell-command (format "windows_toast '%s' '%s'" (or title "No title") (or message "No message")))))
:config
(progn
(setq alert-default-style 'libnotify)
(alert-define-style
'notify-send
:title "notify-send"
:notifier 'imalison:alert-notify-send)
(setq alert-default-style
(if (imalison:linux-notifications-available-p)
'notify-send
'message))
(when (not (string-empty-p (shell-command-to-string "grep -i microsoft /proc/version")))
(alert-define-style
'windows-toast
@@ -4053,6 +4135,9 @@ Ensure all themes that I use are installed:
:commands doom-modeline-mode
:custom
(doom-modeline-height 40))
(defvar imalison:enable-doom-modeline-on-startup t
"Non-nil means enable `doom-modeline-mode' during startup.")
#+end_src
** page-break-lines
#+BEGIN_SRC emacs-lisp
@@ -4203,11 +4288,24 @@ load-theme hook (See the heading below).
(load-theme imalison:dark-theme t))
(apply 'imalison:appearance args)
(message "running appearance")
(doom-modeline-mode +1)
(when (and imalison:enable-doom-modeline-on-startup
(fboundp 'doom-modeline-mode))
(doom-modeline-mode +1))
(setq imalison:default-font-size-pt (face-attribute 'default :height))
(setq imalison:appearance-setup-done t)))
(add-hook 'elpaca-after-init-hook 'imalison:appearance-setup-hook)
(defun imalison:daemon-startup-p ()
(or (daemonp)
(seq-some (lambda (arg)
(or (string= arg "--daemon")
(string= arg "--bg-daemon")
(string-prefix-p "--daemon=" arg)
(string-prefix-p "--bg-daemon=" arg)))
command-line-args)))
(if (imalison:daemon-startup-p)
(add-hook 'server-after-make-frame-hook 'imalison:appearance-setup-hook)
(add-hook 'elpaca-after-init-hook 'imalison:appearance-setup-hook))
#+END_SRC
* Post Init Custom
#+BEGIN_SRC emacs-lisp

View File

@@ -186,5 +186,9 @@
(let ((load-source-file-function nil)) (load autoloads))))
(require 'elpaca)
(setq elpaca-log-functions '(elpaca-log-command-query))
(add-hook 'after-init-hook #'elpaca-process-queues)
(if (daemonp)
(add-hook 'after-init-hook
(lambda ()
(run-with-idle-timer 1 nil #'elpaca-process-queues)))
(add-hook 'after-init-hook #'elpaca-process-queues))
(elpaca `(,@elpaca-order))

View File

@@ -10,6 +10,15 @@
(defun emacs-directory-filepath (filename)
(expand-file-name filename user-emacs-directory))
(add-to-list 'load-path (emacs-directory-filepath "lisp"))
;; Treat this Emacs as if it was built without D-Bus. The local
;; lisp/dbus.el shim prevents `require' from loading the built-in dbus.el,
;; whose top-level form eagerly opens system and session bus connections.
(setq features (delq 'dbusbind features))
(setq dbus-compiled-version nil
dbus-runtime-version nil)
(load-file (expand-file-name "elpaca-installer.el" user-emacs-directory))
;; Elpaca's initial queue logger can fire during self-bootstrap before its
@@ -87,6 +96,14 @@
(setq custom-file "~/.emacs.d/custom-before.el")
(setq load-prefer-newer t)
;; Magit 4.5 and Vertico 2.8 use `set-local', which is native in Emacs 31
;; and provided by recent compat releases. Keep Emacs 30 usable even if
;; package bytecode is stale or compat has not been activated yet.
(unless (fboundp 'set-local)
(defun set-local (variable value)
"Make VARIABLE buffer-local and set it to VALUE."
(set (make-local-variable variable) value)))
;; If this isn't here and there's a problem with init, graphical emacs
;; is super annoying.
(when (equal system-type 'darwin)
@@ -105,10 +122,22 @@
:config
(progn (dash-enable-font-lock)))
;; Emacs 30 ships an older `compat' as a core library. Load Elpaca's newer
;; package explicitly so packages compiled against Compat 31 do not silently
;; see the built-in library.
(elpaca `(compat :host github :repo "emacs-compat/compat" :wait t))
(elpaca-wait)
(let ((compat-build-dir (expand-file-name "compat" elpaca-builds-directory)))
(when (file-directory-p compat-build-dir)
(add-to-list 'load-path compat-build-dir)
(load (expand-file-name "compat" compat-build-dir) nil 'nomessage)))
;; Some split packages fall through the active menus in this config. Give
;; Elpaca an explicit source so startup doesn't get stuck on recipe lookup or
;; stale branch-mapped clones.
(elpaca `(queue :host github :repo "emacs-straight/queue"))
(elpaca `(with-editor :host github :repo "magit/with-editor"
:branch "main"))
(elpaca `(git-commit :host github :repo "magit/magit"
:files ("lisp/git-commit.el" "lisp/git-commit-pkg.el")))
(elpaca `(magit-section :host github :repo "magit/magit"
@@ -185,7 +214,7 @@
(unless (boundp 'overriding-text-conversion-style)
(defvar overriding-text-conversion-style nil))
(use-package transient
:ensure (:host github :repo "magit/transient" :wait t)
:ensure (:host github :repo "magit/transient" :branch "main" :wait t)
:demand t)
(elpaca-wait)

View File

@@ -0,0 +1,93 @@
;;; dbus.el --- Disabled D-Bus shim -*- lexical-binding: t; -*-
;;; Commentary:
;; This file intentionally shadows Emacs' built-in net/dbus.el. Loading the
;; built-in library initializes D-Bus connections at top level, which is noisy
;; and fragile in this configuration.
;;; Code:
(setq features (delq 'dbusbind features))
(setq dbus-compiled-version nil
dbus-runtime-version nil)
(unless (boundp 'dbus-error)
(define-error 'dbus-error "D-Bus disabled in this Emacs config"))
(defvar dbus-debug nil)
(defvar dbus-event-error-functions nil)
(defvar dbus-registered-objects-table (make-hash-table :test #'equal))
(defconst dbus-service-dbus "org.freedesktop.DBus")
(defconst dbus-path-dbus "/org/freedesktop/DBus")
(defconst dbus-path-local "/org/freedesktop/DBus/Local")
(defconst dbus-interface-dbus "org.freedesktop.DBus")
(defconst dbus-interface-peer "org.freedesktop.DBus.Peer")
(defconst dbus-interface-introspectable "org.freedesktop.DBus.Introspectable")
(defconst dbus-interface-properties "org.freedesktop.DBus.Properties")
(defconst dbus-interface-objectmanager "org.freedesktop.DBus.ObjectManager")
(defconst dbus-interface-monitoring "org.freedesktop.DBus.Monitoring")
(defconst dbus-interface-local "org.freedesktop.DBus.Local")
(defconst dbus-service-emacs "org.gnu.Emacs")
(defconst dbus-path-emacs "/org/gnu/Emacs")
(defconst dbus-interface-emacs "org.gnu.Emacs")
(defconst dbus-error-dbus "org.freedesktop.DBus.Error")
(defconst dbus-error-failed "org.freedesktop.DBus.Error.Failed")
(defconst dbus-error-service-unknown "org.freedesktop.DBus.Error.ServiceUnknown")
(defmacro dbus-ignore-errors (&rest body)
"Execute BODY, suppressing `dbus-error' unless `dbus-debug' is non-nil."
(declare (indent 0) (debug t))
`(condition-case err
(progn ,@body)
(dbus-error (when dbus-debug (signal (car err) (cdr err))))))
(defun imalison:dbus-disabled (&rest _args)
"Signal that D-Bus is intentionally unavailable in this config."
(signal 'dbus-error '("D-Bus disabled in this Emacs config")))
(dolist (function
'(dbus-call-method
dbus-call-method-asynchronously
dbus-send-signal
dbus-method-return-internal
dbus-method-error-internal
dbus-register-service
dbus-unregister-service
dbus-register-signal
dbus-register-method
dbus-unregister-object
dbus-register-property
dbus-register-monitor
dbus-init-bus
dbus-ping
dbus-introspect
dbus-introspect-xml
dbus-get-property
dbus-set-property
dbus-get-all-properties
dbus-get-all-managed-objects))
(defalias function #'imalison:dbus-disabled))
(defun dbus-list-activatable-names (&optional _bus) nil)
(defun dbus-list-names (&optional _bus) nil)
(defun dbus-list-known-names (&optional _bus) nil)
(defun dbus-list-queued-owners (&rest _args) nil)
(defun dbus-get-name-owner (&rest _args) nil)
(defun dbus-list-hash-table () nil)
(defun dbus-event-bus-name (_event) nil)
(defun dbus-event-message-type (_event) nil)
(defun dbus-event-serial-number (_event) nil)
(defun dbus-event-service-name (_event) nil)
(defun dbus-event-destination-name (_event) nil)
(defun dbus-event-path-name (_event) nil)
(defun dbus-event-interface-name (_event) nil)
(defun dbus-event-member-name (_event) nil)
(defun dbus-event-handler (_event) nil)
(defun dbus-event-arguments (_event) nil)
(provide 'dbus)
;;; dbus.el ends here

View File

@@ -0,0 +1,41 @@
;;; tramp-gvfs.el --- Disabled TRAMP GVFS backend -*- lexical-binding: t; -*-
;;; Commentary:
;; This file intentionally shadows Emacs' built-in net/tramp-gvfs.el. The real
;; backend depends on D-Bus and GVFS desktop services; this config uses ordinary
;; TRAMP methods instead.
;;; Code:
(require 'tramp)
(defconst tramp-gvfs-enabled nil
"Non-nil when the disabled GVFS backend is available.")
(defvar tramp-gvfs-methods nil
"Disabled list of TRAMP methods handled through GVFS.")
(setq tramp-gvfs-methods nil)
(defconst tramp-goa-methods nil
"Disabled list of GNOME Online Accounts TRAMP methods.")
(defvar tramp-media-methods nil
"Disabled list of media-device TRAMP methods.")
(setq tramp-media-methods nil)
(defvar tramp-gvfs-file-name-handler-alist nil
"Disabled GVFS file name handler alist.")
(defun tramp-gvfs-file-name-p (_filename)
"Return nil because the GVFS backend is disabled."
nil)
(defun tramp-gvfs-file-name-handler (operation &rest args)
"Signal an unsupported OPERATION for the disabled GVFS backend."
(signal 'file-error
(list "TRAMP GVFS backend disabled in this Emacs config"
operation args)))
(provide 'tramp-gvfs)
;;; tramp-gvfs.el ends here

View File

@@ -979,11 +979,8 @@ alphanumeric characters only."
:defer 2
:config
(progn
(use-package org-projectile
:demand t
:config
(setq org-project-capture-default-backend
(make-instance 'org-project-capture-projectile-backend)))
(setq org-project-capture-default-backend
(make-instance 'org-project-capture-project-backend))
(setq org-project-capture-strategy
(make-instance 'org-project-capture-combine-strategies
:strategies (list (make-instance 'org-project-capture-single-file-strategy)
@@ -1117,7 +1114,11 @@ alphanumeric characters only."
(org-wild-notifier--apply-whitelist)
(org-wild-notifier--apply-blacklist)
(-map 'org-wild-notifier--gather-info))))
(org-wild-notifier-mode +1)
(condition-case err
(org-wild-notifier-mode +1)
(error
(message "org-wild-notifier disabled during startup: %s"
(error-message-string err))))
(defun org-wild-notify-check-at-time ()
(interactive)
(imalison:org-at-time

View File

@@ -102,10 +102,10 @@
required = true
[credential "https://github.com"]
helper =
helper = !/run/current-system/sw/bin/gh auth git-credential
helper = !/usr/bin/env gh auth git-credential
[credential "https://gist.github.com"]
helper =
helper = !/run/current-system/sw/bin/gh auth git-credential
helper = !/usr/bin/env gh auth git-credential
[includeIf "gitdir:~/Projects/org-agenda-api/"]
path = ~/.gitconfig.org-agenda-api
[includeIf "gitdir:~/Projects/dotfiles/org-agenda-api/"]

View File

@@ -10,6 +10,17 @@ end)
local config = {
gap = 8,
autoColumns = false,
widgets = {
disk = {
enabled = true,
interval = 60,
volume = "/",
},
memory = {
enabled = true,
interval = 10,
},
},
}
local retileTimer = nil
@@ -219,6 +230,96 @@ local function moveFocusedToScreen(direction)
tileWindows(columnWindows(target))
end
local function userSpacesForScreen(screen)
local spaces, err = hs.spaces.spacesForScreen(screen)
if not spaces then
return nil, err
end
local userSpaces = {}
for _, space in ipairs(spaces) do
if hs.spaces.spaceType(space) == "user" then
table.insert(userSpaces, space)
end
end
return userSpaces
end
local function currentSpaceForScreen(screen)
local activeSpaces = hs.spaces.activeSpaces()
if activeSpaces and screen.getUUID then
local uuid = screen:getUUID()
if uuid and activeSpaces[uuid] then
return activeSpaces[uuid]
end
end
return hs.spaces.focusedSpace()
end
local function nextUserSpaceForScreen(screen, currentSpace)
local spaces, err = userSpacesForScreen(screen)
if not spaces then
return nil, err
end
if #spaces < 2 then
return nil, "no other Desktop on this screen"
end
for index, space in ipairs(spaces) do
if space == currentSpace then
return spaces[(index % #spaces) + 1]
end
end
return spaces[1]
end
local function containsValue(values, target)
if not values then
return false
end
for _, value in ipairs(values) do
if value == target then
return true
end
end
return false
end
local function moveFocusedToNextDesktop()
local focused = hs.window.focusedWindow()
if not focused then
notify("No focused window")
return
end
local screen = focused:screen()
local targetSpace, err = nextUserSpaceForScreen(screen, currentSpaceForScreen(screen))
if not targetSpace then
notify("Desktop move failed: " .. tostring(err))
return
end
local ok, moveErr = hs.spaces.moveWindowToSpace(focused, targetSpace, true)
if not ok then
notify("Desktop move failed: " .. tostring(moveErr))
return
end
hs.timer.doAfter(0.2, function()
if containsValue(hs.spaces.windowSpaces(focused), targetSpace) then
return
end
notify("Desktop move blocked by macOS")
end)
end
local function scheduleRetile()
if arranging or not config.autoColumns then
return
@@ -264,6 +365,7 @@ wf:subscribe({
hs.hotkey.bind(hyper, "c", tileFocusedScreen)
hs.hotkey.bind(hyper, "v", toggleAutoColumns)
hs.hotkey.bind(hyper, "\\", toggleMonitorInput)
hs.hotkey.bind(hyper, "h", moveFocusedToNextDesktop)
hs.hotkey.bind(hyper, "a", function()
focusWindow("left")
@@ -371,6 +473,7 @@ end)
bindRgui("c", tileFocusedScreen)
bindRgui("v", toggleAutoColumns)
bindRgui("\\", toggleMonitorInput)
bindRgui("h", moveFocusedToNextDesktop)
bindRgui("m", function()
placeFocused(1, 1, 1)
@@ -424,7 +527,7 @@ local rguiTap = hs.eventtap.new({
elseif not rightCommandUsed then
hs.eventtap.keyStroke({}, "escape", 0)
end
return false
return true
end
if eventType ~= hs.eventtap.event.types.keyDown or not rightCommandDown then
@@ -433,7 +536,8 @@ local rguiTap = hs.eventtap.new({
local binding = rguiBindings[keyCode]
if not binding then
return false
rightCommandUsed = true
return true
end
rightCommandUsed = true
@@ -448,4 +552,121 @@ end)
rguiTap:start()
local menuWidgets = {}
local widgetTimers = {}
local function round(number)
return math.floor(number + 0.5)
end
local function formatBytes(bytes)
local units = { "B", "K", "M", "G", "T" }
local value = bytes
local unitIndex = 1
while value >= 1024 and unitIndex < #units do
value = value / 1024
unitIndex = unitIndex + 1
end
if unitIndex <= 2 then
return string.format("%d%s", round(value), units[unitIndex])
end
return string.format("%.1f%s", value, units[unitIndex])
end
local function formatGb(bytes)
return string.format("%.1fGB", bytes / 1024 / 1024 / 1024)
end
local function formatCompactGb(bytes, decimals)
return string.format("%." .. decimals .. "f", bytes / 1024 / 1024 / 1024)
end
local function updateDiskWidget()
local widget = menuWidgets.disk
local widgetConfig = config.widgets.disk
if not widget then
return
end
local output, success = hs.execute(string.format(
"/bin/df -k %q | /usr/bin/awk 'NR==2 {print $2, $3, $4, $5}'",
widgetConfig.volume
))
local totalKb, usedKb, availableKb, capacity = output:match("(%d+)%s+(%d+)%s+(%d+)%s+(%d+%%)")
if not success or not totalKb then
widget:setTitle("Disk ?")
widget:setTooltip("Disk usage unavailable")
return
end
widget:setTitle(string.format(
"D %s/%sGB",
formatCompactGb(tonumber(availableKb) * 1024, 0),
formatCompactGb(tonumber(totalKb) * 1024, 0)
))
widget:setTooltip(string.format(
"%s used, %s available on %s (%s full)",
formatBytes(tonumber(usedKb) * 1024),
formatBytes(tonumber(availableKb) * 1024),
widgetConfig.volume,
capacity
))
end
local function updateMemoryWidget()
local widget = menuWidgets.memory
if not widget then
return
end
local stats = hs.host.vmStat()
local pageSize = stats.pageSize
local usedBytes = (
stats.anonymousPages
+ stats.pagesWiredDown
+ stats.pagesUsedByVMCompressor
) * pageSize
local totalBytes = stats.memSize
local availableBytes = totalBytes - usedBytes
local cacheBytes = stats.fileBackedPages * pageSize
local freeBytes = (stats.pagesFree + stats.pagesSpeculative) * pageSize
widget:setTitle(string.format(
"R %s/%sGB",
formatCompactGb(availableBytes, 1),
formatCompactGb(totalBytes, 1)
))
widget:setTooltip(string.format(
"%s used, %s available of %s\n%s cached, %s free",
formatBytes(usedBytes),
formatBytes(availableBytes),
formatBytes(totalBytes),
formatBytes(cacheBytes),
formatBytes(freeBytes)
))
end
local function createMenuWidget(name, update, interval)
menuWidgets[name] = hs.menubar.new()
menuWidgets[name]:setClickCallback(update)
update()
widgetTimers[name] = hs.timer.doEvery(interval, update)
end
local function startMenuWidgets()
if config.widgets.disk.enabled then
createMenuWidget("disk", updateDiskWidget, config.widgets.disk.interval)
end
if config.widgets.memory.enabled then
createMenuWidget("memory", updateMemoryWidget, config.widgets.memory.interval)
end
end
startMenuWidgets()
notify("Hammerspoon loaded")

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
app_id=codex-desktop
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/$app_id"
runtime_dir="${XDG_RUNTIME_DIR:-$state_dir}/$app_id"
pid_file="$state_dir/app.pid"
socket_path="$runtime_dir/launch-action.sock"
pid_is_alive() {
local pid="${1:-}"
case "$pid" in
""|*[!0-9]*) return 1 ;;
esac
[ -e "/proc/$pid/exe" ]
}
running_app_is_alive() {
local pid
[ -r "$pid_file" ] || return 1
pid="$(cat "$pid_file" 2>/dev/null || true)"
pid_is_alive "$pid"
}
send_launch_action() {
[ -S "$socket_path" ] || return 1
running_app_is_alive || return 1
python3 - "$socket_path" "$@" <<'PY'
import json
import socket
import sys
socket_path = sys.argv[1]
argv = sys.argv[2:]
payload = json.dumps({"argv": argv}, separators=(",", ":")).encode("utf-8") + b"\n"
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(1.0)
try:
client.connect(socket_path)
client.sendall(payload)
response = client.recv(16)
finally:
client.close()
if not response.startswith(b"ok"):
raise SystemExit(1)
PY
}
if send_launch_action "$@"; then
exit 0
fi
exec codex-desktop "$@"

View File

@@ -0,0 +1,80 @@
#!/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
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
taffybar)
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" >&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)
set_shell_ui taffybar
;;
apply)
apply_shell_ui "$(current_shell_ui)"
;;
exec-condition)
[[ "$(current_shell_ui)" == "$(normalize_shell_ui "${2:-}")" ]]
;;
*)
echo "usage: desktop_shell_ui {current|set|toggle|apply|exec-condition}" >&2
exit 2
;;
esac

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: generate-wallpaper-crops [WIDTHxHEIGHT] [WALLPAPER_DIR]
Generate centered crop-to-fill PNG wallpapers from WALLPAPER_DIR/originals.
Defaults:
WIDTHxHEIGHT 2560x1600
WALLPAPER_DIR /var/lib/syncthing/sync/Wallpaper
EOF
}
resolution="${1:-2560x1600}"
wallpaper_dir="${2:-/var/lib/syncthing/sync/Wallpaper}"
if [[ "$resolution" == "-h" || "$resolution" == "--help" ]]; then
usage
exit 0
fi
if [[ ! "$resolution" =~ ^([0-9]+)x([0-9]+)$ ]]; then
usage
exit 2
fi
width="${BASH_REMATCH[1]}"
height="${BASH_REMATCH[2]}"
originals_dir="$wallpaper_dir/originals"
output_dir="$wallpaper_dir/$resolution"
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "generate-wallpaper-crops: ffmpeg is required" >&2
exit 1
fi
if [[ ! -d "$originals_dir" ]]; then
echo "generate-wallpaper-crops: missing originals directory: $originals_dir" >&2
exit 1
fi
mkdir -p "$output_dir"
shopt -s nullglob
inputs=(
"$originals_dir"/*.jpg
"$originals_dir"/*.jpeg
"$originals_dir"/*.png
"$originals_dir"/*.webp
)
if [[ "${#inputs[@]}" -eq 0 ]]; then
echo "generate-wallpaper-crops: no source images found in $originals_dir" >&2
exit 1
fi
for input in "${inputs[@]}"; do
name="$(basename "$input")"
stem="${name%.*}"
output="$output_dir/$stem.png"
ffmpeg -hide_banner -loglevel error -y \
-i "$input" \
-vf "scale=${width}:${height}:force_original_aspect_ratio=increase:flags=lanczos,crop=${width}:${height}" \
-frames:v 1 \
-compression_level 9 \
"$output"
echo "$output"
done

View File

@@ -2,27 +2,58 @@
set -euo pipefail
script_path="$(readlink -f "${BASH_SOURCE[0]}")"
state_dir="${XDG_RUNTIME_DIR:-/tmp}/hypr-screensaver"
pid_file="$state_dir/mpvpaper.pid"
neowall_pid_file="$state_dir/neowall.pid"
event_log="$state_dir/events.log"
mkdir -p "$state_dir"
title_prefix="hypr-screensaver:"
backend="${HYPR_SCREENSAVER_BACKEND:-mpvpaper}"
screensaver_dir="${HYPR_SCREENSAVER_DIR:-/var/lib/syncthing/sync/Screensaver}"
screensaver_use_dir="${HYPR_SCREENSAVER_USE_DIR:-$screensaver_dir/use}"
neowall_config="${HYPR_SCREENSAVER_NEOWALL_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/neowall/screensaver.vibe}"
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
Backend selection:
HYPR_SCREENSAVER_BACKEND=mpvpaper
HYPR_SCREENSAVER_BACKEND=neowall
The neowall backend is a wallpaper-layer experiment. It does not cover existing
windows like mpvpaper's overlay layer.
EOF
}
@@ -30,19 +61,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))
@@ -51,107 +74,255 @@ 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
if [ "$backend" = "neowall" ]; then
neowall_is_running
return
fi
shopt -s nullglob
local pid_file
for pid_file in "$state_dir"/*.pid; do
local pid
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
}
neowall_is_running() {
if [ -f "$neowall_pid_file" ]; then
local pid
pid="$(<"$neowall_pid_file")"
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
rm -f "$neowall_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"
}
start() {
local current_monitor spec monitor width height pid
random_source() {
[ -d "$screensaver_use_dir" ] || return 1
if is_running; then
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 "$@"
}
run_neowall() {
if command -v neowall >/dev/null 2>&1; then
neowall "$@"
return
fi
nix shell nixpkgs#neowall --command neowall "$@"
}
start_neowall() {
local pid
if neowall_is_running; then
log_event "neowall start ignored: already running pid=$(<"$neowall_pid_file")"
exit 0
fi
current_monitor="$(focused_monitor || true)"
if [ ! -r "$neowall_config" ]; then
printf 'NeoWall screensaver config not found: %s\n' "$neowall_config" >&2
return 1
fi
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)
stop
systemctl --user stop hyprpaper.service >/dev/null 2>&1 || true
run_neowall -c "$neowall_config" >>"$state_dir/neowall.log" 2>&1
if [ -n "$current_monitor" ]; then
hyprctl dispatch focusmonitor "$current_monitor" >/dev/null 2>&1 || true
sleep 0.2
pid="$(pgrep -n -x neowall || true)"
if [ -z "$pid" ]; then
log_event "neowall start failed: no neowall process found"
return 1
fi
printf '%s\n' "$pid" > "$neowall_pid_file"
log_event "neowall start ok pid=$pid config=$neowall_config"
}
start_mpvpaper() {
local source output layer options pid
if is_running; then
log_event "start ignored: already running pid=$(<"$pid_file")"
exit 0
fi
stop
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"
}
start() {
case "$backend" in
mpvpaper)
start_mpvpaper
;;
neowall)
start_neowall
;;
*)
printf 'Invalid HYPR_SCREENSAVER_BACKEND=%s; expected mpvpaper or neowall\n' "$backend" >&2
exit 2
;;
esac
}
stop_mpvpaper() {
local pid legacy_pid
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"
else
log_event "stop with no pid file"
fi
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
}
stop_neowall() {
if [ -f "$neowall_pid_file" ]; then
log_event "neowall stop pid=$(<"$neowall_pid_file")"
run_neowall kill >/dev/null 2>&1 || pkill -TERM -x neowall >/dev/null 2>&1 || true
rm -f "$neowall_pid_file"
systemctl --user start hyprpaper.service >/dev/null 2>&1 || true
fi
}
stop() {
local pid pid_file
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
pid="$(<"$pid_file")"
kill "$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[@]}"
fi
exec nix shell nixpkgs#mpv --command mpv "${mpv_args[@]}"
stop_mpvpaper
stop_neowall
}
status() {
@@ -176,7 +347,7 @@ case "${1:-}" in
status
;;
session)
session
start
;;
""|-h|--help|help)
usage

Some files were not shown because too many files have changed in this diff Show More