Compare commits
648 Commits
hyprland-l
...
83abdaf69d
| Author | SHA1 | Date | |
|---|---|---|---|
| 83abdaf69d | |||
| 4827c2150f | |||
| 742d756973 | |||
| d56f728fdd | |||
| 2de5618ea0 | |||
| 088c483e97 | |||
| 68a2716e53 | |||
| 22dd235378 | |||
| 87d689a183 | |||
| 48483f9c5f | |||
| ab7b467a5a | |||
| 674b413b4b | |||
| ef81c601b0 | |||
| c09892f842 | |||
| 26b84a28a4 | |||
| fdabad92ce | |||
| 4e4922b018 | |||
| 00eddc5aff | |||
| 91f4b477c3 | |||
| 6ab4308547 | |||
| 5ef237e965 | |||
| 1070efe5f1 | |||
| 8d95666799 | |||
| dc240292b1 | |||
| f3e15518d7 | |||
| 7cdb6dbd56 | |||
| e5e8e60c92 | |||
| ea370c12c7 | |||
| 6be8c5f1b5 | |||
| 5044956429 | |||
| 1a063c3bff | |||
| 28aa0b99e2 | |||
| 591e87364d | |||
| 0b0540a9c0 | |||
| 534196af94 | |||
| 5b31007da2 | |||
| 0f29f493e8 | |||
| 9c9977a12e | |||
| 3268bffcab | |||
| 4eb97e4361 | |||
| 0592807638 | |||
| 459256a685 | |||
| 95943fca97 | |||
| 3b714d1795 | |||
| a9863e394e | |||
| b823ada28f | |||
| fe01e0cdc2 | |||
| 97c9c1fe39 | |||
| dd18277bc2 | |||
| 4abbd0be82 | |||
| 6450ab6143 | |||
| d346925219 | |||
| 114d340987 | |||
| 139775f001 | |||
| d83ff3cd51 | |||
| f25f878e6d | |||
| 9f64f7e063 | |||
| 9f8e51e0e2 | |||
| bec9eb5b2c | |||
| bf0d71d7e5 | |||
| 1c40d35fa6 | |||
| 607cdb6fa3 | |||
| 5c7007912c | |||
| 74d5615f8c | |||
| bfbd797b85 | |||
| acd667c419 | |||
| 73209203a4 | |||
| 8bcdd8b146 | |||
| ee7f18fc43 | |||
| f24a45e321 | |||
| 1c1ca5367d | |||
| 1d352685ba | |||
| 802af00c2e | |||
| 3d9d2a5a4b | |||
| 1b08753d6f | |||
| b511a39c4d | |||
| fb89252949 | |||
| 043fc9ec34 | |||
| 861c024c24 | |||
| cb75208f9e | |||
| 6bd0ccc6d8 | |||
| 01de409d18 | |||
| dc3de9402c | |||
| 8c1ed2d15c | |||
| 4670a9f9aa | |||
| 169391db84 | |||
| ea5e3e4bf2 | |||
| e8c7eb4522 | |||
| 524fcb5310 | |||
| 6277cd8ace | |||
| 00708d4eac | |||
| b22f112a39 | |||
| 78e805d952 | |||
| fdc707174d | |||
| f12034d3c3 | |||
| 3655828fcb | |||
| c4df484cce | |||
| d5a834e1dc | |||
| 8fbebc2a71 | |||
| f9bdc44e34 | |||
| 193e0aed70 | |||
| 74973b231a | |||
| 9eb3dcb8a1 | |||
| 3029f877ec | |||
| 902472e915 | |||
| 621a994a0d | |||
| 8c91e08e7c | |||
| e365db1c64 | |||
| 2567d2828f | |||
| da03de6d1c | |||
| 9e8f0ef8cd | |||
| 30a58fb05f | |||
| 971cece3b4 | |||
| 77bae03b14 | |||
| 686023e006 | |||
| f12285b7f7 | |||
| 0efb55f23f | |||
| 30fb74b13b | |||
| 9bb090bb35 | |||
| 9d09b33e3e | |||
| 8a00f33c75 | |||
| 7b9031d85f | |||
| 6216b54bfc | |||
| 01895cf518 | |||
| ae67cd4ca3 | |||
| b41a1d8e36 | |||
| 9bdc56c207 | |||
| 428ee71396 | |||
| d936b477cd | |||
| d552b3f89f | |||
| f755db41f5 | |||
| 3144fab895 | |||
| 2a3243b240 | |||
| 1869d3af8d | |||
| fe733a9eb4 | |||
| 7058c68e56 | |||
| 68be1cdd09 | |||
| da7a946d31 | |||
| b0df8ef27f | |||
| 4681f49d81 | |||
| 3ee11bcc14 | |||
| 2cfdb47469 | |||
| e0905eb651 | |||
| 5b4f605145 | |||
| 0336aa6e53 | |||
| ad567c3e3f | |||
| 3cb49b51bc | |||
| bd6e8d4e30 | |||
| 7713ad07bd | |||
| a5e3e650ae | |||
| 53becf8cc7 | |||
| 4e98850276 | |||
| 8054fce6a3 | |||
| 563208d03b | |||
| d29c361a9e | |||
| cd18fc2056 | |||
| 39b68274e3 | |||
| a2609b6f5b | |||
| 1955bbed68 | |||
| 942b987cc7 | |||
| fc280676f5 | |||
| dce3666521 | |||
| f3649945cb | |||
| 994291b969 | |||
| ec00711c85 | |||
| 16aa6ed735 | |||
| f36cbe0207 | |||
| 1c0377f3ad | |||
| 6290679406 | |||
| 18ec8a7809 | |||
| d21177b29c | |||
| 395f580645 | |||
| a957e78e25 | |||
| a4f648fef8 | |||
| 5eaaa39527 | |||
| 1ec67b7892 | |||
| c75cf6c29c | |||
| 5aa543209d | |||
| 7276109962 | |||
| 87422afae3 | |||
| ab49f6c079 | |||
| e6e0cd6d5e | |||
| 59fc652eab | |||
| 83ab75a12c | |||
| 85118f187e | |||
| 1da7188781 | |||
| bdd95370bd | |||
| 48369966b4 | |||
| 6bdfcb0c8d | |||
| f94572bda0 | |||
| 267bd10095 | |||
| 897c97c269 | |||
| d0f500daa8 | |||
| e740a657ab | |||
| 1f2a38a8f7 | |||
| 3da55b59d2 | |||
| 6ed6663b72 | |||
| 1d22e827b2 | |||
| 11cd44b3f5 | |||
| 8afbbce109 | |||
| 415b65d0ee | |||
| 66bbdab675 | |||
| 8ed33fc7e8 | |||
| ef3d19f1a4 | |||
| 7e07e768da | |||
| c368f98e9f | |||
| aee236e532 | |||
| acea28cc54 | |||
| 272e71a37c | |||
| 8c61bc4cee | |||
| ce7fd6b7a0 | |||
| 6d0c29a743 | |||
| 38a696cff2 | |||
| 4cc6bee526 | |||
| 1c9e470ff6 | |||
| 495a5cbca2 | |||
| 964ed7584e | |||
| be1ec8556c | |||
| b2942e2a07 | |||
| 34fd17a6a2 | |||
| 4a245306ed | |||
| 30a0ae47a8 | |||
| 2b3a600b1b | |||
| f6ab902015 | |||
| 7069d0af10 | |||
| b1a52b0401 | |||
| d260b7622c | |||
| 06eed9281d | |||
| 38d57d1c0e | |||
| 3e14320f36 | |||
| 6018cc6f1d | |||
| d90a6c7c63 | |||
| 507a306cbf | |||
| 44363cf0fb | |||
| 1b06280cb9 | |||
| 7c1185fa6e | |||
| 4188a6c0d8 | |||
| e6a5464520 | |||
| 58a55209fa | |||
| 9baa4c3d44 | |||
| 422826a62e | |||
| 32e69cbd01 | |||
| 59c7d4ba11 | |||
| 531ad1602b | |||
| 67859a5436 | |||
| b8cbf387fa | |||
| 29a0af4bde | |||
| 03536fbbb1 | |||
| 511d643063 | |||
| 81d4496fe4 | |||
| d6eb0f2e6c | |||
| 05bce81158 | |||
| 51de81b242 | |||
| 758a35c836 | |||
| 187edef30f | |||
| 3338b86b48 | |||
| 63f9ead9a3 | |||
| 4852985801 | |||
| e8612e3df0 | |||
| 798d5c0742 | |||
| 60fa81fecf | |||
| 78842c242d | |||
| e0b3eb9da8 | |||
| 2acbd0937f | |||
| fbec3e7380 | |||
| e4d4547bf1 | |||
| a81d1d2caf | |||
| 0105b52b7a | |||
| 715eb1e76d | |||
| 8360419d2c | |||
| 2bacf623cb | |||
| 443dfb0199 | |||
| 07cdc10ef0 | |||
| 850cddeeb0 | |||
| eec9f0ba0e | |||
| ff23cb8da6 | |||
| 79343c8160 | |||
| 1df5c22b75 | |||
| 81318ce0be | |||
| 680f8b4a91 | |||
| b7eb47a71d | |||
| a860b59e3f | |||
| dde547f694 | |||
| bf71d0ee39 | |||
| 2c22ccd01e | |||
| 348560eefe | |||
| 2573928706 | |||
| 18c293ec5f | |||
| 2e523750e2 | |||
| 72c9177f95 | |||
| 4360850f82 | |||
| d99ccdbd0c | |||
| a37780c443 | |||
| 2c53dda524 | |||
| c39c70f6ac | |||
| e407f009db | |||
| eee7434aca | |||
| 54ec7d3f0a | |||
| 0ee6c78de3 | |||
| c11f81cbf8 | |||
| deef8b8a07 | |||
| d57fda3dc9 | |||
| a8dab69126 | |||
| 2fb8951810 | |||
| b74bb07339 | |||
| 87a79e2c8a | |||
| 2d96b71594 | |||
| aed4d24ae7 | |||
| f3a10e0b66 | |||
| ddb854c362 | |||
| 8ae6f0d676 | |||
| 0c75df5085 | |||
| 1c0de36f52 | |||
| 874b83259d | |||
| 378fa8df34 | |||
| 243f64fade | |||
| 96b9b5cd85 | |||
| f2bb9c8278 | |||
| c121e07452 | |||
| 6c2183c9ae | |||
| b72dab7337 | |||
| 35191bedba | |||
| ba06ab1e00 | |||
| 6e21640e58 | |||
| beef3f8b84 | |||
| 875982b6c2 | |||
| 463cb9d24a | |||
| 8c31b53e33 | |||
| eda407c47d | |||
| ce29ee063d | |||
| d3c49ce7ed | |||
| ddde85ab3f | |||
| 9c1d280c92 | |||
| 937b49c11a | |||
| 3269e803fd | |||
| 9eef758ba2 | |||
| 6b76df2f27 | |||
| 1642626d2a | |||
| 0a118d0673 | |||
| 423d5cd35b | |||
| 5fddfb64d9 | |||
| 3371aa6781 | |||
| 911f02bb55 | |||
| 39f2f4037d | |||
| cb35d31104 | |||
| 94826c2275 | |||
| ca6ae7c34f | |||
| dbc7ec267c | |||
| 199a2e1aab | |||
| 007d6ea4de | |||
| 24c1a0a4d4 | |||
| 890bdb0925 | |||
| 83b4889982 | |||
| 5cac4d4fc3 | |||
| 9d6ef77676 | |||
| f60abcb876 | |||
| 22f6fa1b69 | |||
| c5627de004 | |||
| 445f6bb2d7 | |||
| 79f24aa0ae | |||
| e9c95cfc45 | |||
| e203230c4d | |||
| 038f0c1896 | |||
| 442710bc69 | |||
| 724fb61054 | |||
| 8250cfdbc9 | |||
| ac2295b017 | |||
| a44b21d681 | |||
| 1d701304fe | |||
| 07d382fc03 | |||
| 74bd7e76da | |||
| 2ddeb42416 | |||
| 0ff3100904 | |||
| f781e4a406 | |||
| f94209b7f5 | |||
| 72d0960fb1 | |||
| f4dcefe392 | |||
| 327a1768ab | |||
| 814ea1283e | |||
| bcc61aa6fa | |||
| 8ae7a2e0e4 | |||
| 996d02cc60 | |||
| bd85161f7a | |||
| cade9b9628 | |||
| 4055dfe0b9 | |||
| 6427e89ee4 | |||
| df6dcc2153 | |||
| dbb8f6addf | |||
| 96d456edc2 | |||
| ef84d1a270 | |||
| 9aea5407db | |||
| d91ca93750 | |||
| 098ccbf72b | |||
| 5d414403d8 | |||
| 35bee5750f | |||
| 1742467799 | |||
| bdc42f1ab1 | |||
| 7e9502cbf2 | |||
| 43db4b8f1b | |||
| 191a83bb7b | |||
| 7946892f7f | |||
| 5c80b986ed | |||
| 842f161416 | |||
| 92d8472bd2 | |||
| f5a88df96b | |||
| c2ca860a99 | |||
| a6dee77f58 | |||
| 3e0b8873e5 | |||
| ddaa3a78ac | |||
| 973b67f185 | |||
| 33066b3abf | |||
| 1baf114689 | |||
| aed1f43818 | |||
| ba07ad9747 | |||
| 0f00f7d33f | |||
| 463c842d4f | |||
| 4c669b60f9 | |||
| 65243e8a7e | |||
| a121100271 | |||
| 45c85fae55 | |||
| 23e4cd033a | |||
| de44814a00 | |||
| 21d8d75d86 | |||
| f6386afb49 | |||
| 2a036581c7 | |||
| e7486cb2c4 | |||
| 57cccedcf9 | |||
| cef847f117 | |||
| 1ee2625490 | |||
| fdaaf130f2 | |||
| c12b9c05db | |||
| 82b4dff20a | |||
| d74fa81e10 | |||
| 08eeeb0ad7 | |||
| 2d2d1f3ca8 | |||
| ab22bd551d | |||
| f56cb6ac42 | |||
| 1c5dc8a0c7 | |||
| e07d738857 | |||
| dbd58a9488 | |||
| dd71d880f6 | |||
| 17f9f85073 | |||
| 46e3e7db59 | |||
| 4a57e6f936 | |||
| ad4b8c267e | |||
| d44736aec9 | |||
| 2d92e9d55d | |||
| b8e6abd628 | |||
| 787f312cbe | |||
| 968abf1a05 | |||
| 9a28a63ba3 | |||
| ee35eb2af0 | |||
| 65297d652e | |||
| d28ec5cdd4 | |||
| 5e67c1c795 | |||
| db56ef8aa1 | |||
| 21868cca81 | |||
| 8c1687fa83 | |||
| e5678819f9 | |||
| 10d26e9968 | |||
| 2cf561bf78 | |||
| a51fb925ed | |||
| f602cdbe95 | |||
| 06b0790647 | |||
| bb54a004ae | |||
| cf6533ac2f | |||
| c33fcca67b | |||
| 86b8891084 | |||
| 500a51b0fa | |||
| 598abae7b3 | |||
| fd8f4a222a | |||
| 7a98dd1bcf | |||
| 42e8e6db6f | |||
| 5ba22bb56a | |||
| a56d93d4b1 | |||
| 0fbb831462 | |||
| b38c7867c2 | |||
| dce81586ac | |||
| e1fd076982 | |||
| d04c6b4cd5 | |||
| 291e497d63 | |||
| 1ae061da47 | |||
| 54c86b2366 | |||
| 1ffaa8c5ee | |||
| 58ad1bc679 | |||
| a58b8fb6aa | |||
| 0ab53ed0fb | |||
| fb3af2543a | |||
| 13c465efef | |||
| e3474040b2 | |||
| 7ef9b4be0d | |||
| f6b2a1ae8c | |||
| 34793d7075 | |||
| aaf2ebd569 | |||
| 544da689ab | |||
| e28cbee448 | |||
| 32cb3944cc | |||
| 837ba834ba | |||
| def5b968e2 | |||
| fa28f4c433 | |||
| 8f2bb38d23 | |||
| fc293e079a | |||
| d3912fc060 | |||
| 3ced6dc45c | |||
| e0865300ef | |||
| 9cb7da28e4 | |||
| 1817c73609 | |||
| a59c316d85 | |||
| 63fcebf392 | |||
| c53405bcf7 | |||
| eb95ee9faa | |||
| 8dac748f56 | |||
| dc95fe6561 | |||
| 108b491f6a | |||
| 8d7947a773 | |||
| 103cdeaa9f | |||
| 6e89a3fcb5 | |||
| 4bec7af523 | |||
| 168435d3e7 | |||
| 91f539547c | |||
| e4cccc54a4 | |||
| 2d69c143b1 | |||
| 16fa31887a | |||
| cfe0ca59bf | |||
| 200504318b | |||
| 52aa541aee | |||
| 211aa60b73 | |||
| c6536b76cd | |||
| a573176200 | |||
| 5f5b43839b | |||
| 9a0612e608 | |||
| 4b552afb7a | |||
| 43a718536a | |||
| 77a03e2dc6 | |||
| 4ea7a163e7 | |||
| 576605e3cf | |||
| c0278f9411 | |||
| 06ea3eec29 | |||
| a4374a99ec | |||
| 829a0846a1 | |||
| df36fe2d12 | |||
| 07fb87ddbb | |||
| 5cdea7dd1a | |||
| 6cb3415987 | |||
| 364f9fdc6a | |||
| 8ee4a242ab | |||
| ba435c5119 | |||
| ee1a6b8904 | |||
| ca5b2b566f | |||
| 67589779df | |||
| 9a5a9ec5da | |||
| d652f80d05 | |||
| b4a7096ac9 | |||
| 117b836227 | |||
| 11ae2f489c | |||
| 6801a90e32 | |||
| d9058deb4b | |||
| 815601568f | |||
| 600be3e2b7 | |||
| 5664aa7aae | |||
| 52febc5943 | |||
| 281ec0347e | |||
| 54384995d4 | |||
| a06919778d | |||
| 7384d7f17c | |||
| 95739ca7b4 | |||
| 660b1fa8f3 | |||
| 0c0dc2d318 | |||
| e7b8ff2fc4 | |||
| ee3afe7fdd | |||
| beeb505cdd | |||
| d12cbe0b79 | |||
| 0e5d635132 | |||
| 439b95a593 | |||
| 0750934622 | |||
| b153adcb8c | |||
| 7d7daeb91f | |||
| 950a7994d6 | |||
| a0a71f5d2d | |||
| ad23acab4e | |||
| 0aba31c21f | |||
| 4cccd9db2d | |||
| 5637db1182 | |||
| 0a0024d009 | |||
| 030a67364e | |||
| 716405b1ef | |||
| 854b55086c | |||
| ae4a398e77 | |||
| 8d346bc37e | |||
| 90bd377335 | |||
| 9008190a90 | |||
| b16695b574 | |||
| 937174080c | |||
| 9b970c6458 | |||
| 1bc58095b5 | |||
| d9ea8b0e1f | |||
| 385b28d6a4 | |||
| 24ebab874f | |||
| 0857e4b6da | |||
| ea75c960e8 | |||
| 8936112348 | |||
| 7ac4e091c2 | |||
| 29eefa99e3 | |||
| b3d77bb310 | |||
| d1061a75ad | |||
| 59f5d22b09 | |||
| 98b8c6fbd2 | |||
| 231b22d8ae | |||
| 1d85ed76d6 | |||
| b65010283c | |||
| 022580f1af | |||
| f6026b5cac | |||
| 34906469b9 | |||
| 3cb0301f9a | |||
| 6f489d14ab | |||
| acae19d9c5 | |||
| c30a67facf | |||
| d48edc9bb8 | |||
| af570360d3 | |||
| 34fd60e8f2 | |||
| f826c6ae75 | |||
| 1a2b75adcb | |||
| 4e52e81a50 | |||
| df0b7b6db4 | |||
| a7769545f1 | |||
| bb32668387 | |||
| 8ccf5fb7de | |||
| 52861430da | |||
| d9ebb812c5 | |||
| 5cf2eda008 | |||
| 6299ad2c7d | |||
| 672cc14713 | |||
| 64c45e1060 | |||
| a5413331d9 | |||
| 1044565bf7 | |||
| d684f6fbc5 | |||
| 71deb64ed0 | |||
| bb909849bd | |||
| a37e83fb23 | |||
| 53d8a69a31 | |||
| 87fd1681e2 | |||
| 8933f8e545 | |||
| ed90130233 | |||
| aa1fbf9699 | |||
| 3e05939ce3 | |||
| 8e2128b8d4 | |||
| 1696845579 | |||
| 5522b8bacd |
@@ -4,7 +4,10 @@
|
||||
"Bash(rg:*)",
|
||||
"Bash(wmctrl:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(hyprctl:*)"
|
||||
"Bash(hyprctl:*)",
|
||||
"Bash(set_multiplexer_title 'dotfiles - Claude desktop icon fix')",
|
||||
"Bash(nix eval *)",
|
||||
"Read(//nix/store/xmgdj0242sc04hybgd3x6w0a7cw7kkwl-system-path/share/applications/**)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
32
.github/workflows/cachix.yml
vendored
32
.github/workflows/cachix.yml
vendored
@@ -1,22 +1,28 @@
|
||||
name: Build and Push Cachix (NixOS)
|
||||
name: Build and Push Cachix
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "dotfiles/config/taffybar/**"
|
||||
- "dotfiles/config/hypr/**"
|
||||
- "dotfiles/lib/bin/hypr_*"
|
||||
- "dotfiles/lib/bin/hypr*"
|
||||
- "nixos/**"
|
||||
- "org-agenda-api/**"
|
||||
- ".github/workflows/cachix.yml"
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "dotfiles/config/taffybar/**"
|
||||
- "dotfiles/config/hypr/**"
|
||||
- "dotfiles/lib/bin/hypr_*"
|
||||
- "dotfiles/lib/bin/hypr*"
|
||||
- "nixos/**"
|
||||
- "org-agenda-api/**"
|
||||
- ".github/workflows/cachix.yml"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
nixos-strixi-minaj:
|
||||
imalison-taffybar:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
@@ -36,6 +42,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
@@ -51,9 +58,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 +89,19 @@ 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
|
||||
|
||||
- name: Build Hyprland stuff
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
nix build \
|
||||
--no-link \
|
||||
--print-build-logs \
|
||||
./nixos#hyprland-stuff \
|
||||
--override-input railbird-secrets path:./nixos/ci/railbird-secrets-stub
|
||||
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -49,5 +49,33 @@ gotools
|
||||
|
||||
# Local tool state
|
||||
/.playwright-cli/
|
||||
/nixos/.playwright-cli/
|
||||
/nixos/action-cache-dir/
|
||||
/dotfiles/config/taffybar/dbus-menu/
|
||||
|
||||
# On nix-darwin, ~/.claude resolves into dotfiles/claude (HM out-of-store
|
||||
# symlink), so the claude-history repo and live Claude Code state are nested
|
||||
# inside this worktree there. Keep everything but the managed config out of
|
||||
# the dotfiles repo so chat history can never be committed here.
|
||||
/dotfiles/claude/*
|
||||
!/dotfiles/claude/CLAUDE.md
|
||||
!/dotfiles/claude/settings.json
|
||||
!/dotfiles/claude/settings.local.json
|
||||
!/dotfiles/claude/settings.local.json.example
|
||||
!/dotfiles/claude/agents/
|
||||
!/dotfiles/claude/agents/codex-delegator.md
|
||||
# Expose the shared agent skills library to Claude Code, which only reads
|
||||
# ~/.claude/skills. This is a symlink to ../agents/skills (the canonical
|
||||
# store, also surfaced at ~/.agents/skills); without the allowlist the
|
||||
# /dotfiles/claude/* rule above keeps it out of the flake source.
|
||||
!/dotfiles/claude/skills
|
||||
|
||||
# Same story for Codex: ~/.codex resolves into dotfiles/codex on nix-darwin,
|
||||
# so the codex-history repo and live Codex state nest inside this worktree.
|
||||
# Allowlist only HM-managed configuration and custom agents.
|
||||
/dotfiles/codex/*
|
||||
!/dotfiles/codex/AGENTS.md
|
||||
!/dotfiles/codex/agents/
|
||||
!/dotfiles/codex/agents/claude-delegator.toml
|
||||
!/dotfiles/codex/config.toml
|
||||
!/dotfiles/codex/skills
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dotfiles/emacs.d/README.org
|
||||
230
README.org
Normal file
230
README.org
Normal file
@@ -0,0 +1,230 @@
|
||||
# -*- 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 shared checkout
|
||||
path =/srv/dotfiles= on NixOS machines. 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 /etc/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 /etc/nixos
|
||||
just switch-remote
|
||||
just switch-local-taffybar
|
||||
just remote-switch <host>
|
||||
#+end_src
|
||||
|
||||
Build/check examples:
|
||||
|
||||
#+begin_src sh
|
||||
nix flake check /etc/nixos
|
||||
nix build /etc/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 /srv/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 /etc/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 /tmp/dotfiles
|
||||
cd /tmp/dotfiles
|
||||
just setup-shared-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.
|
||||
@@ -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.
|
||||
@@ -1,218 +0,0 @@
|
||||
# Hyprland Lua Migration Checklist
|
||||
|
||||
This checklist tracks the migration described in `docs/tiling-wm-experience.md`.
|
||||
|
||||
Guiding rule for shelling out:
|
||||
|
||||
- Prefer Lua for compositor/window/workspace state changes.
|
||||
- Avoid `hyprctl` for window manipulation unless there is no usable Lua API.
|
||||
- `hyprctl` remains acceptable for non-window-control escape hatches such as
|
||||
`hyprctl reload`.
|
||||
- External utilities remain acceptable where they are the real tool being
|
||||
launched, for example rofi, cliphist, grim/slurp/swappy, playerctl, hyprlock,
|
||||
and systemd commands.
|
||||
|
||||
## 0. Version And Build Base
|
||||
|
||||
- [x] Update/confirm Hyprland Lua input at latest usable upstream target.
|
||||
- [x] Keep stable Hyprland path intact until Lua path is proven.
|
||||
- [x] Keep hy3 out of the Lua branch.
|
||||
- [x] Keep hyprNStack following the Lua Hyprland input.
|
||||
- [x] Rebuild hyprNStack against the Lua Hyprland branch.
|
||||
- [x] Add a forked hyprexpo input for the Lua Hyprland branch.
|
||||
- [x] Keep a cheap Lua check: parse config, execute against stub, reject
|
||||
`hyprctl` in the Lua config's window/workspace manipulation path.
|
||||
- [x] Add a real Hyprland Lua verifier check for the config parser path.
|
||||
|
||||
Current upstream note: latest Hyprland release observed during this migration is
|
||||
`v0.54.3`; the Lua config input tracks PR 13817 and was already at the current
|
||||
PR head `c35a8a5` dated 2026-04-26. The non-Lua fallback remains pinned to the older
|
||||
hy3/hyprexpo-compatible stack; the Lua branch uses forked hyprexpo branch
|
||||
`colonelpanic8/hyprland-plugins:hyprexpo-lua-hyprland`.
|
||||
|
||||
## 1. Core Layout
|
||||
|
||||
- [x] Primary layout is equal-width columns.
|
||||
- [x] No scrolling layout.
|
||||
- [x] No hy3 in Lua path.
|
||||
- [x] Dynamic redistribution on open/close via Lua-managed nStack count.
|
||||
- [x] Monocle/tabbed-style layout available.
|
||||
- [x] Direct jump to columns layout.
|
||||
- [x] Direct jump to monocle layout.
|
||||
- [x] Directional focus cycles in monocle.
|
||||
- [x] Visual indication of hidden monocle windows, currently notification.
|
||||
- [x] Make layout state per workspace instead of one global current layout.
|
||||
- [x] Preserve one-window smart gaps in the live config path.
|
||||
- [x] Use a persistent monocle indicator instead of a transient notification.
|
||||
|
||||
Smart-gaps note: nStack uses `no_gaps_when_only = true`; Hyprland workspace
|
||||
rules are still applied at runtime for broader parity, but skipped during
|
||||
`--verify-config` because the current Lua PR segfaults when rule bindings run in
|
||||
verifier mode.
|
||||
|
||||
## 2. Workspace Behavior
|
||||
|
||||
- [x] `Super+1..9` focuses bounded workspaces.
|
||||
- [x] `Super+Shift+1..9` sends window without following.
|
||||
- [x] `Super+Ctrl+1..9` sends and follows.
|
||||
- [x] Previous workspace per monitor uses Lua-tracked history.
|
||||
- [x] Implement next empty workspace focus in Lua.
|
||||
- [x] Implement move focused window to next empty workspace without following.
|
||||
- [x] Implement move focused window to next empty workspace and follow.
|
||||
- [x] Implement bounded workspace cycling `1..9` in Lua, replacing
|
||||
`workspace-scroll.sh`.
|
||||
- [x] Implement workspace swap or decide whether native dispatcher is enough.
|
||||
- [x] Track current monitor workspace history explicitly, with native
|
||||
`previous_per_monitor` as fallback.
|
||||
|
||||
## 3. Directional Navigation
|
||||
|
||||
- [x] `Super+w/a/s/d` focuses windows.
|
||||
- [x] `Super+Shift+w/a/s/d` swaps windows.
|
||||
- [x] `Hyper+w/a/s/d` focuses monitors.
|
||||
- [x] `Hyper+Shift+w/a/s/d` moves windows to monitors.
|
||||
- [x] `Super+z` next monitor.
|
||||
- [x] `Super+Shift+z` move to next monitor.
|
||||
- [x] Replace any old cursor-follow/move scripts fully.
|
||||
- [x] Add required `Super+Ctrl+w/a/s/d` move-to-monitor behavior preserving
|
||||
useful focus.
|
||||
- [x] Add "move to empty workspace on monitor in direction" without requiring
|
||||
`Hyper+Ctrl`.
|
||||
- [x] Route directional focus in monocle through deterministic Lua cycling.
|
||||
- [ ] Live-verify directional focus in monocle behaves predictably.
|
||||
|
||||
## 4. Script Elimination Priority
|
||||
|
||||
- [x] Core layout switching no longer uses scripts.
|
||||
- [x] Core column count logic no longer uses scripts or `hyprctl`.
|
||||
- [x] Replace `find-empty-workspace.sh`.
|
||||
- [x] Replace `workspace-goto-empty.sh`.
|
||||
- [x] Replace `workspace-move-to-empty.sh`.
|
||||
- [x] Replace `workspace-scroll.sh`.
|
||||
- [x] Replace `cycle-layout.sh`.
|
||||
- [x] Replace `movewindow-follow-cursor.sh`.
|
||||
- [x] Replace `gather-class.sh`.
|
||||
- [x] Replace `focus-next-class.sh`.
|
||||
- [x] Replace `raise-or-run.sh`.
|
||||
- [x] Replace minimize scripts if Lua can maintain hidden workspace state.
|
||||
- [x] Replace `swap-workspaces.sh`.
|
||||
- [x] Decide whether rofi-backed pickers remain scripts or become
|
||||
Lua-generated command pipes. Rofi itself remains external.
|
||||
|
||||
## 5. Overview And Window Discovery
|
||||
|
||||
- [x] Restore visual hyprexpo for `Super+Tab` overview.
|
||||
- [x] Restore visual hyprexpo `bring` mode for `Super+Shift+Tab`.
|
||||
- [x] Keep first-pass Lua numbered window picker on secondary bindings.
|
||||
- [x] Implement first-pass Lua-native go-to-window picker.
|
||||
- [x] Implement first-pass Lua-native bring-window picker.
|
||||
- [x] Implement first-pass Lua-native replace-window picker.
|
||||
- [ ] Picker entries include icons.
|
||||
- [x] Picker entries include title/workspace.
|
||||
- [x] Hide scratchpad/minimized/internal windows from normal pickers.
|
||||
- [x] Decide whether picker data generation can be Lua-native with rofi as only
|
||||
external process.
|
||||
|
||||
Picker decision: current Lua API can query and manipulate windows directly, but
|
||||
does not expose a synchronous way to run rofi and consume its selected output.
|
||||
The first pass therefore uses Lua-native numbered submaps and notifications.
|
||||
A final rofi/icon picker would need either a small IPC bridge or an upstream Lua
|
||||
process-output/callback primitive.
|
||||
|
||||
Hyprexpo decision: hyprexpo is kept as the visual overview. The forked Lua
|
||||
branch exposes `hl.plugin.hyprexpo.expo(...)`, so the Lua config can invoke
|
||||
`toggle` and `bring` directly without shelling out to `hyprctl`.
|
||||
|
||||
## 6. Scratchpads
|
||||
|
||||
- [x] Preserve named scratchpads: element, gmail, htop, messages, slack,
|
||||
spotify, transmission, volume.
|
||||
- [x] Preserve dropdown terminal scratchpad.
|
||||
- [x] Scratchpads near-fullscreen and centered.
|
||||
- [x] Scratchpads hidden from normal listings/status bar.
|
||||
- [x] Toggling scratchpad exits fullscreen/monocle state first.
|
||||
- [x] Decide hyprscratch daemon is not needed in the Lua branch.
|
||||
- [x] Replace `hyprscratch toggle` with Lua-managed scratchpad toggles.
|
||||
- [x] Disable hyprscratch service on the Lua branch.
|
||||
- [x] Handle delayed class/title assignment with window class/title event adoption.
|
||||
- [x] Handle already-running app.
|
||||
- [x] Handle minimized app.
|
||||
- [x] Handle app on another workspace.
|
||||
|
||||
## 7. Minimization
|
||||
|
||||
- [x] Implement minimize active window.
|
||||
- [x] Implement restore last minimized window.
|
||||
- [x] Exclude minimized windows from layout.
|
||||
- [x] Exclude minimized windows from normal go/bring lists.
|
||||
- [x] Implement minimized picker.
|
||||
- [x] Implement restore all minimized.
|
||||
- [x] Implement minimize other windows of current workspace class.
|
||||
- [x] Implement restore windows of focused class.
|
||||
- [x] Decide hidden workspace naming/state model for minimized windows.
|
||||
- [x] Hydrate minimized-window state from the hidden workspace on restore/picker
|
||||
paths.
|
||||
|
||||
## 8. Class-Aware Workflows
|
||||
|
||||
- [x] Gather all windows of focused class onto current workspace.
|
||||
- [x] Focus next window of different/same class as desired parity.
|
||||
- [x] Browser raise-or-spawn.
|
||||
- [x] Window info command exposes class/title/workspace/address/pid.
|
||||
- [ ] Window menus expose real window icons.
|
||||
- [x] Prefer Lua window queries over `hyprctl clients`.
|
||||
|
||||
## 9. Status Bar Contract
|
||||
|
||||
- [ ] Confirm taffybar can still list normal workspaces.
|
||||
- [ ] Confirm special scratchpad/minimize workspaces are filtered.
|
||||
- [ ] Confirm active workspace per monitor remains visible.
|
||||
- [ ] Confirm class/title/active/minimized/urgent metadata is available.
|
||||
- [x] Expose layout name/state if practical.
|
||||
- [ ] Confirm workspace/window positioning remains enough for icon strips.
|
||||
|
||||
Layout state note: Lua writes `$XDG_RUNTIME_DIR/hyprland-layout-state` with the
|
||||
active workspace, active layout, and per-workspace layout map. Taffybar still
|
||||
needs a live readback check.
|
||||
|
||||
## 10. Session And Utilities
|
||||
|
||||
- [x] Terminal binding preserved.
|
||||
- [x] Launcher/run menu preserved.
|
||||
- [x] Media keys preserved.
|
||||
- [x] Clipboard history binding preserved.
|
||||
- [x] Screenshot binding preserved.
|
||||
- [x] Lock binding preserved.
|
||||
- [x] Session startup target integration preserved.
|
||||
- [x] `hyprctl reload` may remain available as a non-window-manipulation escape
|
||||
hatch.
|
||||
- [x] Resolve `Hyper+w` conflict: monitor focus must win; wallpaper picker
|
||||
needs another key.
|
||||
- [x] Keep rofi utility commands as external commands unless there is a
|
||||
meaningful Lua replacement.
|
||||
- [x] Decide which shell utilities are acceptable because they are not Hyprland
|
||||
control scripts.
|
||||
|
||||
## 11. Validation
|
||||
|
||||
- [x] Lua syntax check.
|
||||
- [x] Lua stub execution check.
|
||||
- [x] `hyprctl` rejection in Lua config for window/workspace manipulation.
|
||||
- [x] Real `Hyprland --verify-config` check.
|
||||
- [x] hyprNStack flake build check.
|
||||
- [x] hyprexpo Lua-branch flake build check.
|
||||
- [x] `ryzen-shine` system dry-run.
|
||||
- [x] `just switch` activates successfully and deploys branch-owned
|
||||
`~/.config/hypr/hyprland.lua`.
|
||||
- [x] Re-run checks after Hyprland/Lua input confirmation.
|
||||
- [ ] Try live compositor smoke test again after version bump.
|
||||
- [x] Document `--verify-config` caveats for Lua rule/plugin-specific config.
|
||||
- [x] Eventually run `just switch` only when the branch is coherent enough for a
|
||||
live test.
|
||||
|
||||
Live-smoke note: this Hyprland binary exposes `--verify-config` but no
|
||||
`--headless` CLI flag. `just switch` now installs the Lua branch binary and
|
||||
deploys `hyprland.lua`, but the currently running compositor remains the old
|
||||
0.53 process until the Hyprland session is restarted. A true compositor smoke
|
||||
test still needs a session restart or a nested Wayland session that avoids
|
||||
startup side effects.
|
||||
@@ -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
|
||||
33
docs/shared-dotfiles.md
Normal file
33
docs/shared-dotfiles.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Shared Dotfiles Worktree
|
||||
|
||||
This repo is intended to live at `/srv/dotfiles` on shared NixOS machines.
|
||||
Home Manager links user dotfiles to that shared checkout instead of to
|
||||
`$HOME/dotfiles`, so the links work consistently for every managed user.
|
||||
|
||||
Set it up from any existing checkout:
|
||||
|
||||
```sh
|
||||
just setup-shared-dotfiles
|
||||
```
|
||||
|
||||
The setup command:
|
||||
|
||||
- copies the current checkout to `/srv/dotfiles` when needed
|
||||
- makes the checkout readable by everyone
|
||||
- makes it writable by the `wheel` group
|
||||
- sets directory setgid/default ACLs so new files stay group-writable
|
||||
- configures Git for group sharing
|
||||
- creates `/etc/nixos -> /srv/dotfiles/nixos` when `/etc/nixos` is absent or already a symlink
|
||||
|
||||
Use a different target or group when needed:
|
||||
|
||||
```sh
|
||||
just setup-shared-dotfiles --target /srv/dotfiles --group wheel
|
||||
```
|
||||
|
||||
If a machine has a real `/etc/nixos` directory and you want to replace it with
|
||||
the shared checkout symlink:
|
||||
|
||||
```sh
|
||||
just setup-shared-dotfiles --force-etc-nixos
|
||||
```
|
||||
@@ -8,6 +8,65 @@ This document describes the tiling window manager experience I am targeting.
|
||||
- Important: expected for parity, but a rough first version is acceptable.
|
||||
- Nice: useful polish or compatibility.
|
||||
|
||||
Priority describes the target experience, not implementation order. A first
|
||||
usable implementation may ship a smaller daily-driver subset as long as it does
|
||||
not choose designs that block required behavior later.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
Phase 1 should establish the core daily-driver loop:
|
||||
|
||||
- Global numbered workspaces across monitors.
|
||||
- Dynamic equal-width columns and tabbed/fullscreen-style layout.
|
||||
- Directional window focus, directional movement, and directional monitor
|
||||
focus.
|
||||
- Direct numbered workspace move/follow bindings.
|
||||
- Focus-follows-mouse and mouse-follows-focus.
|
||||
- Basic rofi launcher, terminal, close, reload, and session-exit bindings.
|
||||
- Basic status-bar workspace and focused-window state.
|
||||
|
||||
Phase 2 should restore high-frequency workflow parity:
|
||||
|
||||
- Per-monitor workspace history and history cycling.
|
||||
- Scratchpads.
|
||||
- Minimization.
|
||||
- Go-to-window, bring-window, and replace-window pickers.
|
||||
- Browser raise-or-spawn and class-aware gather workflows.
|
||||
- Status-bar window lists, class/title/icon metadata, and special-workspace
|
||||
filtering.
|
||||
|
||||
Phase 3 should add visual discovery and polish:
|
||||
|
||||
- Visual window overview.
|
||||
- Visual workspace expose.
|
||||
- Overview go/bring/replace actions.
|
||||
- Smart gaps, smart borders, dimming, wallpaper, lock, screenshot, clipboard,
|
||||
DDC/input switching, and other session utilities.
|
||||
|
||||
## Terms and Semantics
|
||||
|
||||
- First-class operation means the action has a direct command or binding. It
|
||||
does not require opening a picker, manually moving focus, or chaining multiple
|
||||
unrelated commands.
|
||||
- Preserving useful focus means the operation leaves keyboard focus in a
|
||||
predictable place. Non-following moves keep focus on the source monitor or
|
||||
source workspace. Following moves focus the moved window on its destination.
|
||||
- Directional focus uses visible window geometry when windows have distinct
|
||||
rectangles. In tabbed or fullscreen-style layouts where geometry overlaps,
|
||||
directional focus may use a stable logical order instead, but repeated
|
||||
directional actions must cycle predictably through the windows.
|
||||
- Near-fullscreen scratchpads are centered floating windows large enough to
|
||||
dominate the current monitor without taking compositor fullscreen state.
|
||||
- Robust scratchpad behavior means toggling a named scratchpad finds or
|
||||
launches the intended app even when the app starts slowly, changes class or
|
||||
title after launch, is minimized, or is currently on another workspace.
|
||||
- Approximate window position means enough geometry or ordering information for
|
||||
status-bar window strips and expose-like previews. Pixel-perfect compositor
|
||||
geometry is useful but not required.
|
||||
- Normal workspaces are the bounded user-facing workspaces. Special,
|
||||
scratchpad, minimized, hidden, internal, and out-of-range workspaces are not
|
||||
normal workspaces.
|
||||
|
||||
## Modifier Terminology
|
||||
|
||||
- `Super` names the physical modifier key often labeled Windows, Command, GUI,
|
||||
@@ -43,13 +102,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 +121,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:
|
||||
@@ -72,24 +155,26 @@ Required behavior:
|
||||
- Directional monitor focus is available.
|
||||
- Directional window movement between monitors is available.
|
||||
- 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.
|
||||
is available.
|
||||
- Directional bindings are defined in the Binding Appendix. Required
|
||||
directional actions must not depend on `Hyper+Ctrl`, because `Ctrl` may
|
||||
already be part of the fallback `Hyper` chord.
|
||||
|
||||
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:
|
||||
@@ -99,6 +184,8 @@ Required behavior:
|
||||
- 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.
|
||||
@@ -109,6 +196,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:
|
||||
@@ -118,7 +208,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.
|
||||
@@ -127,7 +216,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.
|
||||
@@ -141,28 +231,48 @@ 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
|
||||
minimized picker.
|
||||
- Window switchers hide internal windows.
|
||||
- Go/bring actions unminimize selected windows when needed.
|
||||
- Selecting a minimized window or hidden scratchpad from the visual window
|
||||
overview recovers it onto the current workspace before focusing it.
|
||||
|
||||
## Scratchpads
|
||||
|
||||
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.
|
||||
- A named scratchpad exists for volume.
|
||||
- A named scratchpad exists for x.com.
|
||||
- Scratchpads appear near-fullscreen and centered by default.
|
||||
- Floating scratchpad behavior is a state of a window, not a property of its
|
||||
application class. Floating any window at scratchpad geometry enters that
|
||||
state; tiling it leaves that state.
|
||||
- Invoking a scratchpad replaces only another window currently in floating
|
||||
scratchpad mode. Other floating windows are left alone.
|
||||
- Regular scratchpad targets can be tiled into the normal workspace when
|
||||
desired, while retaining their shortcut association.
|
||||
- Summoning a scratchpad target with its shortcut always restores floating
|
||||
scratchpad mode, even if the window was previously tiled.
|
||||
- Toggling a scratchpad deactivates fullscreen/tabbed state first.
|
||||
- Scratchpads are hidden from normal workspace and window listings.
|
||||
- Floating scratchpads are hidden from normal workspace and window listings.
|
||||
|
||||
Important behavior:
|
||||
|
||||
@@ -274,19 +384,42 @@ 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.
|
||||
follows it.
|
||||
- `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:
|
||||
@@ -295,10 +428,10 @@ Required behavior:
|
||||
- `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.
|
||||
- Moving the focused window to an empty workspace on the monitor in a direction
|
||||
remains required behavior, but it should not require a `Hyper+Ctrl` binding.
|
||||
- `Super+z` focuses the next monitor.
|
||||
- `Super+Shift+z` moves the focused window to the next monitor.
|
||||
|
||||
@@ -316,19 +449,19 @@ Required behavior:
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `Super+Alt+c` toggles the primary AI scratchpad.
|
||||
- `Super+Alt+Shift+c` toggles the backup AI 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.
|
||||
- `Super+Alt+v` toggles the volume scratchpad.
|
||||
- `Super+Alt+x` toggles the x.com scratchpad.
|
||||
|
||||
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
|
||||
@@ -343,8 +476,10 @@ 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+n` opens a Codex Desktop project picker and starts a new thread in
|
||||
the selected saved project root.
|
||||
- `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`.
|
||||
@@ -366,3 +501,8 @@ Important behavior:
|
||||
compositor-appropriate implementation.
|
||||
- Session-destructive operations use shifted or otherwise harder-to-hit
|
||||
variants.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- `Super+Shift+e` is the target replacement for the older `Super+Shift+h`
|
||||
move-to-next-empty-workspace-and-follow binding.
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
# Agentic Session Preferences
|
||||
|
||||
## 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.
|
||||
- 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 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.
|
||||
## Delegating coding work to subagents
|
||||
- When the primary model is Fable, strongly prefer delegating coding tasks to subagents (via the Agent tool). Fable should usually act as the orchestrator: planning, delegating, reviewing, and integrating. It may still work directly when delegation would add disproportionate overhead or the task cannot be usefully separated.
|
||||
- For other primary models, subagent delegation is optional rather than required. Use judgment: delegate when work is meaningfully parallelizable, independently scoped, or benefits from a separate implementation/review pass; work directly when that is simpler and more efficient.
|
||||
- The primary agent remains responsible for reviewing and integrating delegated work.
|
||||
- Whenever you pick a model tier for an agent, record a one-line justification for that choice (in your reasoning/CoT or a brief note in the delegating message) so the decision is auditable. Tie the justification to what the agent must actually decide at execution time, not just the topic's importance — a task specified tightly enough that the taste is already discharged doesn't need the stronger tier. If you can't articulate why the cheaper tier is insufficient, default to it.
|
||||
- These guidelines apply to writing, editing, and refactoring code. Non-coding work (reading, searching, planning, running commands, answering questions) does not need to be delegated.
|
||||
|
||||
## Pane usage
|
||||
- Do not create extra panes or windows unless the user asks.
|
||||
## Model and effort selection
|
||||
|
||||
Treat model selection and effort level as separate decisions. The following scores are subjective routing scores from 1 to 10; for cost, 10 means most expensive.
|
||||
|
||||
| Model | Intelligence / judgment | Design sense | Cost | Primary role |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Fable | 10 | 10 | 10 | Design planning, ambiguity, architecture, intuition, and the hardest work |
|
||||
| GPT-5.6 Sol | 9.5 | 8.5 | 4 | Default engineering model and best intelligence-to-cost choice |
|
||||
| Opus | 8.8 | 9.5 | 7 | Specialized design critique, UX writing, aesthetic review, and alternative perspectives |
|
||||
|
||||
- Do not select models below GPT-5.6 Sol at medium effort. Terra, Sonnet, Luna, GPT Mini, and Haiku should not be used, including for subagents.
|
||||
- Default to Sol at high effort for most implementation, debugging, investigation, review, research, and agentic work.
|
||||
- Use Sol at extra-high effort for unusually difficult but reasonably well-specified engineering, including complex debugging, algorithms, migrations, concurrency, multi-system changes, and high-stakes correctness review.
|
||||
- Sol at medium effort is the minimum permitted configuration. Use it for bounded mechanical work, exploration, summaries, verification, and simple coordination.
|
||||
- Prefer Fable for most design planning, product architecture, UI/UX direction, API design, and work where ambiguity, intuition, or taste materially affects the result.
|
||||
- Use Fable at medium effort for ordinary design planning and at high effort for truly meaty, ambiguous, high-stakes, or long-horizon work.
|
||||
- Use Opus selectively as a specialist rather than the default design planner. Good uses include independent design critique, visual refinement, UX writing, naming, prose, and alternative aesthetic perspectives.
|
||||
- Do not automatically use low or max effort. Medium is the floor, high is the general default, and extra high is reserved for tasks where deeper reasoning is likely to affect correctness.
|
||||
- Difficulty alone does not require Fable. Prefer Sol at extra-high effort when execution is difficult but the desired outcome is clear; prefer Fable when determining the right outcome requires judgment or taste.
|
||||
|
||||
## Cross-model delegation
|
||||
- Use cross-model delegation only when the user requests it or model diversity or an independent check would be useful; it is never mandatory.
|
||||
- Codex should use the `claude_delegator` agent only for the specialized Opus cases below. Claude should prefer the `codex-delegator` agent when delegating to Sol. Use `$cross-agent-delegation` or its `ask-claude` and `ask-codex` wrappers when direct invocation is simpler.
|
||||
- Permit at most one cross-model handoff and never recursively delegate. Keep the child read-only and advisory by default, with only one writer per worktree.
|
||||
- The parent agent owns review, verification, and integration of the child's output.
|
||||
- A Sol agent must never spawn, invoke, or delegate to Fable, including through cross-model wrappers or indirect subagent chains. If a task warrants Fable, select Fable as the primary model before beginning rather than allowing Sol to escalate itself.
|
||||
- Sol may delegate to other Sol agents at medium effort or higher. It may use Opus only for specialized design critique, UX writing, aesthetic review, or an independent design perspective.
|
||||
- Fable may delegate bounded execution, exploration, and verification work downward to Sol.
|
||||
|
||||
## Sharing dev-server / preview links
|
||||
- When sharing a local server or preview URL, always prefer this machine's Tailscale address over `127.0.0.1`/`localhost`/LAN IPs, so the link opens from any device on the tailnet.
|
||||
- Get the address with `tailscale ip -4` (the `100.x.y.z` IP) or the MagicDNS hostname from `tailscale status`. Prefer the `100.x` IP when a server's `allowedHosts` might reject a hostname.
|
||||
- Start the server bound to all interfaces (e.g. vite's `--host 0.0.0.0` / a `dev:lan` script), not just localhost, or the Tailscale link won't connect. Verify reachability (`curl` the `100.x` URL) before handing it over.
|
||||
- Always start each dev/preview server on a new random high port (and pin it with `--strictPort` so it fails loudly instead of drifting). Never reuse a fixed port and never kill, restart, or otherwise shut down an already-running server — assume other sessions/users depend on it. Leave existing servers alone and just stand up your own.
|
||||
|
||||
## Git worktrees
|
||||
- Default to creating git worktrees under a project-local `.worktrees/` directory at the repository root.
|
||||
@@ -23,8 +48,14 @@
|
||||
- Create `.worktrees/` if needed before running `git worktree add`.
|
||||
- Only use a non-`.worktrees/` location when the user explicitly asks for a different path.
|
||||
|
||||
## GitHub pull requests
|
||||
- Default to creating pull requests as ready for review, not drafts.
|
||||
- Do not add a `[codex]` prefix or any other agent/tool prefix to pull request titles.
|
||||
- Create a draft pull request only when the user explicitly asks for a draft or when the remote platform requires draft status.
|
||||
- If using a helper, skill, or CLI wrapper that defaults to draft PRs, override that default before creating the PR.
|
||||
|
||||
## NixOS workflow
|
||||
- This system is managed with a Nix flake at `~/dotfiles/nixos`.
|
||||
- This system is managed with a Nix flake at `/srv/dotfiles/nixos`.
|
||||
- Use `just switch` from that directory for rebuilds instead of plain `nixos-rebuild`.
|
||||
- Host configs live under `machines/`; choose the appropriate host when needed.
|
||||
|
||||
@@ -58,23 +89,6 @@
|
||||
|
||||
This is an org-mode repository containing personal task management, calendars, habits, and project tracking files. It serves as the central hub for Ivan's personal organization.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Chrome DevTools MCP
|
||||
A browser automation MCP is available for interacting with web pages. Use it to:
|
||||
- Navigate to websites and fill out forms
|
||||
- Take screenshots and snapshots of pages
|
||||
- Click elements, type text, and interact with web UIs
|
||||
- Read page content and extract information
|
||||
- Automate multi-step web workflows (booking, purchasing, form submission, etc.)
|
||||
|
||||
### Google Workspace CLI (`gws`)
|
||||
The local `gws` CLI is available for Google Workspace operations. Use it to:
|
||||
- Search, read, and send Gmail messages
|
||||
- Manage Gmail labels and filters
|
||||
- Download attachments and inspect message payloads
|
||||
- Access Drive, Calendar, Docs, Sheets, and other Google Workspace APIs
|
||||
|
||||
## Credentials via `pass`
|
||||
|
||||
Many credentials and personal details are stored in `pass` (the standard unix password manager). There are hundreds of entries covering a wide range of things, so always search before asking the user for information. Use `pass find <keyword>` to search and `pass show <entry>` to retrieve values.
|
||||
@@ -95,8 +109,6 @@ Examples of what's stored:
|
||||
## Guidelines
|
||||
|
||||
- When filling out forms or making purchases, pull personal info from this file and credentials from `pass` rather than asking the user to provide them.
|
||||
- For web tasks, prefer using the Chrome DevTools MCP to automate interactions directly.
|
||||
- For email tasks, prefer using `gws gmail` over navigating to Gmail in the browser.
|
||||
- If a task requires a credential not found in `pass`, ask the user rather than guessing.
|
||||
- This repo's org files (gtd.org, calendar.org, habits.org, projects.org) contain task and scheduling data. The org-agenda-api skill/service can also be used to query agenda data programmatically.
|
||||
|
||||
@@ -122,3 +134,4 @@ Examples of what's stored:
|
||||
- `./project-guides/taffybar.md`
|
||||
- `./project-guides/railbird.md`
|
||||
- `./project-guides/org-emacs-packages.md`
|
||||
- `./project-guides/subtr-actor-rocket-sense-rlru.md`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Subtr Actor / Rocket Sense / rlru constellation
|
||||
|
||||
## Scope
|
||||
- Use this guide for requests involving Rocket League replay parsing, replay analytics, upload flows, or the `rlrml` projects around `subtr-actor`, `rocket-sense`, and `rlru`.
|
||||
- Primary anchors are `subtr-actor` for replay-domain logic, `rocket-sense` for the hosted analytics service, and `rlru` for local replay discovery/upload and PsyNet integration.
|
||||
|
||||
## Related packages/projects (trigger list)
|
||||
- If any of these names are mentioned, open this guide for context.
|
||||
- `subtr-actor`: Rocket League replay processing core and source-of-truth replay domain model.
|
||||
- `rocket-sense`: replay analytics backend and React/Vite web app built on `subtr-actor`.
|
||||
- `rlru`: Rust-first Rocket League replay uploader and desktop client workspace.
|
||||
- `psynet`: Psyonix PsyNet client crate inside the `rlru` workspace.
|
||||
|
||||
## Package inventory
|
||||
- `subtr-actor` repo packages:
|
||||
- Rust crates: `subtr-actor`, `subtr-actor-tools`, `subtr-actor-bakkesmod`, `rl-replay-subtr-actor`.
|
||||
- Python package/crate: `subtr-actor-py` / `subtr_actor`.
|
||||
- npm packages: `@rlrml/subtr-actor`, `@rlrml/player`, `@rlrml/stats-player`.
|
||||
- `rocket-sense` repo packages:
|
||||
- Rust crates: `rocket-sense-server`, `rocket-sense-db`, `rocket-sense-storage`.
|
||||
- Web app package: `rocket-sense-web`.
|
||||
- Vendored packages may appear under `vendor/subtr-actor`; prefer the standalone `subtr-actor` checkout for source-of-truth domain changes unless the user specifically asks about the vendored copy.
|
||||
- `rlru` repo packages:
|
||||
- Rust crates: `rlru`, `psynet`, `rlru-dioxus`.
|
||||
- Apps/binaries: `rlru` CLI and `rlru-dioxus` desktop client.
|
||||
|
||||
## Symlink targets
|
||||
- `./project-links/subtr-actor` -> primary `subtr-actor` repo.
|
||||
- `./project-links/rocket-sense` -> primary `rocket-sense` repo.
|
||||
- `./project-links/rlru` -> primary `rlru` repo.
|
||||
|
||||
## Discovery hints
|
||||
- Start from `~/Projects`.
|
||||
- Common local paths are:
|
||||
- `~/Projects/subtr-actor`
|
||||
- `~/Projects/rocket-sense`
|
||||
- `~/Projects/rlru`
|
||||
- `rocket-sense` may vendor `subtr-actor` under `vendor/subtr-actor`; prefer the standalone `subtr-actor` checkout for source-of-truth replay-domain changes unless the user specifically asks about the vendored copy.
|
||||
|
||||
## Read-first docs
|
||||
- `./project-links/subtr-actor/AGENTS.md`
|
||||
- `./project-links/subtr-actor/README.md`
|
||||
- `./project-links/rocket-sense/AGENTS.md`
|
||||
- `./project-links/rocket-sense/README.md`
|
||||
- `./project-links/rlru/README.md`
|
||||
|
||||
## Notes
|
||||
- Treat `subtr-actor` as the source of truth for replay parsing, frame/state extraction, stats calculators, feature matrices, and JS/WASM replay-player data contracts.
|
||||
- Treat `rocket-sense` as the service/UI layer for replay hosting, metadata, processing state, auth, storage, OpenAPI, and deployed analytics workflows.
|
||||
- Treat `rlru` as the local uploader/client layer for replay discovery, account/auth state, upload destinations, Dioxus desktop UX, and the reusable `psynet` client.
|
||||
- For cross-repo work, check each repo's own `AGENTS.md`, `README.md`, and `justfile` before choosing commands.
|
||||
19
dotfiles/agents/skills/cross-agent-delegation/SKILL.md
Normal file
19
dotfiles/agents/skills/cross-agent-delegation/SKILL.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: cross-agent-delegation
|
||||
description: Safely delegate between Codex and Claude through read-only CLI wrappers. Use when the user explicitly asks Codex or Claude to delegate to the other, or when a workflow explicitly calls for an independent cross-model second opinion.
|
||||
---
|
||||
|
||||
# Cross-Agent Delegation
|
||||
|
||||
Use the minimum permitted native driver agent when available. Invoke the wrapper directly when that is simpler:
|
||||
|
||||
- Pipe a self-contained prompt to `ask-claude` or `ask-codex` on stdin.
|
||||
- Reserve `--base64 '<payload>'` for native driver agents as injection-safe transport. A driver must encode the complete task itself and never embed raw delegated task text in shell source.
|
||||
- Treat the returned plain text as advisory output; the wrappers are read-only and ephemeral.
|
||||
- Review and verify the output in the parent agent before using it.
|
||||
- Never include credentials, secrets, or unnecessary personal data in the prompt.
|
||||
- Permit at most one cross-model handoff. Never ask the child to invoke either CLI or delegate again.
|
||||
- A Sol caller may target Opus for specialized design work but must never target Fable. Choose Fable as the primary model before the task begins when Fable is warranted.
|
||||
- Keep one writer per worktree. Have the parent make any resulting edits.
|
||||
|
||||
Environment variables documented in the scripts may override their conservative model, effort, timeout, turn, and budget defaults.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Cross-agent delegation"
|
||||
short_description: "Safely consult the other coding agent"
|
||||
default_prompt: "Use $cross-agent-delegation to get an independent cross-model second opinion."
|
||||
151
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-claude
Executable file
151
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-claude
Executable file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
input_mode=stdin
|
||||
base64_payload=
|
||||
case "$#" in
|
||||
0) ;;
|
||||
2)
|
||||
if [ "$1" != "--base64" ]; then
|
||||
echo "ask-claude: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
fi
|
||||
input_mode=base64
|
||||
base64_payload=$2
|
||||
;;
|
||||
*)
|
||||
echo "ask-claude: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if [ -z "$base64_payload" ] || [[ ! "$base64_payload" =~ ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ ]]; then
|
||||
echo "ask-claude: invalid RFC4648 base64 payload or padding" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${CROSS_AGENT_DEPTH:-0}" != "0" ]; then
|
||||
echo "ask-claude: recursive cross-agent delegation is not allowed" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for required_command in claude timeout mktemp jq cat chmod rm; do
|
||||
if ! command -v "$required_command" >/dev/null 2>&1; then
|
||||
echo "ask-claude: required command not found: $required_command" >&2
|
||||
exit 127
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$input_mode" = base64 ] && ! command -v base64 >/dev/null 2>&1; then
|
||||
echo "ask-claude: required command not found: base64" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
export CROSS_AGENT_DEPTH=1
|
||||
|
||||
timeout_value=${CROSS_AGENT_TIMEOUT:-10m}
|
||||
model=${CROSS_AGENT_CLAUDE_MODEL:-opus}
|
||||
effort=${CROSS_AGENT_CLAUDE_EFFORT:-medium}
|
||||
max_turns=${CROSS_AGENT_CLAUDE_MAX_TURNS:-4}
|
||||
max_budget_usd=${CROSS_AGENT_CLAUDE_MAX_BUDGET_USD:-0.25}
|
||||
|
||||
case "$model" in
|
||||
opus|claude-opus-*) ;;
|
||||
*)
|
||||
echo "ask-claude: cross-model delegation is restricted to Opus, not Fable or lower-tier models" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$effort" in
|
||||
medium|high|xhigh|max) ;;
|
||||
*)
|
||||
echo "ask-claude: effort must be medium or higher" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
umask 077
|
||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/ask-claude.XXXXXX")
|
||||
cleanup() {
|
||||
rm -rf -- "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
prompt_file="$tmp_dir/prompt"
|
||||
result_file="$tmp_dir/result.json"
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if base64 --decode </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 --decode)
|
||||
elif base64 -D </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 -D)
|
||||
else
|
||||
echo "ask-claude: base64 command supports neither --decode nor -D" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$base64_payload" | "${base64_decode[@]}" >"$prompt_file"; then
|
||||
echo "ask-claude: failed to decode base64 payload" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
cat >"$prompt_file"
|
||||
fi
|
||||
chmod 600 "$prompt_file"
|
||||
|
||||
if [ ! -s "$prompt_file" ]; then
|
||||
echo "ask-claude: prompt on stdin must not be empty" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
guard_instruction='Act only as a read-only advisory child. Do not invoke Codex or Claude, directly or indirectly. Do not spawn, delegate to, or communicate with any other agent. Do not edit, create, delete, move, or rename files. Return only your analysis or recommendation to the parent.'
|
||||
|
||||
set +e
|
||||
timeout --foreground "$timeout_value" \
|
||||
claude -p \
|
||||
--model "$model" \
|
||||
--effort "$effort" \
|
||||
--max-turns "$max_turns" \
|
||||
--max-budget-usd "$max_budget_usd" \
|
||||
--output-format json \
|
||||
--permission-mode dontAsk \
|
||||
--tools Read,Grep,Glob \
|
||||
--allowedTools Read,Grep,Glob \
|
||||
--no-session-persistence \
|
||||
--no-chrome \
|
||||
--append-system-prompt "$guard_instruction" \
|
||||
<"$prompt_file" >"$result_file"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if [ "$status" -eq 124 ]; then
|
||||
echo "ask-claude: timed out after $timeout_value" >&2
|
||||
else
|
||||
echo "ask-claude: claude exited with status $status" >&2
|
||||
fi
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if ! jq -e . "$result_file" >/dev/null 2>&1; then
|
||||
echo "ask-claude: claude returned invalid JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if jq -e '(.is_error? // false) == true' "$result_file" >/dev/null; then
|
||||
echo "ask-claude: claude reported an error" >&2
|
||||
jq -r '.result? // empty' "$result_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq -e '.result? | type == "string"' "$result_file" >/dev/null; then
|
||||
echo "ask-claude: claude response does not contain a string result" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jq -r '.result' "$result_file"
|
||||
139
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-codex
Executable file
139
dotfiles/agents/skills/cross-agent-delegation/scripts/ask-codex
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
input_mode=stdin
|
||||
base64_payload=
|
||||
case "$#" in
|
||||
0) ;;
|
||||
2)
|
||||
if [ "$1" != "--base64" ]; then
|
||||
echo "ask-codex: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
fi
|
||||
input_mode=base64
|
||||
base64_payload=$2
|
||||
;;
|
||||
*)
|
||||
echo "ask-codex: expected a prompt on stdin or --base64 <payload>" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if [ -z "$base64_payload" ] || [[ ! "$base64_payload" =~ ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ ]]; then
|
||||
echo "ask-codex: invalid RFC4648 base64 payload or padding" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${CROSS_AGENT_DEPTH:-0}" != "0" ]; then
|
||||
echo "ask-codex: recursive cross-agent delegation is not allowed" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for required_command in codex timeout mktemp cat chmod rm; do
|
||||
if ! command -v "$required_command" >/dev/null 2>&1; then
|
||||
echo "ask-codex: required command not found: $required_command" >&2
|
||||
exit 127
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$input_mode" = base64 ] && ! command -v base64 >/dev/null 2>&1; then
|
||||
echo "ask-codex: required command not found: base64" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
export CROSS_AGENT_DEPTH=1
|
||||
|
||||
timeout_value=${CROSS_AGENT_TIMEOUT:-10m}
|
||||
model=${CROSS_AGENT_CODEX_MODEL:-gpt-5.6-sol}
|
||||
reasoning_effort=${CROSS_AGENT_CODEX_REASONING_EFFORT:-medium}
|
||||
|
||||
case "$model" in
|
||||
gpt-5.6-sol) ;;
|
||||
*)
|
||||
echo "ask-codex: cross-model delegation is restricted to GPT-5.6 Sol" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$reasoning_effort" in
|
||||
medium|high|xhigh|max) ;;
|
||||
*)
|
||||
echo "ask-codex: reasoning effort must be medium or higher" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
umask 077
|
||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/ask-codex.XXXXXX")
|
||||
cleanup() {
|
||||
rm -rf -- "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
prompt_file="$tmp_dir/prompt"
|
||||
result_file="$tmp_dir/final-message"
|
||||
if [ "$input_mode" = base64 ]; then
|
||||
if base64 --decode </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 --decode)
|
||||
elif base64 -D </dev/null >/dev/null 2>&1; then
|
||||
base64_decode=(base64 -D)
|
||||
else
|
||||
echo "ask-codex: base64 command supports neither --decode nor -D" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$base64_payload" | "${base64_decode[@]}" >"$prompt_file"; then
|
||||
echo "ask-codex: failed to decode base64 payload" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
cat >"$prompt_file"
|
||||
fi
|
||||
chmod 600 "$prompt_file"
|
||||
|
||||
if [ ! -s "$prompt_file" ]; then
|
||||
echo "ask-codex: prompt on stdin must not be empty" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
guard_instruction='Act only as a read-only advisory child. Do not invoke Codex or Claude, directly or indirectly. Do not spawn, delegate to, or communicate with any other agent. Do not edit, create, delete, move, or rename files. Return only your analysis or recommendation to the parent.'
|
||||
|
||||
set +e
|
||||
timeout --foreground "$timeout_value" \
|
||||
codex exec \
|
||||
--ephemeral \
|
||||
--ignore-user-config \
|
||||
--disable multi_agent \
|
||||
--skip-git-repo-check \
|
||||
-C "$PWD" \
|
||||
-s read-only \
|
||||
-m "$model" \
|
||||
-c "approval_policy='never'" \
|
||||
-c "model_reasoning_effort='$reasoning_effort'" \
|
||||
-c "developer_instructions=$guard_instruction" \
|
||||
-o "$result_file" \
|
||||
- \
|
||||
<"$prompt_file" >/dev/null
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if [ "$status" -eq 124 ]; then
|
||||
echo "ask-codex: timed out after $timeout_value" >&2
|
||||
else
|
||||
echo "ask-codex: codex exited with status $status" >&2
|
||||
fi
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if [ ! -s "$result_file" ]; then
|
||||
echo "ask-codex: codex did not produce a final message" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat "$result_file"
|
||||
145
dotfiles/agents/skills/disk-space-assessment/SKILL.md
Normal file
145
dotfiles/agents/skills/disk-space-assessment/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: disk-space-assessment
|
||||
description: Measure and explain disk usage without deleting data, producing reusable compressed ncdu snapshots and an evidence-backed cleanup handoff. Use when disk is low, a filesystem is unexpectedly large, builds fail from space pressure, the user asks what is consuming space, or deeper iterative disk analysis is needed before cleanup.
|
||||
---
|
||||
|
||||
# Disk Space Assessment
|
||||
|
||||
Build a reproducible picture of disk usage. Stop at findings and proposed actions; use `disk-space-cleanup` when the user authorizes remediation.
|
||||
|
||||
Read `references/ignore-paths.md` before scanning. Read `references/direnv-gc-roots.md` for Nix development-root attribution. Read `references/observed-heavy-hitters.md` when triaging this machine or interpreting familiar paths.
|
||||
|
||||
## Required Artifact Contract
|
||||
|
||||
Always create a reusable, timestamped `ncdu` export for every filesystem or major root assessed. Never make an interactive `ncdu` session, transient `/tmp` export, or `du` output the only record of an assessment.
|
||||
|
||||
Always scan `/` with privilege. An unprivileged root scan is incomplete by design because it cannot measure private service state. Also scan each separately mounted pressured filesystem because `safe_ncdu -x` does not cross mount boundaries:
|
||||
|
||||
```bash
|
||||
/run/wrappers/bin/sudo -n env HOME=/home/imalison safe_ncdu /
|
||||
safe_ncdu /home
|
||||
safe_ncdu /nix/store
|
||||
```
|
||||
|
||||
`safe_ncdu` writes these durable artifacts under `~/.cache/ncdu/`:
|
||||
|
||||
- `safe-ncdu-<root>-<timestamp>.json.zst`: compressed importable ncdu data
|
||||
- `<snapshot>.excludes`: exact exclusions used
|
||||
- `<snapshot>.meta`: scan root, timestamps, hostname, ncdu version, status, and artifact paths
|
||||
- `latest-<root>.json.zst` plus matching sidecar symlinks
|
||||
|
||||
Keep timestamped files intact for iterative analysis. Report their absolute paths in the handoff. If a scan cannot finish, retain and report the failed manifest, mark the coverage gap, and use bounded probes as supplemental evidence.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Record filesystem pressure and topology.
|
||||
2. Run a privileged `/` scan and choose additional roots for separately mounted filesystems.
|
||||
3. Create reusable `safe_ncdu` snapshots before any cleanup.
|
||||
4. Analyze snapshots repeatedly with `top` and `open`; do not rescan for every question.
|
||||
5. Attribute special stores such as Nix separately.
|
||||
6. Produce findings, confidence, cleanup candidates, and artifact paths.
|
||||
|
||||
## 1. Establish the Baseline
|
||||
|
||||
```bash
|
||||
df -h /
|
||||
df -h /home
|
||||
df -h /nix
|
||||
findmnt -rn -o TARGET,FSTYPE,SOURCE,OPTIONS
|
||||
```
|
||||
|
||||
Record used/free space and whether `/home`, `/nix`, or other large paths are separate mounts. Note active builds or services that may cause measurements to move during the assessment.
|
||||
|
||||
## 2. Select Scan Roots
|
||||
|
||||
Require one-filesystem coverage:
|
||||
|
||||
- Always scan `/` with `/run/wrappers/bin/sudo -n env HOME=/home/imalison`; never substitute an unprivileged root scan. On NixOS, use the setuid wrapper explicitly because a non-setuid `sudo` store binary may appear earlier on `PATH`.
|
||||
- Scan separately mounted `/home` and `/nix/store` independently.
|
||||
- Add a focused root such as `~/Projects` when the first snapshot identifies it as dominant.
|
||||
- Treat a failed privileged root scan as an explicit coverage gap; do not silently fall back to an unprivileged scan.
|
||||
|
||||
Inspect exclusions before a long scan:
|
||||
|
||||
```bash
|
||||
safe_ncdu excludes /
|
||||
```
|
||||
|
||||
Update `references/ignore-paths.md` and the implementation of `safe_ncdu` together when a newly discovered remote, recursive, or special mount needs a permanent exclusion.
|
||||
|
||||
## 3. Create the Snapshots
|
||||
|
||||
Run scans early enough that cleanup does not destroy the evidence:
|
||||
|
||||
```bash
|
||||
/run/wrappers/bin/sudo -n env HOME=/home/imalison safe_ncdu /
|
||||
safe_ncdu /home
|
||||
safe_ncdu /nix/store
|
||||
```
|
||||
|
||||
If `safe_ncdu` is unavailable, source or run `/srv/dotfiles/dotfiles/lib/functions/safe_ncdu`. If `ncdu` itself is missing, use Nix temporarily rather than substituting a non-reusable interactive scan.
|
||||
|
||||
Do not store the privileged scan in root's home. Set `HOME=/home/imalison` so all artifacts remain together and are available to later sessions. Restore ownership to `imalison:users` if `sudo` creates root-owned snapshot artifacts.
|
||||
|
||||
## 4. Analyze Iteratively
|
||||
|
||||
Query the same export at multiple depths:
|
||||
|
||||
```bash
|
||||
safe_ncdu top ~/.cache/ncdu/latest-root.json.zst 30
|
||||
safe_ncdu top ~/.cache/ncdu/latest-root.json.zst 30 /home/imalison
|
||||
safe_ncdu top ~/.cache/ncdu/latest-home.json.zst 30 /imalison/Projects
|
||||
safe_ncdu open ~/.cache/ncdu/latest-root.json.zst
|
||||
```
|
||||
|
||||
Use `du` only for bounded confirmation, live-change checks, or paths missing from the snapshot:
|
||||
|
||||
```bash
|
||||
timeout 30s du -xh --max-depth=1 "$HOME/.cache" 2>/dev/null | sort -h
|
||||
timeout 30s du -xh --max-depth=1 "$HOME/.local/share" 2>/dev/null | sort -h
|
||||
```
|
||||
|
||||
Distinguish physical allocation from logical/apparent size, especially for sparse files, hardlinked Nix store data, copy-on-write storage, and container layers. Do not sum overlapping Nix closure sizes as reclaim estimates.
|
||||
|
||||
## 5. Attribute Nix Store Usage
|
||||
|
||||
When `/nix/store` is large, first quantify whether garbage collection can help:
|
||||
|
||||
```bash
|
||||
nix_store_audit --top 30
|
||||
nix-store --gc --print-dead
|
||||
nix-store --gc --print-roots
|
||||
```
|
||||
|
||||
Use `/srv/dotfiles/dotfiles/lib/functions/find_store_path_gc_roots` and `nix why-depends` to explain why a large path is retained. Inspect `.direnv/flake-profile-*`, `result*` symlinks, system generations, and current/booted system closures. Prefer `nix_store_audit` over an initial `du -sh /nix/store`, which is slow and does not explain retention.
|
||||
|
||||
Always create a reusable `.direnv` GC-root audit artifact when direnv roots exist:
|
||||
|
||||
```bash
|
||||
python /srv/dotfiles/dotfiles/agents/skills/disk-space-assessment/scripts/direnv_gc_roots_audit.py --top 30
|
||||
```
|
||||
|
||||
This separates collectively direnv-only paths from paths retained by non-direnv roots and estimates each project's marginal uniquely retained footprint. Read `references/direnv-gc-roots.md` before interpreting or acting on the result.
|
||||
|
||||
For very large `/nix/store` exports, use `safe_ncdu open` for read-only navigation and the Nix audit tools for noninteractive attribution. `safe_ncdu top` refuses snapshots over 128 MiB compressed by default because its jq parser may exhaust memory. Do not estimate physical store usage by summing raw ncdu JSON `dsize` fields; use `du -sx --block-size=1 /nix/store` when a hardlink-aware physical total is necessary.
|
||||
|
||||
## Assessment Handoff
|
||||
|
||||
Return:
|
||||
|
||||
- Baseline filesystem usage and scan time.
|
||||
- Absolute paths to every snapshot, `.excludes`, and `.meta` artifact.
|
||||
- Coverage and privilege limitations.
|
||||
- Largest consumers with evidence from snapshot queries or store attribution.
|
||||
- Ranked cleanup candidates, expected reclaim range, risk, and whether data is rebuildable.
|
||||
- Volatile paths or active processes that cleanup must preserve.
|
||||
|
||||
Do not delete, prune, garbage-collect, truncate, or mutate data in this skill.
|
||||
|
||||
## Learning Loop
|
||||
|
||||
After each assessment:
|
||||
|
||||
1. Add new permanent scan exclusions to `references/ignore-paths.md` and `safe_ncdu`.
|
||||
2. Add stable machine-specific diagnostic knowledge to `references/observed-heavy-hitters.md`.
|
||||
3. Remove stale observations instead of accumulating an unbounded chronological log.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Disk Space Assessment"
|
||||
short_description: "Build reusable evidence about disk usage"
|
||||
default_prompt: "Use $disk-space-assessment to measure disk usage and create reusable ncdu snapshots."
|
||||
@@ -0,0 +1,50 @@
|
||||
# Auditing `.direnv` Nix GC Roots
|
||||
|
||||
Audit direnv roots as a retention graph, not as a list of symlink sizes.
|
||||
|
||||
## Required Audit
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python /srv/dotfiles/dotfiles/agents/skills/disk-space-assessment/scripts/direnv_gc_roots_audit.py --top 30
|
||||
```
|
||||
|
||||
The script writes a timestamped JSON artifact under `~/.cache/ncdu/` and updates `latest-direnv-gc-roots.json`. Preserve the timestamped artifact with the related ncdu snapshots.
|
||||
|
||||
## Interpret the Measures
|
||||
|
||||
- **Raw roots**: every `.direnv` entry reported by `nix-store --gc --print-roots`; several may point to the same store target.
|
||||
- **Unique targets**: deduplicated store paths directly referenced by those roots.
|
||||
- **Project closure**: union of the transitive closures of one project's direnv targets.
|
||||
- **Collectively direnv-only**: direnv closure paths absent from the union of every observed non-direnv root closure. Removing all direnv roots could make these collectible.
|
||||
- **Outside non-direnv roots**: one project's closure after subtracting non-direnv-retained paths. This remains an upper bound because other direnv projects may share it.
|
||||
- **Marginal unique**: paths retained by exactly one direnv project and no non-direnv root. This is the best logical NAR-size estimate of what removing only that project's `.direnv` could make collectible.
|
||||
- **Shared direnv-only**: paths absent from non-direnv roots but retained by two or more direnv projects. Inspect the recorded retaining-project set to identify cleanup campaigns that must move together.
|
||||
|
||||
Rank projects by marginal unique size, then inspect age, worktree state, active shells/builds, and project importance. Large closure size with near-zero marginal unique size indicates heavy sharing and little immediate benefit from removing that project alone. Use `top_shared_direnv_only_paths` in the JSON artifact to explain the gap between collectively direnv-only size and the sum of marginal project sizes.
|
||||
|
||||
## Validate Candidates
|
||||
|
||||
For a candidate project:
|
||||
|
||||
```bash
|
||||
find <project>/.direnv -maxdepth 1 -type l -printf '%TY-%Tm-%Td %TH:%TM %p -> %l\n' | sort
|
||||
git -C <project> status --short
|
||||
git worktree list --porcelain
|
||||
ps aux | rg '<project>|direnv|nix develop|nix-shell'
|
||||
```
|
||||
|
||||
Resolve a surprising retained store path with:
|
||||
|
||||
```bash
|
||||
/srv/dotfiles/dotfiles/lib/functions/find_store_path_gc_roots /nix/store/<path>
|
||||
nix why-depends <profile-or-shell-target> /nix/store/<path>
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- NAR sizes are logical database sizes, not guaranteed physical bytes reclaimed.
|
||||
- The graph is a point-in-time view; shells and agents can add or replace roots during the run.
|
||||
- Nix paths may also be retained by roots created immediately after enumeration.
|
||||
- Run the audit immediately before and after any cleanup campaign and use actual `df` change as the final measure.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Ignore Paths for Disk Assessment
|
||||
|
||||
Exclude these paths from `ncdu`/`du` scans because they are remote, recursive, separately mounted, or noisy.
|
||||
|
||||
## Known Ignores
|
||||
|
||||
- `$HOME/keybase`
|
||||
- `$HOME/.cache/keybase`
|
||||
- `$HOME/.local/share/keybase`
|
||||
- `$HOME/.config/keybase`
|
||||
- `/keybase`
|
||||
- `/var/lib/railbird`
|
||||
- `/run/user/*/doc` (FUSE portal mount; observed as `/run/user/1004/doc`)
|
||||
|
||||
Discover candidates with:
|
||||
|
||||
```bash
|
||||
findmnt -rn -o TARGET,FSTYPE,SOURCE | rg '(keybase|fuse|rclone|s3|railbird)'
|
||||
```
|
||||
|
||||
When adding an ignore, update `/srv/dotfiles/dotfiles/lib/functions/safe_ncdu` in the same change so recorded snapshots actually use it.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Observed Disk-Usage Patterns
|
||||
|
||||
Use these as hypotheses, not substitutes for a current snapshot.
|
||||
|
||||
## Home and Project Trees
|
||||
|
||||
- Rust `target/` directories under top-level projects, `.worktrees/*`, and `.claude/worktrees/*` have repeatedly consumed 25–100+ GiB in aggregate.
|
||||
- `~/.cache/uv`, `~/.cache/pypoetry`, browser caches, Spotify, and Gradle caches have each reached multi-GiB sizes.
|
||||
- `~/.local/share/picom/debug.log` has exceeded 15 GiB after verbose debugging or crashes.
|
||||
- Trash and active build outputs are high-yield candidates but require lifecycle and process checks.
|
||||
|
||||
## Service State
|
||||
|
||||
- `/var/lib/private/gitea-runner` has exceeded 50 GiB and is invisible to an unprivileged root scan.
|
||||
- `/var` has also been dominated by Docker, containers, Rancher, private state, and journals.
|
||||
- Root accounting must include large sparse or allocated files such as `/swapfile` and ext4 reserved blocks; these may differ from ordinary directory totals.
|
||||
|
||||
## Nix Store
|
||||
|
||||
- A large store may be almost entirely live. In a 2026-07-10 audit, the Nix DB held 413.0 GiB logical NAR size with zero dead paths.
|
||||
- The current system has directly pinned large ComfyUI model weights plus CUDA, GHC, Android, and Rust families.
|
||||
- Hundreds of `.direnv` GC-root entries have retained more than 100 GiB outside the current-system closure. `railbird-mobile`, Railbird backend, taffybar, rocket-sense, and keepbook dev shells have been notable roots.
|
||||
- `result*` symlinks, NixOS generations, and repo-local profiles can pin large closures.
|
||||
- Closure sizes overlap. Compute unions or uniquely collectible sets; never sum per-profile closure sizes as a reclaim estimate.
|
||||
- Physical `/nix/store` usage can be materially lower than logical NAR size after `nix-store --optimise` hardlinks duplicate files.
|
||||
|
||||
## Volatility
|
||||
|
||||
- Concurrent agent builds can move free space by tens of GiB during an assessment.
|
||||
- Severe pressure may surface as SQLite I/O errors, missing compiler diagnostics, or opaque build-script failures rather than an explicit `ENOSPC` message.
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DB_PATH = Path("/nix/var/nix/db/db.sqlite")
|
||||
|
||||
|
||||
def run(*args: str) -> str:
|
||||
return subprocess.run(args, check=True, text=True, capture_output=True).stdout
|
||||
|
||||
|
||||
def chunks(values: list[str], size: int = 100):
|
||||
for index in range(0, len(values), size):
|
||||
yield values[index : index + size]
|
||||
|
||||
|
||||
def closure(targets: set[str]) -> set[str]:
|
||||
result: set[str] = set()
|
||||
existing = sorted(target for target in targets if Path(target).exists())
|
||||
for batch in chunks(existing):
|
||||
result.update(line for line in run("nix-store", "-qR", *batch).splitlines() if line)
|
||||
return result
|
||||
|
||||
|
||||
def sizes(paths: set[str]) -> dict[str, int]:
|
||||
if not paths:
|
||||
return {}
|
||||
connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
|
||||
try:
|
||||
result: dict[str, int] = {}
|
||||
ordered = sorted(paths)
|
||||
for batch in chunks(ordered, 900):
|
||||
placeholders = ",".join("?" for _ in batch)
|
||||
query = f"select path, narSize from ValidPaths where path in ({placeholders})"
|
||||
result.update({path: int(size or 0) for path, size in connection.execute(query, batch)})
|
||||
return result
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def human_size(value: int) -> str:
|
||||
amount = float(value)
|
||||
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
||||
if amount < 1024 or unit == "TiB":
|
||||
return f"{amount:.2f} {unit}"
|
||||
amount /= 1024
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
|
||||
def parse_roots() -> list[tuple[str, str]]:
|
||||
entries: list[tuple[str, str]] = []
|
||||
for line in run("nix-store", "--gc", "--print-roots").splitlines():
|
||||
if " -> " not in line:
|
||||
continue
|
||||
source, target = line.split(" -> ", 1)
|
||||
source = source.strip('"')
|
||||
target = target.strip('"')
|
||||
if target.startswith("/nix/store/"):
|
||||
entries.append((source, target))
|
||||
return entries
|
||||
|
||||
|
||||
def project_for_root(source: str) -> str | None:
|
||||
marker = "/.direnv/"
|
||||
if marker not in source:
|
||||
return None
|
||||
return source.split(marker, 1)[0]
|
||||
|
||||
|
||||
def top_paths(paths: set[str], path_sizes: dict[str, int], limit: int) -> list[dict]:
|
||||
ranked = sorted(paths, key=lambda path: path_sizes.get(path, 0), reverse=True)[:limit]
|
||||
return [{"path": path, "nar_size_bytes": path_sizes.get(path, 0)} for path in ranked]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Audit Nix paths retained by .direnv GC roots.")
|
||||
parser.add_argument("--top", type=int, default=30, help="Projects and store paths to show.")
|
||||
parser.add_argument("--output", help="JSON artifact path; defaults under ~/.cache/ncdu.")
|
||||
args = parser.parse_args()
|
||||
if args.top < 1:
|
||||
parser.error("--top must be positive")
|
||||
if not DB_PATH.is_file():
|
||||
parser.error(f"Nix database not found: {DB_PATH}")
|
||||
|
||||
generated_at = dt.datetime.now().astimezone()
|
||||
roots = parse_roots()
|
||||
grouped: dict[str, list[tuple[str, str]]] = collections.defaultdict(list)
|
||||
non_direnv_targets: set[str] = set()
|
||||
for source, target in roots:
|
||||
project = project_for_root(source)
|
||||
if project is None:
|
||||
non_direnv_targets.add(target)
|
||||
else:
|
||||
grouped[project].append((source, target))
|
||||
|
||||
project_closures = {
|
||||
project: closure({target for _source, target in entries})
|
||||
for project, entries in sorted(grouped.items())
|
||||
}
|
||||
non_direnv_closure = closure(non_direnv_targets)
|
||||
all_direnv_closure = set().union(*project_closures.values()) if project_closures else set()
|
||||
direnv_only = all_direnv_closure - non_direnv_closure
|
||||
|
||||
membership: collections.Counter[str] = collections.Counter()
|
||||
for project_paths in project_closures.values():
|
||||
membership.update(project_paths)
|
||||
|
||||
all_relevant = all_direnv_closure | non_direnv_closure
|
||||
path_sizes = sizes(all_relevant)
|
||||
total = lambda paths: sum(path_sizes.get(path, 0) for path in paths)
|
||||
|
||||
projects = []
|
||||
for project, entries in grouped.items():
|
||||
project_paths = project_closures[project]
|
||||
outside_non_direnv = project_paths - non_direnv_closure
|
||||
marginal = {path for path in outside_non_direnv if membership[path] == 1}
|
||||
direnv_path = Path(project) / ".direnv"
|
||||
mtime = None
|
||||
age_days = None
|
||||
if direnv_path.exists():
|
||||
timestamp = direnv_path.stat().st_mtime
|
||||
mtime = dt.datetime.fromtimestamp(timestamp).astimezone().isoformat()
|
||||
age_days = (generated_at.timestamp() - timestamp) / 86400
|
||||
projects.append(
|
||||
{
|
||||
"project": project,
|
||||
"direnv_mtime": mtime,
|
||||
"direnv_age_days": age_days,
|
||||
"raw_root_count": len(entries),
|
||||
"unique_target_count": len({target for _source, target in entries}),
|
||||
"closure_path_count": len(project_paths),
|
||||
"closure_nar_bytes": total(project_paths),
|
||||
"outside_non_direnv_nar_bytes": total(outside_non_direnv),
|
||||
"marginal_unique_nar_bytes": total(marginal),
|
||||
"shared_with_other_direnv_nar_bytes": total(outside_non_direnv - marginal),
|
||||
"roots": [{"source": source, "target": target} for source, target in entries],
|
||||
"top_marginal_paths": top_paths(marginal, path_sizes, args.top),
|
||||
}
|
||||
)
|
||||
projects.sort(key=lambda item: (item["marginal_unique_nar_bytes"], item["closure_nar_bytes"]), reverse=True)
|
||||
|
||||
shared_direnv_only = {path for path in direnv_only if membership[path] > 1}
|
||||
shared_ranked = sorted(shared_direnv_only, key=lambda path: path_sizes.get(path, 0), reverse=True)[: args.top]
|
||||
top_shared = [
|
||||
{
|
||||
"path": path,
|
||||
"nar_size_bytes": path_sizes.get(path, 0),
|
||||
"retained_by_projects": sorted(
|
||||
project for project, project_paths in project_closures.items() if path in project_paths
|
||||
),
|
||||
}
|
||||
for path in shared_ranked
|
||||
]
|
||||
marginal_total = sum(item["marginal_unique_nar_bytes"] for item in projects)
|
||||
|
||||
artifact = {
|
||||
"format_version": 1,
|
||||
"generated_at": generated_at.isoformat(),
|
||||
"hostname": socket.gethostname(),
|
||||
"measurement": "logical NAR size from the Nix database",
|
||||
"raw_gc_root_count": len(roots),
|
||||
"raw_direnv_root_count": sum(len(entries) for entries in grouped.values()),
|
||||
"unique_direnv_target_count": len({target for entries in grouped.values() for _source, target in entries}),
|
||||
"direnv_project_count": len(grouped),
|
||||
"all_direnv_closure_nar_bytes": total(all_direnv_closure),
|
||||
"collectively_direnv_only_nar_bytes": total(direnv_only),
|
||||
"collectively_direnv_only_path_count": len(direnv_only),
|
||||
"marginal_unique_total_nar_bytes": marginal_total,
|
||||
"shared_direnv_only_nar_bytes": total(shared_direnv_only),
|
||||
"top_collectively_direnv_only_paths": top_paths(direnv_only, path_sizes, args.top),
|
||||
"top_shared_direnv_only_paths": top_shared,
|
||||
"projects": projects,
|
||||
}
|
||||
|
||||
out_dir = Path.home() / ".cache" / "ncdu"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
output = Path(args.output).expanduser() if args.output else out_dir / f"direnv-gc-roots-{generated_at:%Y%m%d-%H%M%S}.json"
|
||||
output = output.resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = output.with_suffix(output.suffix + ".tmp")
|
||||
temporary.write_text(json.dumps(artifact, indent=2) + "\n")
|
||||
os.replace(temporary, output)
|
||||
latest = out_dir / "latest-direnv-gc-roots.json"
|
||||
latest.unlink(missing_ok=True)
|
||||
latest.symlink_to(output)
|
||||
|
||||
print(f"Direnv GC-root artifact: {output}")
|
||||
print(f"Raw direnv roots: {artifact['raw_direnv_root_count']}")
|
||||
print(f"Unique direnv targets: {artifact['unique_direnv_target_count']}")
|
||||
print(f"Projects: {artifact['direnv_project_count']}")
|
||||
print(f"All direnv closures: {human_size(artifact['all_direnv_closure_nar_bytes'])}")
|
||||
print(f"Collectively direnv-only: {human_size(artifact['collectively_direnv_only_nar_bytes'])}")
|
||||
print(f"Sum of project marginal unique: {human_size(artifact['marginal_unique_total_nar_bytes'])}")
|
||||
print(f"Shared by multiple direnv projects only: {human_size(artifact['shared_direnv_only_nar_bytes'])}")
|
||||
print()
|
||||
print(f"{'MARGINAL':>11} {'CLOSURE':>11} {'AGE(d)':>8} PROJECT")
|
||||
for item in projects[: args.top]:
|
||||
age = "?" if item["direnv_age_days"] is None else f"{item['direnv_age_days']:.1f}"
|
||||
print(
|
||||
f"{human_size(item['marginal_unique_nar_bytes']):>11} "
|
||||
f"{human_size(item['closure_nar_bytes']):>11} {age:>8} {item['project']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,67 +1,75 @@
|
||||
---
|
||||
name: disk-space-cleanup
|
||||
description: Investigate and safely reclaim disk space on this machine, especially on NixOS systems with heavy Nix, Rust/Haskell, Docker, and Podman usage. Use when disk is low, builds fail with no-space errors, /nix/store appears unexpectedly large, or the user asks for easy cleanup wins without deleting important data.
|
||||
description: Safely reclaim disk space through reviewed, automated cleanup actions on this machine, especially for Nix, Rust build artifacts, containers, caches, and service state. Use when an assessment has identified cleanup candidates, the user asks to free space or automate remediation, or low-space recovery requires measured deletion of rebuildable data.
|
||||
---
|
||||
|
||||
# Disk Space Cleanup
|
||||
|
||||
Reclaim disk space with a safety-first workflow: investigate first, run obvious low-risk cleanup wins, then do targeted analysis for larger opportunities.
|
||||
Act on evidence from `disk-space-assessment`. Automate guarded remediation, measure each action, and preserve user data and active work.
|
||||
|
||||
Bundled helpers:
|
||||
Bundled resources:
|
||||
|
||||
- `scripts/rust_target_dirs.py`: inventory and guarded deletion for explicit Rust `target/` directories
|
||||
- `references/rust-target-roots.txt`: machine-specific roots for Rust artifact scans
|
||||
- `references/ignore-paths.md`: machine-specific excludes for `du`/`ncdu`
|
||||
- `scripts/rust_target_dirs.py`: inventory and guarded deletion of explicit Cargo `target/` directories
|
||||
- `references/rust-target-roots.txt`: machine-specific Rust scan roots
|
||||
- `references/cleanup-patterns.md`: validated machine-specific remediation patterns
|
||||
|
||||
## Execution Default
|
||||
## Required Input and Handoff
|
||||
|
||||
- Start with non-destructive investigation and quick sizing.
|
||||
- Prioritize easy wins first (`nix-collect-garbage`, container prune, Cargo artifacts).
|
||||
- Propose destructive actions with expected impact before running them.
|
||||
- Run destructive actions only after confirmation, unless the user explicitly requests immediate execution of obvious wins.
|
||||
- Capture new reusable findings by updating this skill before finishing.
|
||||
Prefer a recent assessment containing baseline `df` output, reusable `ncdu` artifact paths, ranked candidates, and active-process caveats. If no reusable snapshot exists, use `disk-space-assessment` first. Even in an emergency, create a timestamped `safe_ncdu` snapshot of the affected filesystem or focused heavy root before deleting data whenever the scan can complete safely.
|
||||
|
||||
Record before/after free space for every cleanup round. After material cleanup, create a new reusable snapshot through `disk-space-assessment` so the result can be compared without losing the pre-cleanup evidence.
|
||||
|
||||
## Execution Policy
|
||||
|
||||
- Present proposed actions with expected impact and risk before high-impact deletion.
|
||||
- Run destructive actions only after confirmation unless the user explicitly requests immediate cleanup of obvious rebuildable artifacts.
|
||||
- Prefer ownership-aware cleanup commands over raw recursive deletion.
|
||||
- Check for active builds, services, mounts, and open files before deleting their state.
|
||||
- Execute one category at a time and remeasure; stop when the user's space target is met.
|
||||
- Never treat overlapping Nix closure sizes as additive reclaim estimates.
|
||||
|
||||
For Rust artifacts, helper-validated explicit directories literally named `target` are rebuildable. When the user asks to clean Rust targets, inventory them, validate with the helper, delete with `--yes`, and report reclaimed space without repeatedly asking for confirmation.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Establish current pressure and biggest filesystems
|
||||
2. Run easy cleanup wins
|
||||
3. Inventory Rust build artifacts and clean the right kind of target
|
||||
4. Investigate remaining heavy directories with `ncdu`/`du`
|
||||
5. Investigate `/nix/store` roots when large toolchains still persist
|
||||
6. Summarize reclaimed space and next candidate actions
|
||||
7. Record new machine-specific ignore paths, Rust roots, or cleanup patterns in this skill
|
||||
1. Read the assessment and record current free space.
|
||||
2. Rank candidates by reclaim, reversibility, user impact, and confidence.
|
||||
3. Check active use and run dry-run modes where available.
|
||||
4. Execute the smallest sufficient cleanup round.
|
||||
5. Remeasure free space and verify affected services or builds.
|
||||
6. Produce a post-cleanup reusable snapshot after material changes.
|
||||
7. Report actions, actual reclaim, residual candidates, and artifact paths.
|
||||
|
||||
## Step 1: Baseline
|
||||
|
||||
Run a quick baseline before deleting anything:
|
||||
## Baseline and Activity Checks
|
||||
|
||||
```bash
|
||||
df -h /
|
||||
df -h /home
|
||||
df -h /nix
|
||||
ps aux | rg '(cargo|rustc|nix|docker|podman|gitea-runner)' || true
|
||||
```
|
||||
|
||||
Optionally add a quick home-level size snapshot:
|
||||
|
||||
```bash
|
||||
du -xh --max-depth=1 "$HOME" 2>/dev/null | sort -h
|
||||
```
|
||||
|
||||
## Step 2: Easy Wins
|
||||
|
||||
Use these first when the user wants fast, low-effort reclaiming:
|
||||
Use `lsof`, service status, repository state, and worktree age when a candidate could be active. A directory that looks stale by mtime may still belong to a running build or concurrent agent.
|
||||
|
||||
## Low-Risk Automated Wins
|
||||
|
||||
Use dry-run or non-interactive privilege checks first:
|
||||
|
||||
```bash
|
||||
nix-collect-garbage -d
|
||||
sudo -n nix-collect-garbage -d
|
||||
sudo -n docker system prune -a
|
||||
sudo -n podman system prune -a
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Add `--volumes` only when the user approves deleting unused volumes.
|
||||
- Re-check free space after each command to show impact.
|
||||
- Prefer `sudo -n` first so cleanup runs fail fast instead of hanging on password prompts.
|
||||
- If root is still tight after these, run app cache cleaners before proposing raw `rm -rf`:
|
||||
|
||||
- Plain `nix-collect-garbage -d` may work through the daemon when `sudo` is unavailable.
|
||||
- Add container `--volumes` only with explicit approval to delete unused volumes.
|
||||
- Recheck `df` after each command.
|
||||
- Fail fast on unavailable privilege; do not hang on a password prompt.
|
||||
|
||||
Use application-aware cache cleaners when the assessment identifies those caches:
|
||||
|
||||
```bash
|
||||
uv cache clean
|
||||
@@ -70,185 +78,68 @@ yarn cache clean
|
||||
npm cache clean --force
|
||||
```
|
||||
|
||||
## Step 3: Rust Build Artifact Cleanup
|
||||
Expect dependency redownloads. Close applications before clearing their profile caches.
|
||||
|
||||
Do not start with a blind `find ~ -name target` or with hard-coded roots that may miss worktrees. Inventory explicit `target/` directories first using the bundled helper and the machine-specific root list in `references/rust-target-roots.txt`.
|
||||
## Rust Build Artifacts
|
||||
|
||||
Inventory the biggest candidates:
|
||||
Inventory across configured project and worktree roots:
|
||||
|
||||
```bash
|
||||
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 500M --limit 30
|
||||
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 500M --limit 30
|
||||
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 1G --older-than 14 --output tsv
|
||||
```
|
||||
|
||||
Focus on stale targets only:
|
||||
|
||||
```bash
|
||||
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 1G --older-than 14 --output tsv
|
||||
```
|
||||
|
||||
Use `cargo-sweep` when the repo is still active and you want age/toolchain-aware cleanup inside a workspace:
|
||||
For active workspaces, prefer age/toolchain-aware cleanup:
|
||||
|
||||
```bash
|
||||
nix run nixpkgs#cargo-sweep -- sweep -d -r -t 30 <workspace-root>
|
||||
nix run nixpkgs#cargo-sweep -- sweep -r -t 30 <workspace-root>
|
||||
nix run nixpkgs#cargo-sweep -- sweep -d -r -i <workspace-root>
|
||||
nix run nixpkgs#cargo-sweep -- sweep -r -i <workspace-root>
|
||||
```
|
||||
|
||||
Use direct `target/` deletion when inventory shows a discrete stale directory, especially for inactive repos or project-local worktrees. The helper only deletes explicit paths named `target` that are beneath configured roots and a Cargo project:
|
||||
For inactive repos and stale worktrees, use guarded direct deletion:
|
||||
|
||||
```bash
|
||||
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target
|
||||
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target --yes
|
||||
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target
|
||||
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target --yes
|
||||
```
|
||||
|
||||
Recommended sequence:
|
||||
The helper rejects symlinks, paths outside configured roots, non-`target` names, and directories without a Cargo project above them. Re-run inventory after each round. Preserve any target involved in an active Cargo/Rust process.
|
||||
|
||||
1. Run `rust_target_dirs.py list` to see the largest `target/` directories across `~/Projects`, `~/org`, `~/dotfiles`, and other configured roots.
|
||||
2. For active repos, prefer `cargo-sweep` from the workspace root.
|
||||
3. For inactive repos, abandoned branches, and `.worktrees/*/target`, prefer guarded direct deletion of the explicit `target/` directory.
|
||||
4. Re-run the list command after each deletion round to show reclaimed space.
|
||||
If `python` is unavailable, run the helper with `nix run nixpkgs#python3 -- ...`. If a stale `~/.cargo/bin/cargo-sweep` shadows the NixOS binary, inspect `type -a cargo-sweep` before use.
|
||||
|
||||
Machine-specific note:
|
||||
## Nix Remediation
|
||||
|
||||
- Project-local `.worktrees/*/target` directories are common cleanup wins on this machine and are easy to miss with the old hard-coded workflow.
|
||||
Do not delete `/nix/store` paths directly. Act on retaining roots identified by the assessment:
|
||||
|
||||
## Step 4: Investigation with `ncdu` and `du`
|
||||
- Remove stale project `.direnv` directories or `flake-profile-*` roots only after verifying the project/worktree is inactive.
|
||||
- Remove stale `result*` symlinks when the referenced build output no longer needs to remain rooted.
|
||||
- Remove obsolete system generations through Nix tooling.
|
||||
- Run garbage collection after roots are removed and measure actual free-space change.
|
||||
- Run `nix-store --optimise` when duplicate physical files are a demonstrated opportunity; it can be I/O intensive.
|
||||
|
||||
Avoid mounted or remote filesystems when profiling space. Load ignore patterns from `references/ignore-paths.md`.
|
||||
Use `nix_store_audit --top 30` before and after a root-removal campaign. Treat its uniquely collectible estimates as planning evidence, not a promise of exact filesystem reclaim.
|
||||
|
||||
Use one-filesystem scans to avoid crossing mounts:
|
||||
## Service and Cache Remediation
|
||||
|
||||
```bash
|
||||
ncdu -x "$HOME"
|
||||
sudo ncdu -x /
|
||||
```
|
||||
Read `references/cleanup-patterns.md` before touching private service state, runner caches, logs, or ambiguous project artifacts. Preserve configuration, registration, credentials, SSH material, and active workspaces.
|
||||
|
||||
When excluding known noisy mountpoints:
|
||||
Prefer service-aware sequences: stop the service, clean only validated cache/work paths, recreate required directories with correct ownership, restart, and verify health.
|
||||
|
||||
```bash
|
||||
ncdu -x --exclude "$HOME/keybase" "$HOME"
|
||||
sudo ncdu -x --exclude /keybase --exclude /var/lib/railbird /
|
||||
```
|
||||
|
||||
If `ncdu` is missing, use:
|
||||
|
||||
```bash
|
||||
nix run nixpkgs#ncdu -- -x "$HOME"
|
||||
```
|
||||
|
||||
For reusable, mount-safe snapshots on this machine, prefer the local wrapper:
|
||||
|
||||
```bash
|
||||
safe_ncdu /
|
||||
sudo -n env HOME=/home/imalison safe_ncdu /
|
||||
safe_ncdu /nix/store
|
||||
safe_ncdu top ~/.cache/ncdu/latest-root.json.zst 30 /home/imalison
|
||||
safe_ncdu open ~/.cache/ncdu/latest-root.json.zst
|
||||
```
|
||||
|
||||
`safe_ncdu` writes compressed ncdu exports under `~/.cache/ncdu`, records the exclude list beside the export, excludes mounted descendants of the scan root, and supports follow-up `top` queries without rescanning.
|
||||
|
||||
For quick, non-blocking triage on very large trees, prefer bounded probes:
|
||||
|
||||
```bash
|
||||
timeout 30s du -xh --max-depth=1 "$HOME/.cache" 2>/dev/null | sort -h
|
||||
timeout 30s du -xh --max-depth=1 "$HOME/.local/share" 2>/dev/null | sort -h
|
||||
```
|
||||
|
||||
Machine-specific heavy hitters seen in practice:
|
||||
|
||||
- `~/.cache/uv` can exceed 20G and is reclaimable with `uv cache clean`.
|
||||
- `~/.cache/pypoetry` can exceed 7G across artifacts, repository cache, and virtualenvs; inspect first, then use Poetry cache commands or targeted virtualenv removal.
|
||||
- `~/.cache/google-chrome` can exceed 8G across multiple Chrome profiles; close Chrome before clearing profile cache directories.
|
||||
- `~/.cache/spotify` can exceed 10G; treat as optional app-cache cleanup.
|
||||
- `~/.gradle` can exceed 8G, mostly under `caches/`; prefer Gradle-aware cleanup and expect dependency redownloads.
|
||||
- `~/.local/share/picom/debug.log` can grow past 15G when verbose picom debugging is enabled or crashes leave a stale log behind; if `picom` is not running, deleting or truncating the log is a high-yield low-risk win.
|
||||
- `~/.local/share/Trash` can exceed several GB; empty only with user approval.
|
||||
- `/var/lib/private/gitea-runner` can exceed 50G and is not visible to an unprivileged `ncdu /` scan; use `sudo -n env HOME=/home/imalison safe_ncdu /` when `/var` looks undercounted.
|
||||
- Validated cleanup pattern: stop `gitea-runner-nix.service`, remove cache/work directories under `/var/lib/private/gitea-runner` (`.cache`, `.gradle`, `action-cache-dir`, `workspace`, stale nested `gitea-runner`, and nested `nix/.cache`/`nix/.local`), recreate `action-cache-dir`, `workspace`, and `.cache` owned by `gitea-runner:gitea-runner`, then restart the service.
|
||||
- Preserve registration/config-like files such as `/var/lib/private/gitea-runner/nix/.runner`, `/var/lib/private/gitea-runner/nix/.labels`, `/var/lib/private/gitea-runner/.docker/config.json`, and SSH/Kube material.
|
||||
- `~/Projects/*/target` directories can dominate home usage. Recent example candidates included stale `target/` directories under `scrobble-scrubber`, `http-client-vcr`, `http-client`, `subtr-actor`, `http-types`, `subtr-actor-py`, `sdk`, and `async-h1`.
|
||||
|
||||
## Step 5: `/nix/store` Deep Dive
|
||||
|
||||
When `/nix/store` is still large after GC, inspect root causes instead of deleting random paths.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
nix path-info -Sh /nix/store/* 2>/dev/null | sort -h | tail -n 50
|
||||
nix-store --gc --print-roots
|
||||
```
|
||||
|
||||
Avoid `du -sh /nix/store` as a first diagnostic; it can be very slow on large stores.
|
||||
|
||||
For repeated GHC/Rust toolchain copies:
|
||||
|
||||
```bash
|
||||
nix path-info -Sh /nix/store/* 2>/dev/null | rg '(ghc|rustc|rust-std|cargo)'
|
||||
nix-store --gc --print-roots | rg '(ghc|rust)'
|
||||
```
|
||||
|
||||
Resolve why a path is retained:
|
||||
|
||||
```bash
|
||||
/home/imalison/dotfiles/dotfiles/lib/functions/find_store_path_gc_roots /nix/store/<store-path>
|
||||
nix why-depends <consumer-store-path> <dependency-store-path>
|
||||
```
|
||||
|
||||
Common retention pattern on this machine:
|
||||
|
||||
- Many `.direnv/flake-profile-*` symlinks under `~/Projects` and worktrees keep `nix-shell-env`/`ghc-shell-*` roots alive.
|
||||
- Old taffybar constellation repos under `~/Projects` can pin large Haskell closures through `.direnv` and `result` symlinks. Deleting `gtk-sni-tray`, `status-notifier-item`, `dbus-menu`, `dbus-hslogger`, and `gtk-strut` and then rerunning `nix-collect-garbage -d` reclaimed about 11G of store data in one validated run.
|
||||
- `find_store_path_gc_roots` is especially useful for proving GHC retention: many large `ghc-9.10.3-with-packages` paths are unique per project, while the base `ghc-9.10.3` and docs paths are shared.
|
||||
- NixOS system generations and a repo-root `nixos/result` symlink can pin multiple Android Studio and Android SDK versions. Check `/nix/var/nix/profiles/system-*-link`, `/run/current-system`, `/run/booted-system`, and `~/dotfiles/nixos/result` before assuming Android paths are pinned by project shells.
|
||||
- `~/Projects/railbird-mobile/.direnv/flake-profile-*` can pin large Android SDK system images. Removing stale direnv profiles there is a more targeted first step than deleting Android store paths directly.
|
||||
- For a repeatable `/nix/store` `ncdu` snapshot without driving the TUI, export and inspect it:
|
||||
|
||||
```bash
|
||||
ncdu -0 -x -c -o /tmp/nix-store.ncdu.json.zst /nix/store
|
||||
zstdcat /tmp/nix-store.ncdu.json.zst | jq 'def sumd: if type=="array" then ((.[0].dsize // 0) + ([.[1:][] | sumd] | add // 0)) elif type=="object" then (.dsize // 0) else 0 end; .[3] | sumd'
|
||||
```
|
||||
|
||||
- `nix-store --gc --print-dead` plus the Nix SQLite database is a fast way to estimate immediate GC wins before deleting anything:
|
||||
|
||||
```bash
|
||||
nix-store --gc --print-dead > /tmp/nix-dead-paths.txt
|
||||
printf '%s\n' '.mode list' '.separator |' 'create temp table dead(path text);' \
|
||||
'.import /tmp/nix-dead-paths.txt dead' \
|
||||
'select count(*), sum(narSize) from ValidPaths join dead using(path);' \
|
||||
| nix shell nixpkgs#sqlite --command sqlite3 /nix/var/nix/db/db.sqlite
|
||||
```
|
||||
|
||||
- Quantify before acting:
|
||||
|
||||
```bash
|
||||
find ~/Projects -type l -path '*/.direnv/flake-profile-*' | wc -l
|
||||
find ~/Projects -type d -name .direnv | wc -l
|
||||
nix-store --gc --print-roots | rg '/\\.direnv/flake-profile-' | awk -F' -> ' '{print $1 \"|\" $2}' \
|
||||
| while IFS='|' read -r root target; do \
|
||||
nix-store -qR \"$target\" | rg '^/nix/store/.+-ghc-[0-9]'; \
|
||||
done | sort | uniq -c | sort -nr | head
|
||||
```
|
||||
|
||||
- If counts are high and the projects are inactive, propose targeted `.direnv` cleanup for user confirmation.
|
||||
- 2026-07-10 `railbird-sf` incident: K3s reported `DiskPressure` even with tens of GiB free because its container `imagefs` shares `/`, kubelet image GC used the default 85% high-water mark, and the K3s config overrode only `nodefs` eviction thresholds. `crictl imagefsinfo` showed only ~677M of images, so image GC could not reclaim its requested ~149G and repeatedly evicted application pods. Set matching `imagefs.available` values alongside `nodefs.available` in `eviction-hard`, `eviction-soft`, and `eviction-soft-grace-period`; verify via K3s eviction-manager logs rather than trusting `df` alone.
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Do not delete user files directly unless explicitly requested.
|
||||
- Prefer cleanup tools that understand ownership/metadata (`nix`, `docker`, `podman`, `cargo-sweep`) over `rm -rf`.
|
||||
- For Rust build artifacts, deleting an explicit directory literally named `target` is acceptable when it is discovered by the bundled helper; Cargo will rebuild it.
|
||||
- Present a concise “proposed actions” list before high-impact deletes.
|
||||
- If uncertain whether data is needed, stop at investigation and ask.
|
||||
- Do not delete user documents, source code, credentials, volumes, or unknown data without explicit approval.
|
||||
- Never delete a path solely because its name resembles a cache or build directory.
|
||||
- Do not clean targets used by running builds or concurrent agents.
|
||||
- Preserve dirty worktrees and registration/configuration files.
|
||||
- Stop when evidence is insufficient and return to `disk-space-assessment`.
|
||||
|
||||
## Learning Loop (Required)
|
||||
## Learning Loop
|
||||
|
||||
Treat this skill as a living playbook.
|
||||
After each cleanup:
|
||||
|
||||
After each disk cleanup task:
|
||||
|
||||
1. Add newly discovered mountpoints or directories to ignore in `references/ignore-paths.md`.
|
||||
2. Add newly discovered Rust repo roots in `references/rust-target-roots.txt`.
|
||||
3. Add validated command patterns or caveats discovered during the run to this `SKILL.md`.
|
||||
4. Keep instructions practical and machine-specific; remove stale guidance.
|
||||
1. Add stable, validated remediation knowledge to `references/cleanup-patterns.md`.
|
||||
2. Add new Rust repository roots to `references/rust-target-roots.txt`.
|
||||
3. Update assessment-side diagnostic patterns separately; do not turn this skill into a usage-history log.
|
||||
4. Remove stale guidance and keep automation guarded and reproducible.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
interface:
|
||||
display_name: "Disk Space Cleanup"
|
||||
short_description: "Find safe disk-space wins on NixOS hosts"
|
||||
short_description: "Automate safe, measured disk-space cleanup"
|
||||
default_prompt: "Use $disk-space-cleanup to act on assessed disk-space candidates and verify reclaimed space."
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Validated Cleanup Patterns
|
||||
|
||||
Use only after a current assessment confirms the path is significant.
|
||||
|
||||
## Rust and Worktrees
|
||||
|
||||
- Stale Cargo-backed targets in top-level projects, `.worktrees/*/target`, and `.claude/worktrees/*/target` have repeatedly reclaimed 25–100+ GiB.
|
||||
- Preserve targets used by active builds. Concurrent agents may recreate deleted outputs immediately.
|
||||
- `hypr-workspace-history/target` has been a removable Rust-style cache without a nearby `Cargo.toml`, so the guarded helper rejects it; inspect manually.
|
||||
- Preserve `~/Projects/Hyprland/src/layout/target`, which is source code rather than build output.
|
||||
- Clean registered git worktrees only after preserving dirty ones. Stale nixpkgs worktree `result` symlinks may pin Nix closures.
|
||||
|
||||
## Nix Roots and Store Optimization
|
||||
|
||||
- Removing conservatively stale `.direnv` roots followed by `nix-store --gc` has reclaimed about 15 GiB in a validated run.
|
||||
- With `auto-optimise-store = false`, `nix-store --optimise` once reclaimed about 60 GiB by hardlinking duplicate store files. Measure current conditions before assuming similar impact.
|
||||
- Old taffybar constellation repos and `railbird-mobile` direnv profiles have pinned large Haskell or Android closures.
|
||||
- NixOS generations and `/srv/dotfiles/nixos/result` can pin Android Studio and SDK versions.
|
||||
- A stale rustup toolchain can break `cargo-sweep --installed`; remove it only after confirming the toolchain is unusable and unwanted.
|
||||
|
||||
## Gitea Runner
|
||||
|
||||
For `/var/lib/private/gitea-runner`, a validated sequence is:
|
||||
|
||||
1. Stop `gitea-runner-nix.service`.
|
||||
2. Remove only assessed cache/work directories such as `.cache`, `.gradle`, `action-cache-dir`, `workspace`, stale nested `gitea-runner`, and nested `nix/.cache` or `nix/.local`.
|
||||
3. Recreate `action-cache-dir`, `workspace`, and `.cache` owned by `gitea-runner:gitea-runner`.
|
||||
4. Restart the service and verify it.
|
||||
|
||||
Preserve `.runner`, `.labels`, `.docker/config.json`, SSH material, Kubernetes material, and other registration/configuration state.
|
||||
|
||||
## Logs and App Caches
|
||||
|
||||
- Delete or truncate `~/.local/share/picom/debug.log` only after confirming picom is not actively writing it and correcting the verbose logging cause.
|
||||
- Empty Trash only with explicit approval.
|
||||
- Close Chrome, Spotify, Gradle daemons, and similar applications before targeted cache cleanup.
|
||||
|
||||
## nix-homebrew Tap Recovery
|
||||
|
||||
- On macOS, `darwin-rebuild switch` can report that an existing Homebrew tap directory is in the way when legacy `homebrew-cask` or `homebrew-core` checkouts predate nix-homebrew management.
|
||||
- For a root-owned cask tap, restore user ownership and run `brew untap --force homebrew/cask`.
|
||||
- Homebrew refuses to untap core. Move the legacy core checkout out of the expected tap path, then rerun the switch so nix-homebrew recreates both managed tap links. Verify the subsequent Homebrew bundle and Darwin activation complete.
|
||||
|
||||
## Severe Pressure
|
||||
|
||||
When only megabytes remain, run the smallest safe cleanup first. Build tools and SQLite may fail opaquely before reporting `ENOSPC`. Avoid commands that need substantial temporary disk space until a safe margin has been restored.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Ignore Paths for Disk Investigation
|
||||
|
||||
Use this file to track mountpoints or directories that should be excluded from `ncdu`/`du` scans because they are remote, special-purpose, or noisy.
|
||||
|
||||
## Known Ignores
|
||||
|
||||
- `$HOME/keybase`
|
||||
- `$HOME/.cache/keybase`
|
||||
- `$HOME/.local/share/keybase`
|
||||
- `$HOME/.config/keybase`
|
||||
- `/keybase`
|
||||
- `/var/lib/railbird`
|
||||
- `/run/user/*/doc` (FUSE portal mount; machine-specific example observed: `/run/user/1004/doc`)
|
||||
|
||||
## Discovery Commands
|
||||
|
||||
List mounted filesystems and spot special mounts:
|
||||
|
||||
```bash
|
||||
findmnt -rn -o TARGET,FSTYPE,SOURCE
|
||||
```
|
||||
|
||||
Target likely remote/special mounts:
|
||||
|
||||
```bash
|
||||
findmnt -rn -o TARGET,FSTYPE,SOURCE | rg '(keybase|fuse|rclone|s3|railbird)'
|
||||
```
|
||||
|
||||
## Maintenance Rule
|
||||
|
||||
When a disk cleanup run encounters a mount or path that should be ignored in future runs, add it here immediately with a short note.
|
||||
@@ -3,4 +3,5 @@
|
||||
|
||||
/home/imalison/Projects
|
||||
/home/imalison/org
|
||||
/home/imalison/dotfiles
|
||||
/home/imalison/.codex/worktrees
|
||||
/srv/dotfiles
|
||||
|
||||
@@ -57,7 +57,7 @@ Two approaches depending on the sender:
|
||||
|
||||
**For emails with unsubscribe links:**
|
||||
- Read the email via `gws gmail` to find the unsubscribe URL (usually at bottom of email body)
|
||||
- Navigate to the URL with Chrome DevTools MCP
|
||||
- Navigate to the URL in the browser via the Chrome extension
|
||||
- Take a snapshot, find the confirmation button/checkbox
|
||||
- Click through to complete the unsubscribe
|
||||
- Verify the confirmation page
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Use when investigating production org-agenda-api state, testing end
|
||||
|
||||
## Overview
|
||||
|
||||
Access the production org-agenda-api instance at https://colonelpanic-org-agenda.fly.dev/ for debugging, testing, or verification.
|
||||
Access the production org-agenda-api instance at https://org-agenda-api.rocket-sense.duckdns.org/ for debugging, testing, or verification.
|
||||
|
||||
## Credentials
|
||||
|
||||
@@ -20,10 +20,10 @@ Username is currently `imalison`.
|
||||
|
||||
## Quick Access with just
|
||||
|
||||
This repo includes a `justfile` under `~/dotfiles/org-agenda-api` with pre-configured commands:
|
||||
This repo includes a `justfile` under `/srv/dotfiles/org-agenda-api` with pre-configured commands:
|
||||
|
||||
```bash
|
||||
cd ~/dotfiles/org-agenda-api
|
||||
cd /srv/dotfiles/org-agenda-api
|
||||
just health
|
||||
just get-all-todos
|
||||
just get-todays-agenda
|
||||
|
||||
@@ -11,16 +11,16 @@ HTTP API for org-mode agenda data. Use this skill when you need to query or modi
|
||||
|
||||
Get credentials from pass:
|
||||
```bash
|
||||
pass show colonelpanic-org-agenda.fly.dev
|
||||
pass show org-agenda-api-imalison
|
||||
```
|
||||
|
||||
Returns: password on first line, then `user:` and `url:` fields.
|
||||
Returns: password on first line. The username is currently `imalison`.
|
||||
|
||||
**Note:** The `url` field in pass may be outdated. Use the base URL below.
|
||||
|
||||
## Base URL
|
||||
|
||||
`https://colonelpanic-org-agenda.fly.dev`
|
||||
`https://org-agenda-api.rocket-sense.duckdns.org`
|
||||
|
||||
All requests use Basic Auth with the credentials from pass.
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
name: password-reset
|
||||
description: Use when the user wants to reset or rotate a website or service password end-to-end, including finding the right `pass` entry, generating a new password with `xkcdpassgen`, retrieving reset emails through `gws gmail` or a local mail CLI, completing the reset in the browser with Chrome DevTools MCP, and updating the password store safely without losing entry metadata.
|
||||
description: Use when the user wants to reset or rotate a website or service password end-to-end, including finding the right `pass` entry, generating a new password with `xkcdpassgen`, retrieving reset emails through `gws gmail` or a local mail CLI, completing the reset in the browser via the Chrome extension, and updating the password store safely without losing entry metadata.
|
||||
---
|
||||
|
||||
# Password Reset
|
||||
|
||||
## Overview
|
||||
|
||||
Handle password resets end-to-end. Prefer `gws gmail` for reset-email retrieval, Chrome DevTools MCP for website interaction, and the local `xkcdpassgen` helper for password generation.
|
||||
Handle password resets end-to-end. Prefer `gws gmail` for reset-email retrieval, the Chrome browser extension for website interaction, and the local `xkcdpassgen` helper for password generation.
|
||||
|
||||
## Tool Priorities
|
||||
|
||||
- Prefer `gws gmail` over opening Gmail in the browser.
|
||||
- If `gws` is unavailable, use an installed Gmail CLI or IMAP-based mail tool if one exists locally. Inspect the environment first instead of guessing command names.
|
||||
- Prefer Chrome DevTools MCP for all browser interaction.
|
||||
- Prefer the Chrome browser extension for all browser interaction.
|
||||
- Use `pass find` and `pass show` before asking the user for credentials or account details.
|
||||
|
||||
## Password Generation
|
||||
@@ -73,14 +73,14 @@ If the site rejects the password because of policy constraints, keep the canonic
|
||||
2. Run `pass find <service>` and inspect likely matches with `pass show`.
|
||||
3. Capture existing metadata before generating a new password.
|
||||
4. Generate the candidate password into a temporary `pass` entry with `xkcdpassgen`.
|
||||
5. Start the reset flow in Chrome DevTools MCP:
|
||||
5. Start the reset flow in the browser via the Chrome extension:
|
||||
- navigate to the login or account page
|
||||
- use the site's "forgot password" flow, or
|
||||
- sign in and navigate to security settings if the user asked for a rotation rather than a reset
|
||||
6. Use `gws gmail` to retrieve the reset email when needed:
|
||||
- search recent mail by sender domain, subject, or reset-related keywords
|
||||
- open the message and extract the reset link
|
||||
- navigate to that link in Chrome DevTools MCP
|
||||
- navigate to that link in the browser via the Chrome extension
|
||||
7. Fill the new password from the temporary `pass` entry and complete the form.
|
||||
8. Verify success:
|
||||
- confirmation page, or
|
||||
@@ -99,14 +99,14 @@ If `gws` is unavailable, use an installed Gmail CLI or local mail helper only as
|
||||
|
||||
## Browser Guidance
|
||||
|
||||
Use Chrome DevTools MCP to complete the reset flow directly:
|
||||
Use the Chrome browser extension to complete the reset flow directly:
|
||||
|
||||
- navigate to the reset or security page
|
||||
- take snapshots to identify the relevant inputs and buttons
|
||||
- click, fill, and submit through the site UI
|
||||
- verify the success state before updating the canonical `pass` entry
|
||||
|
||||
Prefer MCP interaction over describing steps for the user to perform manually.
|
||||
Prefer driving the browser directly over describing steps for the user to perform manually.
|
||||
|
||||
## Credentials And Account Data
|
||||
|
||||
|
||||
@@ -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,19 +46,19 @@ 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 /srv/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
|
||||
|
||||
```bash
|
||||
cd ~/dotfiles/nixos && just switch
|
||||
cd /srv/dotfiles/nixos && just switch
|
||||
```
|
||||
|
||||
If taffybar seems stale after a rebuild, check whether the flake.lock at each layer actually points at the expected revision — a missed cascade step is the usual cause.
|
||||
|
||||
6
dotfiles/claude/.gitignore
vendored
6
dotfiles/claude/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!CLAUDE.md
|
||||
!settings.json
|
||||
!settings.local.json
|
||||
!settings.local.json.example
|
||||
@@ -1 +0,0 @@
|
||||
../agents/AGENTS.md
|
||||
1
dotfiles/claude/CLAUDE.md
Normal file
1
dotfiles/claude/CLAUDE.md
Normal file
@@ -0,0 +1 @@
|
||||
@~/.agents/AGENTS.md
|
||||
16
dotfiles/claude/agents/codex-delegator.md
Normal file
16
dotfiles/claude/agents/codex-delegator.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: codex-delegator
|
||||
description: Ask Codex for one independent read-only opinion
|
||||
tools: Bash
|
||||
model: opus
|
||||
effort: medium
|
||||
maxTurns: 3
|
||||
---
|
||||
|
||||
UTF-8 base64-encode the complete task yourself without using a tool. Verify that the payload contains only `A-Z`, `a-z`, `0-9`, `+`, `/`, and valid terminal `=` padding. Then make exactly one Bash tool call:
|
||||
|
||||
```bash
|
||||
ask-codex --base64 '<payload>'
|
||||
```
|
||||
|
||||
Never place raw task text in shell source. Return the command's stdout verbatim. Do not investigate, reason about the task, edit files, call any other command, or call or delegate to any other agent.
|
||||
@@ -16,5 +16,8 @@
|
||||
"agent-browser@agent-browser": true
|
||||
},
|
||||
"effortLevel": "high",
|
||||
"skipDangerousModePermissionPrompt": true
|
||||
"skipDangerousModePermissionPrompt": true,
|
||||
"remoteControlAtStartup": true,
|
||||
"inputNeededNotifEnabled": true,
|
||||
"agentPushNotifEnabled": true
|
||||
}
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
"set -euo pipefail; export GITEA_BASE_URL='https://dev.railbird.ai'; export GITEA_ACCESS_TOKEN=\"$(pass show claude-mcp/gitea-access-token | head -1)\"; exec docker run -i --rm -e GITEA_ACCESS_TOKEN -e GITEA_BASE_URL docker.gitea.com/gitea-mcp-server"
|
||||
]
|
||||
},
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--auto-connect"
|
||||
]
|
||||
},
|
||||
"imap-email": {
|
||||
"command": "bash",
|
||||
"args": [
|
||||
@@ -32,7 +25,6 @@
|
||||
}
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"chrome-devtools",
|
||||
"imap-email"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
|
||||
@@ -21,13 +21,6 @@
|
||||
"docker.gitea.com/gitea-mcp-server"
|
||||
]
|
||||
},
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--auto-connect"
|
||||
]
|
||||
},
|
||||
"imap-email": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "imap-email-mcp"],
|
||||
@@ -36,7 +29,6 @@
|
||||
}
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"chrome-devtools",
|
||||
"imap-email"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
|
||||
1
dotfiles/claude/skills
Symbolic link
1
dotfiles/claude/skills
Symbolic link
@@ -0,0 +1 @@
|
||||
../agents/skills
|
||||
5
dotfiles/codex/.gitignore
vendored
5
dotfiles/codex/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!AGENTS.md
|
||||
!config.toml
|
||||
!skills
|
||||
7
dotfiles/codex/agents/claude-delegator.toml
Normal file
7
dotfiles/codex/agents/claude-delegator.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
name = "claude_delegator"
|
||||
description = "Ask Opus for specialized independent design critique"
|
||||
model = "gpt-5.6-sol"
|
||||
model_reasoning_effort = "medium"
|
||||
developer_instructions = """
|
||||
UTF-8 base64-encode the complete task yourself without using a tool. Verify that the payload contains only `A-Z`, `a-z`, `0-9`, `+`, `/`, and valid terminal `=` padding. Then make exactly one shell tool call: `ask-claude --base64 '<payload>'`. Never place raw task text in shell source. Return the command's stdout verbatim. Do not investigate or reason about the task yourself, edit files, run any other command, or call, spawn, communicate with, or delegate to any other agent.
|
||||
"""
|
||||
@@ -1,156 +1,16 @@
|
||||
model = "gpt-5.5"
|
||||
model = "gpt-5.6-sol"
|
||||
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"
|
||||
|
||||
[mcp_servers.chrome-devtools]
|
||||
command = "npx"
|
||||
args = ["-y", "chrome-devtools-mcp@latest", "--auto-connect"]
|
||||
# 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.observability]
|
||||
command = "npx"
|
||||
args = ["-y", "@google-cloud/observability-mcp"]
|
||||
command = "/usr/bin/env"
|
||||
args = ["PATH=/etc/profiles/per-user/imalison/bin:/run/current-system/sw/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin", "npx", "-y", "@google-cloud/observability-mcp"]
|
||||
|
||||
[mcp_servers.openaiDeveloperDocs]
|
||||
url = "https://developers.openai.com/mcp"
|
||||
@@ -159,16 +19,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 +49,3 @@ enabled = true
|
||||
|
||||
[plugins."browser-use@openai-bundled"]
|
||||
enabled = true
|
||||
|
||||
[tui.model_availability_nux]
|
||||
"gpt-5.5" = 4
|
||||
|
||||
37
dotfiles/config/autorandr/jay-lenovo-with-benq/config
Normal file
37
dotfiles/config/autorandr/jay-lenovo-with-benq/config
Normal file
@@ -0,0 +1,37 @@
|
||||
output HDMI-A-0
|
||||
off
|
||||
output DisplayPort-1-1
|
||||
off
|
||||
output DisplayPort-1-2
|
||||
off
|
||||
output DisplayPort-1-3
|
||||
off
|
||||
output DisplayPort-1-4
|
||||
off
|
||||
output DisplayPort-0
|
||||
crtc 1
|
||||
mode 2560x1440
|
||||
pos 0x0
|
||||
rate 144.00
|
||||
x-prop-colorspace Default
|
||||
x-prop-max_bpc 16
|
||||
x-prop-non_desktop 0
|
||||
x-prop-scaling_mode None
|
||||
x-prop-tearfree auto
|
||||
x-prop-underscan off
|
||||
x-prop-underscan_hborder 0
|
||||
x-prop-underscan_vborder 0
|
||||
output eDP
|
||||
crtc 0
|
||||
mode 2560x1600
|
||||
pos 2560x0
|
||||
primary
|
||||
rate 165.00
|
||||
x-prop-colorspace Default
|
||||
x-prop-max_bpc 16
|
||||
x-prop-non_desktop 0
|
||||
x-prop-scaling_mode None
|
||||
x-prop-tearfree auto
|
||||
x-prop-underscan off
|
||||
x-prop-underscan_hborder 0
|
||||
x-prop-underscan_vborder 0
|
||||
2
dotfiles/config/autorandr/jay-lenovo-with-benq/setup
Normal file
2
dotfiles/config/autorandr/jay-lenovo-with-benq/setup
Normal file
@@ -0,0 +1,2 @@
|
||||
DisplayPort-0 00ffffffffffff0009d1767f45540000281d0103803c22782a9325ad4f44a9260d5054a56b80d1fcd1e8d1c0b300a9c08180810081c0f8e300a0a0a032500820980455502100001a000000ff0033414b30313335343031390a20000000fd0028901ede3c000a202020202020000000fc0042656e5120455832373830510a0174020350f1515d5e5f60613f40101f22212004131203012309070783010000e200cf6d030c001000383c20006001020367d85dc401788003681a000001012890e6e305c301e40f180000e60605016262216fc200a0a0a055503020350055502100001e565e00a0a0a029502f20350055502100001a0000000000000000000000bf
|
||||
eDP 00ffffffffffff0009e59b0a000000001c1e0104b5221578037ce5a4554c9f260f5054000000010101010101010101010101010101016b6e00a0a04084603020360058d71000001a000000fd0c3ca51f1f4e010a202020202020000000fe00424f452043510a202020202020000000fe004e4531363051444d2d4e59310a02d502031d00e3058000e60605016a6a246d1a000002033ca500046a246a240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff7013790000030114a52f0185ff099f002f001f003f0683000200050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e90
|
||||
@@ -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 = hypr-screensaver start
|
||||
on-resume = hypr-screensaver stop
|
||||
}
|
||||
|
||||
@@ -1,562 +0,0 @@
|
||||
# Hyprland Configuration
|
||||
# XMonad-like dynamic tiling using hy3 plugin
|
||||
# Based on XMonad configuration from xmonad.hs
|
||||
|
||||
# =============================================================================
|
||||
# PLUGINS (Hyprland pinned to 0.53.0 to match hy3)
|
||||
# =============================================================================
|
||||
# Load the plugin before parsing keybinds/layouts that depend on it
|
||||
plugin = /run/current-system/sw/lib/libhy3.so
|
||||
plugin = /run/current-system/sw/lib/libhyprexpo.so
|
||||
|
||||
# =============================================================================
|
||||
# MONITORS
|
||||
# =============================================================================
|
||||
monitor=,preferred,auto,1
|
||||
|
||||
# =============================================================================
|
||||
# PROGRAMS
|
||||
# =============================================================================
|
||||
$terminal = ghostty --gtk-single-instance=false
|
||||
$fileManager = dolphin
|
||||
$menu = rofi -show drun -show-icons
|
||||
$runMenu = rofi -show run
|
||||
|
||||
# =============================================================================
|
||||
# ENVIRONMENT VARIABLES
|
||||
# =============================================================================
|
||||
env = XCURSOR_SIZE,24
|
||||
env = QT_QPA_PLATFORMTHEME,qt5ct
|
||||
# Used by ~/.config/hypr/scripts/* to keep workspace IDs bounded.
|
||||
env = HYPR_MAX_WORKSPACE,9
|
||||
|
||||
# =============================================================================
|
||||
# INPUT CONFIGURATION
|
||||
# =============================================================================
|
||||
input {
|
||||
kb_layout = us
|
||||
kb_variant =
|
||||
kb_model =
|
||||
kb_options =
|
||||
kb_rules =
|
||||
|
||||
follow_mouse = 1
|
||||
|
||||
touchpad {
|
||||
natural_scroll = no
|
||||
}
|
||||
|
||||
sensitivity = 0
|
||||
}
|
||||
|
||||
# Cursor warping behavior
|
||||
cursor {
|
||||
persistent_warps = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# GENERAL SETTINGS
|
||||
# =============================================================================
|
||||
general {
|
||||
gaps_in = 5
|
||||
gaps_out = 10
|
||||
border_size = 0
|
||||
col.active_border = rgba(edb443ee) rgba(33ccffee) 45deg
|
||||
col.inactive_border = rgba(595959aa)
|
||||
|
||||
# Use hy3 layout for XMonad-like dynamic tiling
|
||||
layout = hy3
|
||||
|
||||
allow_tearing = false
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DECORATION
|
||||
# =============================================================================
|
||||
decoration {
|
||||
rounding = 5
|
||||
|
||||
blur {
|
||||
enabled = true
|
||||
size = 3
|
||||
passes = 1
|
||||
}
|
||||
|
||||
# Fade inactive windows (like XMonad's fadeInactive)
|
||||
active_opacity = 1.0
|
||||
inactive_opacity = 0.9
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# ANIMATIONS
|
||||
# =============================================================================
|
||||
animations {
|
||||
enabled = yes
|
||||
|
||||
# Hyprland supports bezier curves, not true spring physics.
|
||||
# Use a mild overshoot plus GNOME-like window animation style.
|
||||
bezier = overshoot, 0.05, 0.9, 0.1, 1.1
|
||||
bezier = smoothOut, 0.36, 1, 0.3, 1
|
||||
bezier = smoothInOut, 0.42, 0, 0.58, 1
|
||||
bezier = linear, 0, 0, 1, 1
|
||||
|
||||
# SPEED is in deciseconds (e.g. 6 == 600ms).
|
||||
animation = windows, 1, 6, overshoot, gnomed
|
||||
animation = windowsIn, 1, 6, overshoot, gnomed
|
||||
animation = windowsOut, 1, 5, smoothInOut, gnomed
|
||||
animation = windowsMove, 1, 6, smoothOut
|
||||
animation = border, 0
|
||||
animation = borderangle, 0
|
||||
animation = fade, 1, 5, smoothOut
|
||||
animation = workspaces, 1, 6, smoothOut, slidefade 15%
|
||||
animation = specialWorkspace, 1, 6, smoothOut, slidevert
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MASTER LAYOUT CONFIGURATION
|
||||
# =============================================================================
|
||||
master {
|
||||
new_status = slave
|
||||
mfact = 0.5
|
||||
orientation = left
|
||||
}
|
||||
|
||||
# Dwindle layout (alternative - binary tree like i3)
|
||||
dwindle {
|
||||
pseudotile = yes
|
||||
preserve_split = yes
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# WORKSPACE RULES (SMART GAPS)
|
||||
# =============================================================================
|
||||
# Replace no_gaps_when_only (removed in newer Hyprland)
|
||||
# Remove gaps when there's only one visible tiled window (ignore special workspaces)
|
||||
workspace = w[tv1]s[false], gapsout:0, gapsin:0
|
||||
workspace = f[1]s[false], gapsout:0, gapsin:0
|
||||
|
||||
# Group/tabbed window configuration (built-in alternative to hy3 tabs)
|
||||
group {
|
||||
col.border_active = rgba(edb443ff)
|
||||
col.border_inactive = rgba(091f2eff)
|
||||
|
||||
groupbar {
|
||||
enabled = true
|
||||
font_size = 12
|
||||
height = 22
|
||||
col.active = rgba(edb443ff)
|
||||
col.inactive = rgba(091f2eff)
|
||||
text_color = rgba(091f2eff)
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HY3/HYPREXPO PLUGIN CONFIG
|
||||
# =============================================================================
|
||||
plugin {
|
||||
hy3 {
|
||||
# Disable autotile to get XMonad-like manual control
|
||||
autotile {
|
||||
enable = false
|
||||
}
|
||||
|
||||
# Tab configuration
|
||||
tabs {
|
||||
height = 22
|
||||
padding = 6
|
||||
render_text = true
|
||||
text_font = "Sans"
|
||||
text_height = 10
|
||||
text_padding = 3
|
||||
col.active = rgba(edb443ff)
|
||||
col.inactive = rgba(091f2eff)
|
||||
col.urgent = rgba(ff0000ff)
|
||||
col.text.active = rgba(091f2eff)
|
||||
col.text.inactive = rgba(ffffffff)
|
||||
col.text.urgent = rgba(ffffffff)
|
||||
}
|
||||
}
|
||||
|
||||
hyprexpo {
|
||||
# Always include workspace 1 in the overview grid
|
||||
workspace_method = first 1
|
||||
# Only show workspaces with windows
|
||||
skip_empty = true
|
||||
# Show numeric workspace labels in the expo grid
|
||||
show_workspace_numbers = true
|
||||
# 3 columns -> 3x3 grid when 9 workspaces are visible
|
||||
columns = 3
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MISC
|
||||
# =============================================================================
|
||||
misc {
|
||||
force_default_wallpaper = 0
|
||||
disable_hyprland_logo = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# BINDS OPTIONS
|
||||
# =============================================================================
|
||||
binds {
|
||||
# Keep workspace history so "previous" can toggle back reliably.
|
||||
allow_workspace_cycles = true
|
||||
workspace_back_and_forth = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# WINDOW RULES
|
||||
# =============================================================================
|
||||
# Float dialogs
|
||||
windowrule = match:class ^()$, match:title ^()$, float on
|
||||
windowrule = match:title ^(Picture-in-Picture)$, float on
|
||||
windowrule = match:title ^(Open File)$, float on
|
||||
windowrule = match:title ^(Save File)$, float on
|
||||
windowrule = match:title ^(Confirm)$, float on
|
||||
|
||||
# Rumno OSD/notifications: treat as an overlay, not a "real" managed window.
|
||||
# (Matches both class and title because rumno may set either depending on backend.)
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, float on
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, pin on
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, center on
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, decorate off
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, no_shadow on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, float on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, pin on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, center on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, decorate off
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, no_shadow on
|
||||
|
||||
# Scratchpad sizing handled by hyprscratch exec rules (see hyprland.nix)
|
||||
# Using hyprscratch rules instead of windowrule to avoid affecting child windows (e.g. Slack meets)
|
||||
|
||||
# =============================================================================
|
||||
# KEY BINDINGS
|
||||
# =============================================================================
|
||||
|
||||
# Modifier keys
|
||||
$mainMod = SUPER
|
||||
$modAlt = SUPER ALT
|
||||
$hyper = SUPER CTRL ALT
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Program Launching
|
||||
# -----------------------------------------------------------------------------
|
||||
bind = $mainMod, P, exec, $menu
|
||||
bind = $mainMod SHIFT, P, exec, $runMenu
|
||||
bind = $mainMod SHIFT, Return, exec, $terminal
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Overview (Hyprexpo)
|
||||
# -----------------------------------------------------------------------------
|
||||
bind = $mainMod, TAB, hyprexpo:expo, toggle
|
||||
bind = $mainMod SHIFT, TAB, hyprexpo:expo, bring
|
||||
bind = $mainMod, Q, killactive,
|
||||
bind = $mainMod SHIFT, C, killactive,
|
||||
bind = $mainMod SHIFT, Q, exit,
|
||||
# Emacs-everywhere (like XMonad's emacs-everywhere)
|
||||
bind = $mainMod, E, exec, emacsclient --eval '(emacs-everywhere)'
|
||||
bind = $mainMod, V, exec, wl-paste | xdotool type --file -
|
||||
|
||||
# Chrome/Browser (raise or spawn like XMonad's bindBringAndRaise)
|
||||
bind = $modAlt, C, exec, ~/.config/hypr/scripts/raise-or-run.sh google-chrome google-chrome-stable
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SCRATCHPADS (managed by hyprscratch daemon with auto-dismiss)
|
||||
# -----------------------------------------------------------------------------
|
||||
bind = $modAlt, E, exec, hyprscratch toggle element
|
||||
bind = $modAlt, G, exec, hyprscratch toggle gmail
|
||||
bind = $modAlt, H, exec, hyprscratch toggle htop
|
||||
bind = $modAlt, M, exec, hyprscratch toggle messages
|
||||
bind = $modAlt, K, exec, hyprscratch toggle slack
|
||||
bind = $modAlt, S, exec, hyprscratch toggle spotify
|
||||
bind = $modAlt, T, exec, hyprscratch toggle transmission
|
||||
bind = $modAlt, V, exec, hyprscratch toggle volume
|
||||
bind = $modAlt, grave, exec, hyprscratch toggle dropdown
|
||||
|
||||
# Hidden workspace (like XMonad's NSP)
|
||||
bind = $mainMod, X, movetoworkspace, special:NSP
|
||||
bind = $mainMod SHIFT, X, togglespecialworkspace, NSP
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DIRECTIONAL NAVIGATION (WASD - like XMonad Navigation2D)
|
||||
# Using hy3 dispatchers for proper tree-based navigation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Focus movement (Mod + WASD) - hy3:movefocus navigates the tree
|
||||
bind = $mainMod, W, hy3:movefocus, u
|
||||
bind = $mainMod, S, hy3:movefocus, d
|
||||
bind = $mainMod, A, hy3:movefocus, l
|
||||
bind = $mainMod, D, hy3:movefocus, r
|
||||
|
||||
# Move windows (Mod + Shift + WASD) - hy3:movewindow with once=true for swapping
|
||||
bind = $mainMod SHIFT, W, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh u once
|
||||
bind = $mainMod SHIFT, S, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh d once
|
||||
bind = $mainMod SHIFT, A, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh l once
|
||||
bind = $mainMod SHIFT, D, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh r once
|
||||
|
||||
# Resize windows (Mod + Ctrl + WASD)
|
||||
binde = $mainMod CTRL, W, resizeactive, 0 -50
|
||||
binde = $mainMod CTRL, S, resizeactive, 0 50
|
||||
binde = $mainMod CTRL, A, resizeactive, -50 0
|
||||
binde = $mainMod CTRL, D, resizeactive, 50 0
|
||||
|
||||
# Screen/Monitor focus (Hyper + WASD)
|
||||
bind = $hyper, W, focusmonitor, u
|
||||
bind = $hyper, S, focusmonitor, d
|
||||
bind = $hyper, A, focusmonitor, l
|
||||
bind = $hyper, D, focusmonitor, r
|
||||
|
||||
# Move window to monitor and follow (Hyper + Shift + WASD)
|
||||
bind = $hyper SHIFT, W, movewindow, mon:u
|
||||
bind = $hyper SHIFT, S, movewindow, mon:d
|
||||
bind = $hyper SHIFT, A, movewindow, mon:l
|
||||
bind = $hyper SHIFT, D, movewindow, mon:r
|
||||
|
||||
# Shift to empty workspace on screen direction (Super + Ctrl + Shift + WASD)
|
||||
# Like XMonad's shiftToEmptyOnScreen
|
||||
bind = $mainMod CTRL SHIFT, W, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh u
|
||||
bind = $mainMod CTRL SHIFT, S, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh d
|
||||
bind = $mainMod CTRL SHIFT, A, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh l
|
||||
bind = $mainMod CTRL SHIFT, 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, comma, exec, rofi_wallpaper.sh
|
||||
bind = $hyper, Y, exec, rofi_agentic_skill
|
||||
|
||||
# Reload config
|
||||
bind = $mainMod, R, exec, hyprctl reload
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MOUSE BINDINGS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
bindm = $mainMod, mouse:272, movewindow
|
||||
bindm = $mainMod, mouse:273, resizewindow
|
||||
|
||||
# Scroll through workspaces
|
||||
bind = $mainMod, mouse_down, exec, ~/.config/hypr/scripts/workspace-scroll.sh +1
|
||||
bind = $mainMod, mouse_up, exec, ~/.config/hypr/scripts/workspace-scroll.sh -1
|
||||
|
||||
# =============================================================================
|
||||
# AUTOSTART
|
||||
# =============================================================================
|
||||
|
||||
# Wire Hyprland into Home Manager's standard user-session targets.
|
||||
# `graphical-session.target` pulls in most tray/SNI applets (which in turn pull in `tray.target`).
|
||||
# Keep the systemd user manager in sync with the current Hyprland session before
|
||||
# starting any session-bound units. Separate `exec-once` commands race.
|
||||
exec-once = sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'
|
||||
# Force a fresh daemon after compositor restarts so hyprscratch doesn't keep a stale socket.
|
||||
exec-once = systemctl --user restart hyprscratch.service
|
||||
exec-once = hypridle
|
||||
|
||||
# Clipboard history daemon
|
||||
exec-once = wl-paste --type text --watch cliphist store
|
||||
exec-once = wl-paste --type image --watch cliphist store
|
||||
File diff suppressed because it is too large
Load Diff
416
dotfiles/config/hypr/hyprland/binds.lua
Normal file
416
dotfiles/config/hypr/hyprland/binds.lua
Normal file
@@ -0,0 +1,416 @@
|
||||
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("rofi_paswitch"), desc("Open PulseAudio output switcher"))
|
||||
bind(hyper .. " + SHIFT + O", exec("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("mpg341cx_input toggle"), desc("Toggle monitor input"))
|
||||
bind(hyper .. " + comma", exec("rofi_wallpaper.sh"), desc("Open wallpaper menu"))
|
||||
bind(hyper .. " + SHIFT + comma", exec("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 .. " + N", exec("rofi_codex_desktop_project.sh"), desc("Start Codex Desktop thread from project"))
|
||||
bind(hyper .. " + T", exec("mova_capture.sh"), desc("Quick-capture task/note into mova (Enter: Today/NEXT, Alt+i: Inbox)"))
|
||||
bind(hyper .. " + SHIFT + T", exec("mova_capture.sh --pick"), desc("Quick-capture into mova, picking a capture template"))
|
||||
bind(hyper .. " + C", exec("rofi_ai_scratchpad.sh"), desc("Choose AI scratchpad (Codex/Claude)"))
|
||||
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()
|
||||
local function hyprtasking_loaded()
|
||||
return hl.plugin and hl.plugin.hyprtasking and hl.plugin.hyprtasking.toggle
|
||||
end
|
||||
|
||||
local function hyprtasking_active()
|
||||
return hyprtasking_loaded()
|
||||
and hl.plugin.hyprtasking.is_active
|
||||
and hl.plugin.hyprtasking.is_active()
|
||||
end
|
||||
|
||||
-- Workspace the overview was opened from, so Escape can cancel back to it
|
||||
-- (hyprtasking:move switches the active workspace live as you navigate).
|
||||
local hyprtasking_origin_workspace = nil
|
||||
|
||||
local function toggle_hyprtasking()
|
||||
if hyprtasking_loaded() then
|
||||
if not hyprtasking_active() then
|
||||
hyprtasking_origin_workspace = hl.get_active_workspace()
|
||||
end
|
||||
hl.plugin.hyprtasking.toggle("cursor")
|
||||
else
|
||||
hl.notification.create({
|
||||
text = "hyprtasking is not loaded",
|
||||
duration = 1800,
|
||||
icon = notification_icons.warning,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Directional keyboard navigation; only acts while the overview is open so
|
||||
-- the arrow keys keep their normal meaning otherwise. The active-border
|
||||
-- highlight follows the selection.
|
||||
local function hyprtasking_move(direction)
|
||||
return function()
|
||||
if hyprtasking_active() and hl.plugin.hyprtasking.move then
|
||||
hl.plugin.hyprtasking.move(direction)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Confirm: close the overview, staying on the highlighted workspace.
|
||||
local function hyprtasking_confirm()
|
||||
if hyprtasking_active() then
|
||||
hl.plugin.hyprtasking.toggle("cursor")
|
||||
end
|
||||
end
|
||||
|
||||
-- Cancel: restore the origin workspace, then close.
|
||||
local function hyprtasking_cancel()
|
||||
if not hyprtasking_active() then
|
||||
return
|
||||
end
|
||||
if hyprtasking_origin_workspace and hyprtasking_origin_workspace.id then
|
||||
dispatch(hl.dsp.focus({
|
||||
workspace = tostring(hyprtasking_origin_workspace.id),
|
||||
on_current_monitor = true,
|
||||
}))
|
||||
end
|
||||
hl.plugin.hyprtasking.toggle("cursor")
|
||||
end
|
||||
|
||||
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", toggle_hyprtasking, desc("Toggle hyprtasking 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", toggle_hyprtasking, desc("Toggle hyprtasking workspace overview", overview_bind_opts))
|
||||
bind(main_mod .. " + Left", hyprtasking_move("left"), desc("Overview: select workspace left", overview_bind_opts))
|
||||
bind(main_mod .. " + Down", hyprtasking_move("down"), desc("Overview: select workspace down", overview_bind_opts))
|
||||
bind(main_mod .. " + Up", hyprtasking_move("up"), desc("Overview: select workspace up", overview_bind_opts))
|
||||
bind(main_mod .. " + Right", hyprtasking_move("right"), desc("Overview: select workspace right", overview_bind_opts))
|
||||
bind(main_mod .. " + Return", hyprtasking_confirm, desc("Overview: confirm selected workspace", overview_bind_opts))
|
||||
bind("Escape", hyprtasking_cancel, desc("Overview: cancel back to origin workspace", { non_consuming = true }))
|
||||
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()
|
||||
-- While the hyprtasking overview is open, WASD navigates the workspace grid;
|
||||
-- otherwise it keeps its normal directional window-focus behavior.
|
||||
local function focus_or_overview_move(direction)
|
||||
return function()
|
||||
if hl.plugin and hl.plugin.hyprtasking and hl.plugin.hyprtasking.is_active
|
||||
and hl.plugin.hyprtasking.is_active() and hl.plugin.hyprtasking.move then
|
||||
hl.plugin.hyprtasking.move(direction)
|
||||
else
|
||||
focus_direction(direction)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
bind(main_mod .. " + W", focus_or_overview_move("up"), desc("Focus window above / overview up"))
|
||||
bind(main_mod .. " + S", focus_or_overview_move("down"), desc("Focus window below / overview down"))
|
||||
bind(main_mod .. " + A", focus_or_overview_move("left"), desc("Focus window left / overview left"))
|
||||
bind(main_mod .. " + D", focus_or_overview_move("right"), desc("Focus window right / overview 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 .. " + F", toggle_active_window_real_fullscreen, desc("Toggle active window real fullscreen"))
|
||||
bind(main_mod .. " + SHIFT + F", toggle_active_window_gaming_mode, desc("Toggle active window gaming fullscreen"))
|
||||
bind(main_mod .. " + T", tile_or_float_active_window, desc("Tile or float 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", toggle_active_ai_scratchpad, desc("Toggle AI scratchpad (Codex/Claude)"))
|
||||
bind(mod_alt .. " + SHIFT + C", toggle_backup_ai_scratchpad, desc("Toggle backup AI scratchpad (Codex/Claude)"))
|
||||
bind(mod_alt .. " + D", function()
|
||||
toggle_scratchpad("discord")
|
||||
end, desc("Toggle Discord 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 .. " + X", function()
|
||||
toggle_scratchpad("x_com")
|
||||
end, desc("Toggle X 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"))
|
||||
bind(hyper .. " + mouse:272", 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
|
||||
636
dotfiles/config/hypr/hyprland/core.lua
Normal file
636
dotfiles/config/hypr/hyprland/core.lua
Normal file
@@ -0,0 +1,636 @@
|
||||
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
|
||||
|
||||
-- True when the running hyprNStack build knows the auto_stacks option
|
||||
-- (old builds crash-parse "setstackcount auto", so callers must gate on this).
|
||||
local function nstack_supports_auto_stacks()
|
||||
return hl.get_config("plugin.nstack.layout.auto_stacks") ~= nil
|
||||
end
|
||||
|
||||
local function apply_nstack_config()
|
||||
if verify_config or not enable_nstack or not configure_nstack_plugin_from_lua then
|
||||
return
|
||||
end
|
||||
|
||||
local 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,
|
||||
}
|
||||
|
||||
if nstack_supports_auto_stacks() then
|
||||
layout.auto_stacks = true
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
nstack = {
|
||||
layout = layout,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_hyprexpo_config()
|
||||
if verify_config or not enable_hyprexpo or enable_hyprtasking 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,
|
||||
live_preview_follow_focus = 0,
|
||||
border_width = 2,
|
||||
border_color_current = "rgb(66ccff)",
|
||||
border_color_focus = "rgb(edb443)",
|
||||
border_color_hover = "rgb(aabbcc)",
|
||||
window_icon_enable = 1,
|
||||
window_icon_position = "bottom-right",
|
||||
window_icon_size = 32,
|
||||
window_icon_offset_x = 8,
|
||||
window_icon_offset_y = 8,
|
||||
window_icon_bg_enable = 1,
|
||||
window_icon_bg_color = 0x88000000,
|
||||
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.nstack_supports_auto_stacks = nstack_supports_auto_stacks
|
||||
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
|
||||
97
dotfiles/config/hypr/hyprland/events.lua
Normal file
97
dotfiles/config/hypr/hyprland/events.lua
Normal file
@@ -0,0 +1,97 @@
|
||||
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 or is_game_like_window(window) 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_dynamic_cursors_config()
|
||||
apply_hyprtasking_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_dynamic_cursors_config)
|
||||
hl.on("config.reloaded", apply_hyprtasking_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
|
||||
715
dotfiles/config/hypr/hyprland/layouts.lua
Normal file
715
dotfiles/config/hypr/hyprland/layouts.lua
Normal file
@@ -0,0 +1,715 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local configure_quadrants_master
|
||||
local focus_workspace
|
||||
local move_window_to_workspace
|
||||
|
||||
local function is_nstack_layout(layout)
|
||||
return layout == columns_layout or layout == grid_layout
|
||||
end
|
||||
|
||||
local function hyprland_layout(layout)
|
||||
if layout == quadrants_layout then
|
||||
return large_main_layout
|
||||
elseif layout == grid_layout then
|
||||
return columns_layout
|
||||
end
|
||||
return layout
|
||||
end
|
||||
|
||||
configure_quadrants_master = function()
|
||||
if quadrants_arranging or current_layout ~= quadrants_layout then
|
||||
return
|
||||
end
|
||||
|
||||
local workspace = active_workspace()
|
||||
if not is_normal_workspace(workspace) then
|
||||
return
|
||||
end
|
||||
|
||||
local windows = tiled_windows(workspace)
|
||||
if #windows == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
sort_windows_by_visual_position(windows)
|
||||
|
||||
quadrants_arranging = true
|
||||
dispatch(hl.dsp.focus({ window = window_selector(windows[1]) }))
|
||||
dispatch(hl.dsp.layout("orientationleft"))
|
||||
dispatch(hl.dsp.layout("mfact exact 0.5"))
|
||||
|
||||
for _ = 1, #windows do
|
||||
dispatch(hl.dsp.layout("removemaster"))
|
||||
end
|
||||
|
||||
if #windows >= 3 then
|
||||
dispatch(hl.dsp.layout("addmaster"))
|
||||
end
|
||||
|
||||
quadrants_arranging = false
|
||||
focus_workspace(workspace.id)
|
||||
end
|
||||
|
||||
local function update_nstack_count()
|
||||
if current_layout == quadrants_layout then
|
||||
configure_quadrants_master()
|
||||
return
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if current_layout == columns_layout and nstack_supports_auto_stacks() then
|
||||
-- The plugin sizes stacks to the window count on every recalculation,
|
||||
-- which also covers paths that fire no window events (e.g. float -> tile).
|
||||
dispatch(hl.dsp.layout("setstackcount auto"))
|
||||
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 layout == quadrants_layout then
|
||||
dismiss_monocle_notice()
|
||||
schedule_nstack_count_update()
|
||||
elseif 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 current_layout == quadrants_layout then
|
||||
dismiss_monocle_notice()
|
||||
schedule_nstack_count_update()
|
||||
elseif 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
|
||||
|
||||
focus_workspace = function(workspace_id)
|
||||
dispatch(hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true }))
|
||||
end
|
||||
|
||||
move_window_to_workspace = function(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 window_contains_point(window, x, y)
|
||||
local at = window and window.at
|
||||
local size = window and window.size
|
||||
if not at or not size then
|
||||
return false
|
||||
end
|
||||
|
||||
local left = tonumber(at.x or at[1])
|
||||
local top = tonumber(at.y or at[2])
|
||||
local width = tonumber(size.x or size[1])
|
||||
local height = tonumber(size.y or size[2])
|
||||
if not left or not top or not width or not height then
|
||||
return false
|
||||
end
|
||||
|
||||
return x >= left and x < left + width and y >= top and y < top + height
|
||||
end
|
||||
|
||||
-- With follow_mouse=1, a bare focus dispatch does not survive the next
|
||||
-- pointer motion unless the cursor already sits inside the target window,
|
||||
-- so warp the cursor into the window when needed.
|
||||
local function focus_window_with_cursor(window)
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return false
|
||||
end
|
||||
|
||||
local live = type(hl.get_window) == "function" and hl.get_window(selector) or window
|
||||
if not live then
|
||||
return false
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.focus({ window = selector }))
|
||||
|
||||
local cursor = hl.get_cursor_pos and hl.get_cursor_pos()
|
||||
if cursor and window_contains_point(live, cursor.x, cursor.y) then
|
||||
return true
|
||||
end
|
||||
|
||||
local center_x, center_y = window_center(live)
|
||||
dispatch(hl.dsp.cursor.move({ x = math.floor(center_x), y = math.floor(center_y) }))
|
||||
return true
|
||||
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
|
||||
|
||||
local workspace = active_workspace()
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if
|
||||
window
|
||||
and window.address == state.anchor
|
||||
and same_workspace(window.workspace, workspace)
|
||||
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 entry_focused = hl.get_active_window()
|
||||
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)
|
||||
if not focus_window_with_cursor(entry_focused) then
|
||||
focus_window_with_cursor(anchor)
|
||||
end
|
||||
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")
|
||||
if not focus_window_with_cursor(focused) then
|
||||
focus_window_with_cursor(anchor)
|
||||
end
|
||||
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,
|
||||
}
|
||||
if not focus_window_with_cursor(focused) then
|
||||
focus_window_with_cursor(anchor)
|
||||
end
|
||||
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.configure_quadrants_master = configure_quadrants_master
|
||||
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
|
||||
665
dotfiles/config/hypr/hyprland/scratchpads.lua
Normal file
665
dotfiles/config/hypr/hyprland/scratchpads.lua
Normal file
@@ -0,0 +1,665 @@
|
||||
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 = {}
|
||||
scratchpad_mode_tag = "scratchpad"
|
||||
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",
|
||||
},
|
||||
claude = {
|
||||
command = "claude-desktop",
|
||||
classes = { "com.anthropic.Claude", "claude-desktop" },
|
||||
},
|
||||
htop = {
|
||||
command = "alacritty --class htop-scratch --title htop -e htop",
|
||||
class = "htop-scratch",
|
||||
},
|
||||
discord = {
|
||||
command = "discord",
|
||||
class = "discord",
|
||||
},
|
||||
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",
|
||||
},
|
||||
x_com = {
|
||||
command = "x-com-pwa",
|
||||
classes = { "x-com-pwa", "chrome-x.com" },
|
||||
title = "X",
|
||||
},
|
||||
transmission = {
|
||||
command = "transmission-gtk",
|
||||
class = "transmission-gtk",
|
||||
},
|
||||
dropdown = {
|
||||
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown",
|
||||
class = "com.mitchellh.ghostty.dropdown",
|
||||
dropdown = true,
|
||||
allow_tiling = false,
|
||||
},
|
||||
}
|
||||
|
||||
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 tiled_scratchpad_is_normal_window(window, def)
|
||||
return def.allow_tiling ~= false and window and window.floating == false
|
||||
end
|
||||
|
||||
local function window_has_scratchpad_tag(window)
|
||||
for _, tag in ipairs((window and window.tags) or {}) do
|
||||
if tag == scratchpad_mode_tag then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function window_is_floating_scratchpad(window)
|
||||
return window and window.floating == true and window_has_scratchpad_tag(window)
|
||||
end
|
||||
|
||||
local function is_scratchpad_window(window)
|
||||
return window_is_floating_scratchpad(window)
|
||||
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 "name:scratch-hidden-" .. name
|
||||
end
|
||||
|
||||
local function mark_window_as_floating_scratchpad(window, name)
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
|
||||
dispatch(hl.dsp.window.tag({ tag = "+" .. scratchpad_mode_tag, window = selector }))
|
||||
if name then
|
||||
dispatch(hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector }))
|
||||
end
|
||||
end
|
||||
|
||||
local function clear_floating_scratchpad_state(window)
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.window.tag({ tag = "-" .. scratchpad_mode_tag, window = selector }))
|
||||
for name in pairs(scratchpads) do
|
||||
dispatch(hl.dsp.window.tag({ tag = "-scratchpad-" .. name, window = selector }))
|
||||
end
|
||||
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 default_scratchpad_geometry(target_monitor)
|
||||
local monitor = target_monitor or hl.get_active_monitor()
|
||||
if not monitor then
|
||||
return
|
||||
end
|
||||
|
||||
local workarea = monitor_workarea(monitor)
|
||||
local width = math.floor(workarea.width * scratchpad_size_ratio)
|
||||
local height = math.floor(workarea.height * scratchpad_size_ratio)
|
||||
|
||||
return {
|
||||
width = width,
|
||||
height = height,
|
||||
x = workarea.x + math.floor((workarea.width - width) / 2),
|
||||
y = workarea.y + math.floor((workarea.height - height) / 2),
|
||||
}
|
||||
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
|
||||
return default_scratchpad_geometry(monitor)
|
||||
end
|
||||
|
||||
return {
|
||||
width = width,
|
||||
height = height,
|
||||
x = x,
|
||||
y = y,
|
||||
}
|
||||
end
|
||||
|
||||
local function should_apply_scratchpad_geometry(name, window, opts)
|
||||
local def = scratchpads[name]
|
||||
if not def then
|
||||
return false
|
||||
end
|
||||
|
||||
return (opts and opts.force_geometry) or def.dropdown or not tiled_scratchpad_is_normal_window(window, def)
|
||||
end
|
||||
|
||||
local function refreshed_window(window)
|
||||
if not window or not window.address or type(hl.get_window) ~= "function" then
|
||||
return window
|
||||
end
|
||||
|
||||
return hl.get_window(window_selector(window)) or window
|
||||
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)
|
||||
|
||||
mark_window_as_floating_scratchpad(window, name)
|
||||
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, opts)
|
||||
hl.timer(function()
|
||||
local current_window = refreshed_window(window)
|
||||
if should_apply_scratchpad_geometry(name, current_window, opts) then
|
||||
apply_scratchpad_geometry(name, current_window, target_monitor, position)
|
||||
end
|
||||
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)
|
||||
name = name or matching_scratchpad_name(window)
|
||||
move_window_to_workspace(name and scratchpad_workspace(name) or "special:NSP", false, window)
|
||||
end
|
||||
|
||||
local function scratchpad_show_workspace(workspace)
|
||||
workspace = workspace or active_workspace()
|
||||
if is_normal_workspace(workspace) then
|
||||
return workspace
|
||||
end
|
||||
|
||||
return hl.get_workspace(tostring(active_workspace_id()))
|
||||
end
|
||||
|
||||
local function show_scratchpad_window(name, window, workspace, target_monitor, opts)
|
||||
workspace = scratchpad_show_workspace(workspace)
|
||||
if not workspace then
|
||||
return
|
||||
end
|
||||
|
||||
remove_minimized_window(window)
|
||||
mark_window_as_floating_scratchpad(window, name)
|
||||
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(), nil, nil, {
|
||||
force_geometry = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function scratchpad_is_visible(window)
|
||||
local workspace = active_workspace()
|
||||
return workspace and window and same_workspace(window.workspace, workspace)
|
||||
end
|
||||
|
||||
-- Scratchpad exclusivity belongs to a window's current presentation state,
|
||||
-- not its class. Invoking a named shortcut replaces any other window that is
|
||||
-- visibly in floating scratchpad mode, including an otherwise ordinary app.
|
||||
local function active_scratchpad_windows(except_name)
|
||||
local windows = {}
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
local name = matching_scratchpad_name(window)
|
||||
if
|
||||
(except_name == nil or name ~= except_name)
|
||||
and scratchpad_is_visible(window)
|
||||
and window_is_floating_scratchpad(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(), {
|
||||
force_geometry = true,
|
||||
})
|
||||
elseif scratchpad_is_visible(window) and window_is_floating_scratchpad(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
|
||||
local workspace = scratchpad_show_workspace()
|
||||
local target_monitor = hl.get_active_monitor()
|
||||
hide_active_scratchpads(name)
|
||||
scratchpad_pending[name] = {
|
||||
monitor = target_monitor,
|
||||
workspace = 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
|
||||
local workspace = scratchpad_show_workspace()
|
||||
local target_monitor = hl.get_active_monitor()
|
||||
hide_active_scratchpads(name)
|
||||
for _, window in ipairs(windows) do
|
||||
show_scratchpad_window(name, window, workspace, target_monitor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Which AI scratchpad SUPER+ALT+C targets. Selected at runtime (no reload)
|
||||
-- by rofi_ai_scratchpad.sh, which writes the chosen name to this file.
|
||||
local ai_scratchpad_default = "codex"
|
||||
|
||||
local function ai_scratchpad_state_path()
|
||||
local base = os.getenv("XDG_STATE_HOME") or ((os.getenv("HOME") or "") .. "/.local/state")
|
||||
return base .. "/hypr/ai-scratchpad"
|
||||
end
|
||||
|
||||
local function active_ai_scratchpad()
|
||||
local file = io.open(ai_scratchpad_state_path(), "r")
|
||||
if not file then
|
||||
return ai_scratchpad_default
|
||||
end
|
||||
|
||||
local value = file:read("*l")
|
||||
file:close()
|
||||
value = value and value:gsub("%s+", "")
|
||||
if scratchpads[value] then
|
||||
return value
|
||||
end
|
||||
return ai_scratchpad_default
|
||||
end
|
||||
|
||||
local function toggle_active_ai_scratchpad()
|
||||
toggle_scratchpad(active_ai_scratchpad())
|
||||
end
|
||||
|
||||
local function backup_ai_scratchpad()
|
||||
if active_ai_scratchpad() == "codex" then
|
||||
return "claude"
|
||||
end
|
||||
return "codex"
|
||||
end
|
||||
|
||||
local function toggle_backup_ai_scratchpad()
|
||||
toggle_scratchpad(backup_ai_scratchpad())
|
||||
end
|
||||
|
||||
-- Used by rofi_ai_scratchpad.sh after a selection: bring the chosen
|
||||
-- scratchpad into view if it isn't already, without hiding it when it is.
|
||||
local function show_active_ai_scratchpad()
|
||||
local name = active_ai_scratchpad()
|
||||
for _, window in ipairs(matching_scratchpad_windows(name)) do
|
||||
if scratchpad_is_visible(window) then
|
||||
return
|
||||
end
|
||||
end
|
||||
toggle_scratchpad(name)
|
||||
end
|
||||
|
||||
_G.im_hyprland_toggle_ai_scratchpad = toggle_active_ai_scratchpad
|
||||
_G.im_hyprland_show_ai_scratchpad = show_active_ai_scratchpad
|
||||
|
||||
ctx.lower_contains = lower_contains
|
||||
ctx.lower_contains_any = lower_contains_any
|
||||
ctx.scratchpad_window_matches = scratchpad_window_matches
|
||||
ctx.tiled_scratchpad_is_normal_window = tiled_scratchpad_is_normal_window
|
||||
ctx.window_has_scratchpad_tag = window_has_scratchpad_tag
|
||||
ctx.window_is_floating_scratchpad = window_is_floating_scratchpad
|
||||
ctx.is_scratchpad_window = is_scratchpad_window
|
||||
ctx.matching_scratchpad_name = matching_scratchpad_name
|
||||
ctx.scratchpad_workspace = scratchpad_workspace
|
||||
ctx.mark_window_as_floating_scratchpad = mark_window_as_floating_scratchpad
|
||||
ctx.clear_floating_scratchpad_state = clear_floating_scratchpad_state
|
||||
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.should_apply_scratchpad_geometry = should_apply_scratchpad_geometry
|
||||
ctx.refreshed_window = refreshed_window
|
||||
ctx.matching_scratchpad_windows = matching_scratchpad_windows
|
||||
ctx.default_scratchpad_geometry = default_scratchpad_geometry
|
||||
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.scratchpad_show_workspace = scratchpad_show_workspace
|
||||
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
|
||||
ctx.active_ai_scratchpad = active_ai_scratchpad
|
||||
ctx.backup_ai_scratchpad = backup_ai_scratchpad
|
||||
ctx.toggle_active_ai_scratchpad = toggle_active_ai_scratchpad
|
||||
ctx.toggle_backup_ai_scratchpad = toggle_backup_ai_scratchpad
|
||||
ctx.show_active_ai_scratchpad = show_active_ai_scratchpad
|
||||
end
|
||||
|
||||
return M
|
||||
509
dotfiles/config/hypr/hyprland/settings.lua
Normal file
509
dotfiles/config/hypr/hyprland/settings.lua
Normal file
@@ -0,0 +1,509 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local file_chooser_class_rule = "^(xdg-desktop-portal-gtk|org\\.freedesktop\\.impl\\.portal\\.desktop\\.gtk)$"
|
||||
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 class_indicates_file_chooser(class)
|
||||
class = lower_string(class)
|
||||
return class == "xdg-desktop-portal-gtk"
|
||||
or class == "org.freedesktop.impl.portal.desktop.gtk"
|
||||
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)
|
||||
or class_indicates_file_chooser(window.class)
|
||||
or class_indicates_file_chooser(window.initial_class)
|
||||
)
|
||||
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_hyprwinview and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprwinview.so")
|
||||
end
|
||||
if enable_hyprtasking and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprtasking.so")
|
||||
end
|
||||
if enable_hyprexpo and not enable_hyprtasking and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.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_dynamic_cursors and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhypr-dynamic-cursors.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", tostring(hyprland_cursor_size))
|
||||
hl.env("HYPRCURSOR_SIZE", tostring(hyprland_cursor_size))
|
||||
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 = hyprland_gaps_enabled and 5 or 0,
|
||||
gaps_out = hyprland_gaps_enabled and 10 or 0,
|
||||
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,
|
||||
focus_on_activate = 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_dynamic_cursors_config()
|
||||
if verify_config or not enable_dynamic_cursors then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
dynamic_cursors = {
|
||||
enabled = true,
|
||||
mode = "tilt",
|
||||
threshold = 2,
|
||||
tilt = {
|
||||
limit = 5000,
|
||||
activation = "negative_quadratic",
|
||||
window = 100,
|
||||
full = 60,
|
||||
},
|
||||
shake = {
|
||||
enabled = true,
|
||||
threshold = 6.0,
|
||||
base = 4.0,
|
||||
speed = 4.0,
|
||||
influence = 0.0,
|
||||
limit = 0.0,
|
||||
timeout = 2000,
|
||||
effects = true,
|
||||
ipc = false,
|
||||
},
|
||||
hyprcursor = {
|
||||
nearest = 1,
|
||||
enabled = true,
|
||||
resolution = -1,
|
||||
fallback = "clientside",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_hyprtasking_config()
|
||||
if verify_config or not enable_hyprtasking then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
hyprtasking = {
|
||||
layout = "grid",
|
||||
-- hyprtasking reads bg_color as 0xAARRGGBB and forces it opaque, so
|
||||
-- the stock default (0x000000FF) renders as solid blue. Pin true black.
|
||||
bg_color = 0xff000000,
|
||||
gap_size = 10,
|
||||
-- Active workspace tile is bordered with general:col.active_border,
|
||||
-- inactive tiles with col.inactive_border; this is the selection
|
||||
-- highlight and it follows hyprtasking:move. Keep it clearly visible.
|
||||
border_size = 3,
|
||||
exit_on_hovered = false,
|
||||
close_overview_on_reload = false,
|
||||
jump = {
|
||||
enabled = true,
|
||||
},
|
||||
grid = {
|
||||
rows = 3,
|
||||
cols = 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
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({
|
||||
name = "tagged-gaming-window",
|
||||
match = { tag = gaming_window_tag },
|
||||
idle_inhibit = "fullscreen",
|
||||
opaque = true,
|
||||
no_blur = true,
|
||||
no_shadow = true,
|
||||
no_anim = true,
|
||||
rounding = 0,
|
||||
border_size = 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({
|
||||
name = "portal-gtk-dialogs",
|
||||
match = { class = file_chooser_class_rule },
|
||||
float = true,
|
||||
center = true,
|
||||
focus_on_activate = true,
|
||||
stay_focused = true,
|
||||
})
|
||||
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
|
||||
|
||||
-- The AI desktop apps fire xdg-activation requests while streaming
|
||||
-- responses; with misc:focus_on_activate=true that steals focus from
|
||||
-- whatever window the user is actually working in. focus_on_activate is
|
||||
-- a dynamic rule (applies to already-mapped windows on reload);
|
||||
-- suppress_event only applies at map time.
|
||||
for index, class in ipairs({ "^(com\\.anthropic\\.Claude|claude-desktop)$", "^(codex-desktop)$" }) do
|
||||
hl.window_rule({
|
||||
name = "ai-app-no-activate-focus-" .. tostring(index),
|
||||
match = { class = class },
|
||||
focus_on_activate = false,
|
||||
suppress_event = "activatefocus",
|
||||
})
|
||||
end
|
||||
|
||||
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_dynamic_cursors_config = apply_dynamic_cursors_config
|
||||
ctx.apply_hyprtasking_config = apply_hyprtasking_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
|
||||
88
dotfiles/config/hypr/hyprland/state.lua
Normal file
88
dotfiles/config/hypr/hyprland/state.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
local shell_ui_command = "hypr_shell_ui"
|
||||
local columns_layout = "nStack"
|
||||
local large_main_layout = "master"
|
||||
local grid_layout = "grid"
|
||||
local quadrants_layout = "quadrants"
|
||||
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,
|
||||
quadrants_layout = quadrants_layout,
|
||||
monocle_layout = monocle_layout,
|
||||
layout_cycle = { columns_layout, large_main_layout, quadrants_layout, grid_layout },
|
||||
layout_names = {
|
||||
[columns_layout] = "Columns",
|
||||
[large_main_layout] = "Large main",
|
||||
[quadrants_layout] = "Quadrants",
|
||||
[grid_layout] = "Grid",
|
||||
[monocle_layout] = "Monocle",
|
||||
},
|
||||
minimized_workspace = "special:minimized",
|
||||
inactive_opacity_override_tag = "no-inactive-opacity",
|
||||
gaming_window_tag = "gaming",
|
||||
gaming_window_disabled_tag = "no-gaming",
|
||||
gaming_window_class_patterns = {},
|
||||
gaming_window_title_patterns = {},
|
||||
gaming_window_excluded_class_patterns = {
|
||||
"^[Hh]eroic$",
|
||||
"^[Ss]team$",
|
||||
},
|
||||
gaming_window_excluded_title_patterns = {
|
||||
"^Heroic Games Launcher$",
|
||||
"^Steam$",
|
||||
},
|
||||
tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-",
|
||||
current_layout = columns_layout,
|
||||
enable_nstack = true,
|
||||
-- Disabled 2026-06-11: live-preview backend SEGVs Hyprland (shouldRenderWindow
|
||||
-- hook fires on every popup commit). Re-enable once fixed upstream.
|
||||
enable_hyprexpo = false,
|
||||
enable_hyprwinview = true,
|
||||
enable_hyprtasking = true,
|
||||
enable_workspace_history = true,
|
||||
-- Disabled 2026-06-28: causes the cursor to intermittently disappear
|
||||
-- (confirmed by unloading the plugin live — cursor stopped vanishing).
|
||||
-- It replaces cursor rendering via lockSoftwareAll for its tilt/shake
|
||||
-- effects. Re-enable only after verifying upstream fixed this.
|
||||
enable_dynamic_cursors = false,
|
||||
enable_hyprglass = false,
|
||||
hyprland_gaps_enabled = os.getenv("IMALISON_HYPRLAND_GAPS") ~= "0",
|
||||
hyprland_cursor_size = tonumber(os.getenv("IMALISON_HYPRLAND_CURSOR_SIZE")) or 24,
|
||||
hypr_visual_performance_mode = false,
|
||||
configure_nstack_plugin_from_lua = false,
|
||||
workspace_layouts = {},
|
||||
minimized_windows = {},
|
||||
tabbed_workspace_groups = {},
|
||||
quadrants_arranging = false,
|
||||
window_picker_mode = nil,
|
||||
window_picker_candidates = {},
|
||||
stack_update_timer = nil,
|
||||
monocle_notice = nil,
|
||||
}
|
||||
651
dotfiles/config/hypr/hyprland/windows.lua
Normal file
651
dotfiles/config/hypr/hyprland/windows.lua
Normal file
@@ -0,0 +1,651 @@
|
||||
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 float_active_window_to_default_scratchpad_geometry()
|
||||
local window = hl.get_active_window()
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
local geometry = default_scratchpad_geometry(hl.get_active_monitor())
|
||||
if not geometry then
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.window.fullscreen_state({
|
||||
internal = 0,
|
||||
client = 0,
|
||||
action = "set",
|
||||
window = selector,
|
||||
}))
|
||||
mark_window_as_floating_scratchpad(window, matching_scratchpad_name(window))
|
||||
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 }))
|
||||
end
|
||||
|
||||
local function tile_or_float_active_window()
|
||||
local window = hl.get_active_window()
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
if window.floating then
|
||||
clear_floating_scratchpad_state(window)
|
||||
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
|
||||
return
|
||||
end
|
||||
|
||||
float_active_window_to_default_scratchpad_geometry()
|
||||
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 value_matches_any_pattern(value, patterns)
|
||||
value = tostring(value or "")
|
||||
if value == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, pattern in ipairs(patterns or {}) do
|
||||
if value:find(pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function is_game_like_window(window)
|
||||
if not window then
|
||||
return false
|
||||
end
|
||||
|
||||
if window_has_tag(window, gaming_window_disabled_tag) then
|
||||
return false
|
||||
end
|
||||
if
|
||||
value_matches_any_pattern(window.class, gaming_window_excluded_class_patterns)
|
||||
or value_matches_any_pattern(window.initial_class, gaming_window_excluded_class_patterns)
|
||||
or value_matches_any_pattern(window.title, gaming_window_excluded_title_patterns)
|
||||
or value_matches_any_pattern(window.initial_title, gaming_window_excluded_title_patterns)
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
return tostring(window.content_type or window.content or "") == "game"
|
||||
or window_has_tag(window, gaming_window_tag)
|
||||
or value_matches_any_pattern(window.class, gaming_window_class_patterns)
|
||||
or value_matches_any_pattern(window.initial_class, gaming_window_class_patterns)
|
||||
or value_matches_any_pattern(window.title, gaming_window_title_patterns)
|
||||
or value_matches_any_pattern(window.initial_title, gaming_window_title_patterns)
|
||||
end
|
||||
|
||||
local function set_window_gaming_mode(window, enabled, opts)
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
if enabled then
|
||||
dispatch(hl.dsp.window.tag({ tag = "-" .. gaming_window_disabled_tag, window = selector }))
|
||||
dispatch(hl.dsp.window.tag({ tag = "+" .. gaming_window_tag, window = selector }))
|
||||
dispatch(hl.dsp.window.fullscreen_state({
|
||||
internal = 2,
|
||||
client = 2,
|
||||
action = "set",
|
||||
window = selector,
|
||||
}))
|
||||
else
|
||||
dispatch(hl.dsp.window.tag({ tag = "-" .. gaming_window_tag, window = selector }))
|
||||
dispatch(hl.dsp.window.tag({ tag = "+" .. gaming_window_disabled_tag, window = selector }))
|
||||
dispatch(hl.dsp.window.fullscreen_state({
|
||||
internal = 0,
|
||||
client = 0,
|
||||
action = "set",
|
||||
window = selector,
|
||||
}))
|
||||
end
|
||||
|
||||
if not (opts and opts.quiet) then
|
||||
hl.notification.create({
|
||||
text = "Gaming fullscreen: " .. (enabled and "on" or "off"),
|
||||
duration = 1600,
|
||||
icon = enabled and notification_icons.ok or notification_icons.info,
|
||||
color = enabled and "rgba(33ccffee)" or "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle_active_window_gaming_mode()
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
set_window_gaming_mode(window, not window_has_tag(window, gaming_window_tag))
|
||||
end
|
||||
|
||||
local function toggle_active_window_real_fullscreen()
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
local fullscreen = tonumber(window.fullscreen) or 0
|
||||
local fullscreen_client = tonumber(window.fullscreen_client) or 0
|
||||
local enabling = fullscreen ~= 2 or fullscreen_client ~= 2
|
||||
dispatch(hl.dsp.window.fullscreen_state({
|
||||
internal = enabling and 2 or 0,
|
||||
client = enabling and 2 or 0,
|
||||
action = "set",
|
||||
window = window_selector(window),
|
||||
}))
|
||||
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.float_active_window_to_default_scratchpad_geometry = float_active_window_to_default_scratchpad_geometry
|
||||
ctx.tile_or_float_active_window = tile_or_float_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.window_has_tag = window_has_tag
|
||||
ctx.value_matches_any_pattern = value_matches_any_pattern
|
||||
ctx.is_game_like_window = is_game_like_window
|
||||
ctx.set_window_gaming_mode = set_window_gaming_mode
|
||||
ctx.toggle_active_window_gaming_mode = toggle_active_window_gaming_mode
|
||||
ctx.toggle_active_window_real_fullscreen = toggle_active_window_real_fullscreen
|
||||
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
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bring window to current workspace (like XMonad's bringWindow)
|
||||
# Uses rofi with icons to select a window, then moves it here.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/window-icon-map.sh"
|
||||
|
||||
CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id')
|
||||
|
||||
# Get windows on OTHER workspaces as TSV
|
||||
WINDOW_DATA=$(hyprctl clients -j | jq -r --argjson cws "$CURRENT_WS" '
|
||||
.[] | select(.workspace.id >= 0 and .workspace.id != $cws)
|
||||
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
|
||||
| @tsv')
|
||||
|
||||
if [ -z "$WINDOW_DATA" ]; then
|
||||
notify-send "Bring Window" "No windows on other workspaces"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
addresses=()
|
||||
TMPFILE=$(mktemp)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
while IFS=$'\t' read -r address class title ws_id; do
|
||||
icon=$(icon_for_class "$class")
|
||||
addresses+=("$address")
|
||||
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
|
||||
"$class" "$title" "$ws_id" "$icon"
|
||||
done <<< "$WINDOW_DATA" > "$TMPFILE"
|
||||
|
||||
INDEX=$(rofi -dmenu -i -show-icons -p "Bring window" -format i < "$TMPFILE") || exit 0
|
||||
|
||||
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
|
||||
ADDRESS="${addresses[$INDEX]}"
|
||||
hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDRESS"
|
||||
hyprctl dispatch focuswindow "address:$ADDRESS"
|
||||
fi
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cycle between master and dwindle layouts
|
||||
# Like XMonad's NextLayout
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CURRENT=$(hyprctl getoption general:layout -j | jq -r '.str')
|
||||
|
||||
if [ "$CURRENT" = "master" ]; then
|
||||
hyprctl keyword general:layout dwindle
|
||||
notify-send "Layout" "Switched to Dwindle (binary tree)"
|
||||
else
|
||||
hyprctl keyword general:layout master
|
||||
notify-send "Layout" "Switched to Master (XMonad-like)"
|
||||
fi
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Print an "empty" workspace id within 1..$HYPR_MAX_WORKSPACE (default 9).
|
||||
#
|
||||
# Preference order (lowest id wins within each tier):
|
||||
# 1. Workspace exists on the target monitor and has 0 windows
|
||||
# 2. Workspace id does not exist at all (will be created on dispatch)
|
||||
# 3. Workspace exists (elsewhere) and has 0 windows
|
||||
#
|
||||
# Usage:
|
||||
# find-empty-workspace.sh [monitor] [exclude_id]
|
||||
|
||||
max_ws="${HYPR_MAX_WORKSPACE:-9}"
|
||||
|
||||
monitor="${1:-}"
|
||||
exclude_id="${2:-}"
|
||||
|
||||
if [[ -z "${monitor}" ]]; then
|
||||
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "${monitor}" || "${monitor}" == "null" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
workspaces_json="$(hyprctl workspaces -j 2>/dev/null || echo '[]')"
|
||||
|
||||
unused_candidate=""
|
||||
elsewhere_empty_candidate=""
|
||||
|
||||
for i in $(seq 1 "${max_ws}"); do
|
||||
if [[ -n "${exclude_id}" && "${i}" == "${exclude_id}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
exists="$(jq -r --argjson id "${i}" '[.[] | select(.id == $id)] | length' <<<"${workspaces_json}")"
|
||||
if [[ "${exists}" == "0" ]]; then
|
||||
if [[ -z "${unused_candidate}" ]]; then
|
||||
unused_candidate="${i}"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
windows="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .windows] | .[0]) // 0' <<<"${workspaces_json}")"
|
||||
if [[ "${windows}" != "0" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
ws_monitor="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .monitor] | .[0]) // ""' <<<"${workspaces_json}")"
|
||||
if [[ "${ws_monitor}" == "${monitor}" ]]; then
|
||||
printf '%s\n' "${i}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "${elsewhere_empty_candidate}" ]]; then
|
||||
elsewhere_empty_candidate="${i}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "${unused_candidate}" ]]; then
|
||||
printf '%s\n' "${unused_candidate}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "${elsewhere_empty_candidate}" ]]; then
|
||||
printf '%s\n' "${elsewhere_empty_candidate}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 1
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Focus next window of a different class (like XMonad's focusNextClass)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get focused window class
|
||||
FOCUSED_CLASS=$(hyprctl activewindow -j | jq -r '.class')
|
||||
FOCUSED_ADDR=$(hyprctl activewindow -j | jq -r '.address')
|
||||
|
||||
if [ "$FOCUSED_CLASS" = "null" ] || [ -z "$FOCUSED_CLASS" ]; then
|
||||
# No focused window, just focus any window
|
||||
hyprctl dispatch cyclenext
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get all unique classes
|
||||
ALL_CLASSES=$(hyprctl clients -j | jq -r '[.[] | select(.workspace.id >= 0) | .class] | unique | .[]')
|
||||
|
||||
# Get sorted list of classes
|
||||
CLASSES_ARRAY=()
|
||||
while IFS= read -r class; do
|
||||
CLASSES_ARRAY+=("$class")
|
||||
done <<< "$ALL_CLASSES"
|
||||
|
||||
# Find current class index and get next class
|
||||
CURRENT_INDEX=-1
|
||||
for i in "${!CLASSES_ARRAY[@]}"; do
|
||||
if [ "${CLASSES_ARRAY[$i]}" = "$FOCUSED_CLASS" ]; then
|
||||
CURRENT_INDEX=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $CURRENT_INDEX -eq -1 ] || [ ${#CLASSES_ARRAY[@]} -le 1 ]; then
|
||||
# Only one class or class not found
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get next class (wrapping around)
|
||||
NEXT_INDEX=$(( (CURRENT_INDEX + 1) % ${#CLASSES_ARRAY[@]} ))
|
||||
NEXT_CLASS="${CLASSES_ARRAY[$NEXT_INDEX]}"
|
||||
|
||||
# Find first window of next class
|
||||
NEXT_WINDOW=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$NEXT_CLASS\" and .workspace.id >= 0) | .address" | head -1)
|
||||
|
||||
if [ -n "$NEXT_WINDOW" ]; then
|
||||
hyprctl dispatch focuswindow "address:$NEXT_WINDOW"
|
||||
fi
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gather all windows of the same class as focused window (like XMonad's gatherThisClass)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get focused window class
|
||||
FOCUSED_CLASS=$(hyprctl activewindow -j | jq -r '.class')
|
||||
CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id')
|
||||
|
||||
if [ "$FOCUSED_CLASS" = "null" ] || [ -z "$FOCUSED_CLASS" ]; then
|
||||
notify-send "Gather Class" "No focused window"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find all windows with same class on other workspaces
|
||||
WINDOWS=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$FOCUSED_CLASS\" and .workspace.id != $CURRENT_WS and .workspace.id >= 0) | .address")
|
||||
|
||||
if [ -z "$WINDOWS" ]; then
|
||||
notify-send "Gather Class" "No other windows of class '$FOCUSED_CLASS'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Move each window to current workspace
|
||||
COUNT=0
|
||||
for ADDR in $WINDOWS; do
|
||||
hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDR"
|
||||
COUNT=$((COUNT + 1))
|
||||
done
|
||||
|
||||
notify-send "Gather Class" "Gathered $COUNT windows of class '$FOCUSED_CLASS'"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Go to a window selected via rofi (with icons from desktop entries).
|
||||
# Replaces "rofi -show window" which doesn't work well on Wayland.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/window-icon-map.sh"
|
||||
|
||||
# Get all windows on regular workspaces as TSV
|
||||
WINDOW_DATA=$(hyprctl clients -j | jq -r '
|
||||
.[] | select(.workspace.id >= 0)
|
||||
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
|
||||
| @tsv')
|
||||
|
||||
[ -n "$WINDOW_DATA" ] || exit 0
|
||||
|
||||
addresses=()
|
||||
TMPFILE=$(mktemp)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
while IFS=$'\t' read -r address class title ws_id; do
|
||||
icon=$(icon_for_class "$class")
|
||||
addresses+=("$address")
|
||||
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
|
||||
"$class" "$title" "$ws_id" "$icon"
|
||||
done <<< "$WINDOW_DATA" > "$TMPFILE"
|
||||
|
||||
INDEX=$(rofi -dmenu -i -show-icons -p "Go to window" -format i < "$TMPFILE") || exit 0
|
||||
|
||||
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
|
||||
hyprctl dispatch focuswindow "address:${addresses[$INDEX]}"
|
||||
fi
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Minimize the active window by moving it to a special workspace without
|
||||
# toggling that special workspace open.
|
||||
#
|
||||
# Usage: minimize-active.sh <name>
|
||||
# Example: minimize-active.sh minimized
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${1:-minimized}"
|
||||
NAME="${NAME#special:}"
|
||||
|
||||
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
# We could parse plain output, but jq should exist in this setup; if it
|
||||
# doesn't, fail soft.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ACTIVE_JSON="$(hyprctl -j activewindow 2>/dev/null || true)"
|
||||
ADDR="$(printf '%s' "$ACTIVE_JSON" | jq -r '.address // empty')"
|
||||
if [ -z "$ADDR" ] || [ "$ADDR" = "null" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If the minimized special workspace is currently visible, closing it after the
|
||||
# move keeps the window hidden (what "minimize" usually means).
|
||||
MONITOR_ID="$(printf '%s' "$ACTIVE_JSON" | jq -r '.monitor // empty')"
|
||||
SPECIAL_OPEN="$(
|
||||
hyprctl -j monitors 2>/dev/null \
|
||||
| jq -r --arg n "special:$NAME" --argjson mid "${MONITOR_ID:-0}" '
|
||||
.[]
|
||||
| select(.id == $mid)
|
||||
| (.specialWorkspace.name // "")
|
||||
| select(. == $n)
|
||||
' \
|
||||
| head -n 1 \
|
||||
|| true
|
||||
)"
|
||||
|
||||
hyprctl dispatch movetoworkspacesilent "special:${NAME},address:${ADDR}" >/dev/null 2>&1 || true
|
||||
|
||||
if [ -n "$SPECIAL_OPEN" ]; then
|
||||
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exit minimized picker mode:
|
||||
# - Hide the minimized special workspace on the active monitor (if visible)
|
||||
# - Reset the submap
|
||||
#
|
||||
# Usage: minimized-cancel.sh <name>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${1:-minimized}"
|
||||
NAME="${NAME#special:}"
|
||||
SPECIAL_WS="special:${NAME}"
|
||||
|
||||
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MONITOR_ID="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.monitorID // empty' || true)"
|
||||
if [ -z "$MONITOR_ID" ] || [ "$MONITOR_ID" = "null" ]; then
|
||||
MONITOR_ID=0
|
||||
fi
|
||||
|
||||
OPEN="$(
|
||||
hyprctl -j monitors 2>/dev/null \
|
||||
| jq -r --argjson mid "$MONITOR_ID" '.[] | select(.id == $mid) | (.specialWorkspace.name // "")' \
|
||||
| head -n 1 \
|
||||
|| true
|
||||
)"
|
||||
|
||||
if [ "$OPEN" = "$SPECIAL_WS" ]; then
|
||||
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
hyprctl dispatch submap reset >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Enter a "picker" mode for minimized windows:
|
||||
# - Ensure the minimized special workspace is visible on the active monitor
|
||||
# - Switch Hyprland into a submap so Enter restores and Escape cancels
|
||||
#
|
||||
# Usage: minimized-mode.sh <name>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${1:-minimized}"
|
||||
NAME="${NAME#special:}"
|
||||
SPECIAL_WS="special:${NAME}"
|
||||
|
||||
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MONITOR_ID="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.monitorID // empty' || true)"
|
||||
if [ -z "$MONITOR_ID" ] || [ "$MONITOR_ID" = "null" ]; then
|
||||
MONITOR_ID=0
|
||||
fi
|
||||
|
||||
OPEN="$(
|
||||
hyprctl -j monitors 2>/dev/null \
|
||||
| jq -r --argjson mid "$MONITOR_ID" '.[] | select(.id == $mid) | (.specialWorkspace.name // "")' \
|
||||
| head -n 1 \
|
||||
|| true
|
||||
)"
|
||||
|
||||
# Ensure it's visible (but don't toggle it off if already open).
|
||||
if [ "$OPEN" != "$SPECIAL_WS" ]; then
|
||||
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
hyprctl dispatch submap minimized >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Move the active window in a direction and warp the cursor to keep its
|
||||
# relative position inside the moved window.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/run/current-system/sw/bin:${PATH}"
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: $0 <dir> [mode]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dir="$1"
|
||||
mode="${2:-}"
|
||||
|
||||
if ! command -v hyprctl >/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
move_window() {
|
||||
if [[ -n "$mode" ]]; then
|
||||
hyprctl dispatch hy3:movewindow "$dir, $mode" >/dev/null 2>&1 || true
|
||||
else
|
||||
hyprctl dispatch hy3:movewindow "$dir" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
|
||||
cur_json="$(hyprctl -j cursorpos 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$win_json" || "$win_json" == "null" || -z "$cur_json" || "$cur_json" == "null" ]]; then
|
||||
move_window
|
||||
exit 0
|
||||
fi
|
||||
|
||||
win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
|
||||
win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
|
||||
win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
|
||||
win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
|
||||
cur_x="$(jq -er '.x' <<<"$cur_json" 2>/dev/null || true)"
|
||||
cur_y="$(jq -er '.y' <<<"$cur_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ || ! "$cur_x" =~ ^-?[0-9]+$ || ! "$cur_y" =~ ^-?[0-9]+$ ]]; then
|
||||
move_window
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rel_x=$((cur_x - win_x))
|
||||
rel_y=$((cur_y - win_y))
|
||||
|
||||
move_window
|
||||
|
||||
win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
|
||||
if [[ -z "$win_json" || "$win_json" == "null" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
|
||||
win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
|
||||
win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
|
||||
win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ((rel_x < 0)); then
|
||||
rel_x=0
|
||||
elif ((rel_x > win_w)); then
|
||||
rel_x=$win_w
|
||||
fi
|
||||
|
||||
if ((rel_y < 0)); then
|
||||
rel_y=0
|
||||
elif ((rel_y > win_h)); then
|
||||
rel_y=$win_h
|
||||
fi
|
||||
|
||||
new_x=$((win_x + rel_x))
|
||||
new_y=$((win_y + rel_y))
|
||||
|
||||
hyprctl dispatch movecursor "$new_x" "$new_y" >/dev/null 2>&1 || true
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Raise existing window or run command (like XMonad's raiseNextMaybe)
|
||||
# Usage: raise-or-run.sh <class-pattern> <command>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CLASS_PATTERN="$1"
|
||||
COMMAND="$2"
|
||||
|
||||
# Find windows matching the class pattern
|
||||
MATCHING=$(hyprctl clients -j | jq -r ".[] | select(.class | test(\"$CLASS_PATTERN\"; \"i\")) | .address" | head -1)
|
||||
|
||||
if [ -n "$MATCHING" ]; then
|
||||
# Window exists, focus it
|
||||
hyprctl dispatch focuswindow "address:$MATCHING"
|
||||
else
|
||||
# No matching window, run the command
|
||||
exec $COMMAND
|
||||
fi
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Replace focused window with selected window (like XMonad's myReplaceWindow)
|
||||
# Swaps the positions of focused window and selected window
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/window-icon-map.sh"
|
||||
|
||||
FOCUSED=$(hyprctl activewindow -j | jq -r '.address')
|
||||
|
||||
if [ "$FOCUSED" = "null" ] || [ -z "$FOCUSED" ]; then
|
||||
notify-send "Replace Window" "No focused window"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get all windows except focused as TSV
|
||||
WINDOW_DATA=$(hyprctl clients -j | jq -r --arg focused "$FOCUSED" '
|
||||
.[] | select(.workspace.id >= 0 and .address != $focused)
|
||||
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
|
||||
| @tsv')
|
||||
|
||||
if [ -z "$WINDOW_DATA" ]; then
|
||||
notify-send "Replace Window" "No other windows available"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
addresses=()
|
||||
TMPFILE=$(mktemp)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
while IFS=$'\t' read -r address class title ws_id; do
|
||||
icon=$(icon_for_class "$class")
|
||||
addresses+=("$address")
|
||||
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
|
||||
"$class" "$title" "$ws_id" "$icon"
|
||||
done <<< "$WINDOW_DATA" > "$TMPFILE"
|
||||
|
||||
INDEX=$(rofi -dmenu -i -show-icons -p "Replace with" -format i < "$TMPFILE") || exit 0
|
||||
|
||||
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
|
||||
hyprctl dispatch hy3:movewindow "address:${addresses[$INDEX]}"
|
||||
fi
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shift window to empty workspace on screen in given direction
|
||||
# Like XMonad's shiftToEmptyOnScreen
|
||||
# Usage: shift-to-empty-on-screen.sh <direction: u|d|l|r>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DIRECTION="$1"
|
||||
max_ws="${HYPR_MAX_WORKSPACE:-9}"
|
||||
|
||||
# Track the current monitor so we can return
|
||||
ORIG_MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
|
||||
|
||||
# Move focus to the screen in that direction
|
||||
hyprctl dispatch focusmonitor "$DIRECTION"
|
||||
|
||||
# Get the monitor we're now on (target monitor)
|
||||
MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
|
||||
|
||||
# If there is no monitor in that direction, bail
|
||||
if [ "$MONITOR" = "$ORIG_MONITOR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find an empty workspace within 1..$HYPR_MAX_WORKSPACE.
|
||||
EMPTY_WS="$(~/.config/hypr/scripts/find-empty-workspace.sh "${MONITOR}" 2>/dev/null || true)"
|
||||
if [[ -z "${EMPTY_WS}" ]]; then
|
||||
# No empty workspace available within the cap; restore focus and bail.
|
||||
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( EMPTY_WS < 1 || EMPTY_WS > max_ws )); then
|
||||
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ensure the workspace exists on the target monitor
|
||||
hyprctl dispatch workspace "$EMPTY_WS"
|
||||
|
||||
# Go back to original monitor and move the window (without following)
|
||||
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
|
||||
hyprctl dispatch movetoworkspacesilent "$EMPTY_WS"
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Swap the contents of the current workspace with another workspace.
|
||||
# Intended to mirror XMonad's swapWithCurrent behavior.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
max_ws="${HYPR_MAX_WORKSPACE:-9}"
|
||||
|
||||
CURRENT_WS="$(hyprctl activeworkspace -j | jq -r '.id')"
|
||||
if [[ -z "${CURRENT_WS}" || "${CURRENT_WS}" == "null" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TARGET_WS="${1:-}"
|
||||
|
||||
if [[ -z "${TARGET_WS}" ]]; then
|
||||
WS_LIST="$({
|
||||
seq 1 "${max_ws}"
|
||||
hyprctl workspaces -j | jq -r '.[].id' 2>/dev/null || true
|
||||
} | awk 'NF {print $1}' | awk '!seen[$0]++' | sort -n)"
|
||||
|
||||
TARGET_WS="$(printf "%s\n" "${WS_LIST}" | rofi -dmenu -p "Swap with workspace")"
|
||||
fi
|
||||
|
||||
if [[ -z "${TARGET_WS}" || "${TARGET_WS}" == "null" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${TARGET_WS}" == "${CURRENT_WS}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! [[ "${TARGET_WS}" =~ ^-?[0-9]+$ ]]; then
|
||||
notify-send "Swap Workspace" "Invalid workspace: ${TARGET_WS}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (( TARGET_WS < 1 || TARGET_WS > max_ws )); then
|
||||
notify-send "Swap Workspace" "Workspace out of range (1-${max_ws}): ${TARGET_WS}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WINDOWS_CURRENT="$(hyprctl clients -j | jq -r --arg ws "${CURRENT_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
|
||||
WINDOWS_TARGET="$(hyprctl clients -j | jq -r --arg ws "${TARGET_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
|
||||
|
||||
for ADDR in ${WINDOWS_CURRENT}; do
|
||||
hyprctl dispatch movetoworkspace "${TARGET_WS},address:${ADDR}"
|
||||
done
|
||||
|
||||
for ADDR in ${WINDOWS_TARGET}; do
|
||||
hyprctl dispatch movetoworkspace "${CURRENT_WS},address:${ADDR}"
|
||||
done
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Toggle a named Hyprland scratchpad, spawning it if needed.
|
||||
# Usage: toggle-scratchpad.sh <name> <class_regex|-> <title_regex|-> <command...>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -lt 4 ]; then
|
||||
echo "usage: $0 <name> <class_regex|-> <title_regex|-> <command...>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME="$1"
|
||||
shift
|
||||
CLASS_REGEX="$1"
|
||||
shift
|
||||
TITLE_REGEX="$1"
|
||||
shift
|
||||
COMMAND=("$@")
|
||||
|
||||
if [ "$CLASS_REGEX" = "-" ]; then
|
||||
CLASS_REGEX=""
|
||||
fi
|
||||
if [ "$TITLE_REGEX" = "-" ]; then
|
||||
TITLE_REGEX=""
|
||||
fi
|
||||
|
||||
if [ -z "$CLASS_REGEX" ] && [ -z "$TITLE_REGEX" ]; then
|
||||
echo "toggle-scratchpad: provide a class or title regex" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MATCHING=$(hyprctl clients -j | jq -r --arg cre "$CLASS_REGEX" --arg tre "$TITLE_REGEX" '
|
||||
.[]
|
||||
| select(
|
||||
(($cre == "") or (.class | test($cre; "i")))
|
||||
and
|
||||
(($tre == "") or (.title | test($tre; "i")))
|
||||
)
|
||||
| .address
|
||||
')
|
||||
|
||||
if [ -z "$MATCHING" ]; then
|
||||
"${COMMAND[@]}" &
|
||||
else
|
||||
while IFS= read -r ADDR; do
|
||||
[ -n "$ADDR" ] || continue
|
||||
hyprctl dispatch movetoworkspacesilent "special:$NAME,address:$ADDR"
|
||||
done <<< "$MATCHING"
|
||||
fi
|
||||
|
||||
hyprctl dispatch togglespecialworkspace "$NAME"
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Restore a minimized window by moving it out of a special workspace.
|
||||
#
|
||||
# Usage: unminimize-last.sh <name>
|
||||
# Example: unminimize-last.sh minimized
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${1:-minimized}"
|
||||
NAME="${NAME#special:}"
|
||||
SPECIAL_WS="special:${NAME}"
|
||||
|
||||
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ACTIVE_JSON="$(hyprctl -j activewindow 2>/dev/null || true)"
|
||||
ACTIVE_ADDR="$(printf '%s' "$ACTIVE_JSON" | jq -r '.address // empty')"
|
||||
ACTIVE_WS="$(printf '%s' "$ACTIVE_JSON" | jq -r '.workspace.name // empty')"
|
||||
MONITOR_ID="$(printf '%s' "$ACTIVE_JSON" | jq -r '.monitor // empty')"
|
||||
|
||||
# Destination is the normal active workspace for the active monitor.
|
||||
DEST_WS="$(
|
||||
hyprctl -j monitors 2>/dev/null \
|
||||
| jq -r --argjson mid "${MONITOR_ID:-0}" '.[] | select(.id == $mid) | .activeWorkspace.name' \
|
||||
| head -n 1 \
|
||||
|| true
|
||||
)"
|
||||
if [ -z "$DEST_WS" ] || [ "$DEST_WS" = "null" ]; then
|
||||
DEST_WS="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.name // empty' || true)"
|
||||
fi
|
||||
if [ -z "$DEST_WS" ] || [ "$DEST_WS" = "null" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If we're focused on a minimized window already, restore that one.
|
||||
ADDR=""
|
||||
if [ "$ACTIVE_WS" = "$SPECIAL_WS" ] && [ -n "$ACTIVE_ADDR" ] && [ "$ACTIVE_ADDR" != "null" ]; then
|
||||
ADDR="$ACTIVE_ADDR"
|
||||
else
|
||||
# Otherwise, restore the "most recent" minimized window we can find.
|
||||
# focusHistoryID tends to have 0 as most recent; pick the smallest value.
|
||||
ADDR="$(
|
||||
hyprctl -j clients 2>/dev/null \
|
||||
| jq -r --arg sw "$SPECIAL_WS" '
|
||||
[ .[]
|
||||
| select(.workspace.name == $sw)
|
||||
| { addr: .address, fh: (.focusHistoryID // 999999999) }
|
||||
]
|
||||
| sort_by(.fh)
|
||||
| (.[0].addr // empty)
|
||||
' \
|
||||
| head -n 1 \
|
||||
|| true
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -z "$ADDR" ] || [ "$ADDR" = "null" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
hyprctl dispatch movetoworkspacesilent "${DEST_WS},address:${ADDR}" >/dev/null 2>&1 || true
|
||||
hyprctl dispatch focuswindow "address:${ADDR}" >/dev/null 2>&1 || true
|
||||
|
||||
# If the minimized special workspace is currently visible, close it so we don't
|
||||
# leave things in a special state after a restore.
|
||||
SPECIAL_OPEN="$(
|
||||
hyprctl -j monitors 2>/dev/null \
|
||||
| jq -r --arg n "$SPECIAL_WS" --argjson mid "${MONITOR_ID:-0}" '
|
||||
.[]
|
||||
| select(.id == $mid)
|
||||
| (.specialWorkspace.name // "")
|
||||
| select(. == $n)
|
||||
' \
|
||||
| head -n 1 \
|
||||
|| true
|
||||
)"
|
||||
if [ -n "$SPECIAL_OPEN" ]; then
|
||||
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Source this file to get icon_for_class function.
|
||||
# Builds a mapping from window class → freedesktop icon name
|
||||
# by scanning .desktop files for StartupWMClass and Icon fields.
|
||||
#
|
||||
# Usage:
|
||||
# source "$(dirname "$0")/window-icon-map.sh"
|
||||
# icon=$(icon_for_class "google-chrome")
|
||||
|
||||
declare -A _WINDOW_ICON_MAP
|
||||
|
||||
_build_window_icon_map() {
|
||||
local IFS=':'
|
||||
local -a search_dirs=()
|
||||
local dir
|
||||
|
||||
for dir in ${XDG_DATA_DIRS:-/run/current-system/sw/share:/usr/share:/usr/local/share}; do
|
||||
[ -d "$dir/applications" ] && search_dirs+=("$dir/applications")
|
||||
done
|
||||
[ -d "$HOME/.local/share/applications" ] && search_dirs+=("$HOME/.local/share/applications")
|
||||
[ ${#search_dirs[@]} -eq 0 ] && return
|
||||
|
||||
# Expand globs per-directory so the pattern works correctly
|
||||
local -a desktop_files=()
|
||||
for dir in "${search_dirs[@]}"; do
|
||||
desktop_files+=("$dir"/*.desktop)
|
||||
done
|
||||
[ ${#desktop_files[@]} -eq 0 ] && return
|
||||
|
||||
# Single grep pass across all desktop files
|
||||
local -A file_icons file_wmclass
|
||||
local filepath line
|
||||
while IFS=: read -r filepath line; do
|
||||
case "$line" in
|
||||
Icon=*)
|
||||
[ -z "${file_icons[$filepath]:-}" ] && file_icons["$filepath"]="${line#Icon=}"
|
||||
;;
|
||||
StartupWMClass=*)
|
||||
[ -z "${file_wmclass[$filepath]:-}" ] && file_wmclass["$filepath"]="${line#StartupWMClass=}"
|
||||
;;
|
||||
esac
|
||||
done < <(grep -H '^Icon=\|^StartupWMClass=' "${desktop_files[@]}" 2>/dev/null)
|
||||
|
||||
# Build class → icon map
|
||||
local icon wm_class bn name
|
||||
for filepath in "${!file_icons[@]}"; do
|
||||
icon="${file_icons[$filepath]}"
|
||||
[ -n "$icon" ] || continue
|
||||
|
||||
wm_class="${file_wmclass[$filepath]:-}"
|
||||
if [ -n "$wm_class" ]; then
|
||||
_WINDOW_ICON_MAP["${wm_class,,}"]="$icon"
|
||||
fi
|
||||
|
||||
bn="${filepath##*/}"
|
||||
name="${bn%.desktop}"
|
||||
_WINDOW_ICON_MAP["${name,,}"]="$icon"
|
||||
done
|
||||
}
|
||||
|
||||
_build_window_icon_map
|
||||
|
||||
icon_for_class() {
|
||||
local class_lower="${1,,}"
|
||||
echo "${_WINDOW_ICON_MAP[$class_lower]:-$class_lower}"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
|
||||
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
|
||||
|
||||
ws="$(
|
||||
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -z "${ws}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
hyprctl dispatch workspace "${ws}" >/dev/null 2>&1 || true
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
|
||||
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
|
||||
|
||||
ws="$(
|
||||
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -z "${ws}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
hyprctl dispatch movetoworkspace "${ws}" >/dev/null 2>&1 || true
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
max_ws="${HYPR_MAX_WORKSPACE:-9}"
|
||||
delta="${1:-}"
|
||||
|
||||
case "${delta}" in
|
||||
+1|-1) ;;
|
||||
next) delta="+1" ;;
|
||||
prev) delta="-1" ;;
|
||||
*)
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
cur="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
|
||||
if ! [[ "${cur}" =~ ^[0-9]+$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( cur < 1 )); then
|
||||
cur=1
|
||||
elif (( cur > max_ws )); then
|
||||
cur="${max_ws}"
|
||||
fi
|
||||
|
||||
if [[ "${delta}" == "+1" ]]; then
|
||||
if (( cur >= max_ws )); then
|
||||
nxt=1
|
||||
else
|
||||
nxt=$((cur + 1))
|
||||
fi
|
||||
else
|
||||
if (( cur <= 1 )); then
|
||||
nxt="${max_ws}"
|
||||
else
|
||||
nxt=$((cur - 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
hyprctl dispatch workspace "${nxt}" >/dev/null 2>&1 || true
|
||||
|
||||
7
dotfiles/config/neowall/config.vibe
Normal file
7
dotfiles/config/neowall/config.vibe
Normal 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
|
||||
}
|
||||
7
dotfiles/config/neowall/screensaver.vibe
Normal file
7
dotfiles/config/neowall/screensaver.vibe
Normal 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
|
||||
}
|
||||
783
dotfiles/config/river-xmonad/Main.hs
Normal file
783
dotfiles/config/river-xmonad/Main.hs
Normal file
@@ -0,0 +1,783 @@
|
||||
{-# 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_x (toggleScratchpad "x-com")
|
||||
, 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 "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"]
|
||||
, ScratchpadDefinition "x-com" "x-com-pwa" $
|
||||
anyMatcher [appIdMatches "x-com-pwa", appIdContains "chrome-x.com"]
|
||||
]
|
||||
|
||||
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
|
||||
18
dotfiles/config/river-xmonad/imalison-river-xmonad.cabal
Normal file
18
dotfiles/config/river-xmonad/imalison-river-xmonad.cabal
Normal 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
|
||||
13
dotfiles/config/river-xmonad/overlay.nix
Normal file
13
dotfiles/config/river-xmonad/overlay.nix
Normal 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"
|
||||
]
|
||||
) { };
|
||||
});
|
||||
});
|
||||
}
|
||||
154
dotfiles/config/rofi/apple-frost.rasi
Normal file
154
dotfiles/config/rofi/apple-frost.rasi
Normal file
@@ -0,0 +1,154 @@
|
||||
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-soft: #0b102018;
|
||||
candidate-frost: #ffffff12;
|
||||
candidate: #18203372;
|
||||
candidate-active:#0a84ffd9;
|
||||
candidate-line: #ffffff16;
|
||||
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;
|
||||
height: 50px;
|
||||
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: 17;
|
||||
spacing: 0px;
|
||||
border: 0px;
|
||||
border-radius: 0px 0px 14px 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: @candidate-line;
|
||||
border-radius: 0px;
|
||||
/*
|
||||
* Rofi percentages are monitor-relative. Derive padding from:
|
||||
* 78% - 176px mainbox padding - 50px inputbar - 10px spacing - 2px border.
|
||||
*/
|
||||
padding: calc( ( 78% - 238px ) / 34 - 12px ) 11px calc( ( 78% - 238px ) / 34 - 13px ) 11px;
|
||||
spacing: 10px;
|
||||
}
|
||||
|
||||
element normal {
|
||||
background-color: @candidate-soft;
|
||||
}
|
||||
|
||||
element alternate {
|
||||
background-color: @candidate-soft;
|
||||
}
|
||||
|
||||
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-active;
|
||||
text-color: @text-on-dark;
|
||||
border: 0px 0px 1px 4px;
|
||||
border-color: @accent-soft;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/* colors */
|
||||
|
||||
* {
|
||||
al: #00000000;
|
||||
bg: #000000ff;
|
||||
se: #101010ff;
|
||||
fg: #FFFFFFff;
|
||||
ac: #EC7875ff;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user