Compare commits
801 Commits
master
...
codex/hypr
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 9c9af9f856 | |||
| c5a0ddd7b1 | |||
| d5abefe15e | |||
| 4dfaabc569 | |||
| 86d32eb0c8 | |||
| 119b250bcc | |||
| 63e5d1636d | |||
| ba9cc6b811 | |||
| c448b1d106 | |||
| 8ca8492b3b | |||
| c62d5df036 | |||
| 2ae2f56889 | |||
| f4387182e6 | |||
| b8ece14ea0 | |||
| ba2b24d436 | |||
| 975c9701ce | |||
| 92637b30e4 | |||
| 4e9e2408b5 | |||
| e799e7876a | |||
| dbaf9af3b6 | |||
| 89bfcba9fe | |||
| 3966f7acda | |||
| 31504d8e5f | |||
| c8941870e2 | |||
| 3bcbb70494 | |||
| 1a09aa134f | |||
| 431b4da1d1 | |||
| edc6e0309b | |||
| 9c901f84a3 | |||
| 156d04a4a8 | |||
| fe13ddd907 | |||
| 36a08c5c26 | |||
| f15aea9a9c | |||
| c3c0d54da4 | |||
| 9c2f244309 | |||
| b596c38cb9 | |||
| cde795dcae | |||
| 31fbcf73ac | |||
| 1b8c54d722 | |||
| 0ed6b7f3e1 | |||
| 1a7dd966f9 | |||
| 5ca4d2745e | |||
| 3a5aaa9351 | |||
| 7bec7d53d3 | |||
| 4f4c1b0a5c | |||
| bcee95fc23 | |||
| 1b7c6acf3f | |||
| 1a06382365 | |||
| 222228742f | |||
| d931e668da | |||
| f6423481b8 | |||
| aac1dcc78a | |||
| aa035663e9 | |||
| 4b97e6c5f1 | |||
| 5907395512 | |||
| 1bc595dc13 | |||
| 2ac7faf884 | |||
| ab2f057dd2 | |||
| 06cde76b84 | |||
| 7a07c2b308 | |||
| 6d9d5265b9 | |||
| 5f6b81c6c1 | |||
| ea6a127fa5 | |||
| f9b51ae635 | |||
| efcc20c730 | |||
| ddb0a85c68 | |||
| 0b1d058417 | |||
| ac1c958256 | |||
| a8d2e3bf36 | |||
| 6d6176d354 | |||
| 002d36cff7 | |||
| 64f84cc946 | |||
| fb9b526be4 | |||
| 7f1f34732a | |||
| 499c7a1c34 | |||
| 89f3220933 | |||
| b09f4d6131 | |||
| 425d97a844 | |||
| f420c442d8 | |||
| c8a0b2920c | |||
| 13becd22f7 | |||
| bae205a553 | |||
| 3cc412cb6b | |||
| b98c160268 | |||
| fce5ff3ea4 | |||
| aa2ccc3c02 | |||
| 4b2479afc5 | |||
| 429d8cc850 | |||
| dfbe961e3c | |||
| d78393ec35 | |||
| 8f974d11f6 | |||
| a61792a5d9 | |||
| 9442aef530 | |||
| bcf70f0183 | |||
| 3298e50089 | |||
| 851849c4ac | |||
| a1c0fa68a7 | |||
| 25f182ab99 | |||
| cc7c159ee4 | |||
| 934f41d9aa | |||
| f49946a3f5 | |||
| 546ea66941 | |||
| 51b9868ae4 | |||
| 7c30537caf | |||
| 34f30390a9 | |||
| 5d6a289e42 | |||
| 58e45bda3c | |||
| e0d053b0d9 | |||
| bd958027a9 | |||
| 7de1b406b2 | |||
| 34b02c927a | |||
| 337ee6872a | |||
| 4107edcc55 | |||
| f3fd6a7d95 | |||
| 26627e012f | |||
| e10f8cd76c | |||
| 7e7ca69c37 | |||
| c64045f2ee | |||
| 76fd5fc86f | |||
| 34c3ad0ff8 | |||
| fa34081247 | |||
| f49feaf378 | |||
| d29b03c475 | |||
| bbf1a99589 | |||
| 9da5c5f345 | |||
| 7a17338936 | |||
| 5ad7ccf6bb | |||
| 6cd3343614 | |||
| 5db63a2bb0 | |||
| 2cd0febd2f | |||
| dad13f5a19 | |||
| 0e0dba42ad | |||
| e3c7c9d809 | |||
| c8650dd0ed | |||
| 0fb27354a6 | |||
| 880d7047df | |||
| a55bc242a8 | |||
| a127ae9522 | |||
| 313eb0db81 | |||
| 72a035dd0c | |||
| efe8993d26 | |||
| f6d27a01c1 | |||
| f80d61cb74 | |||
| 13f477f300 | |||
| 16ae80b8a0 | |||
| 3140b4f7c5 | |||
| ba1cc41e87 | |||
| a031d4d987 | |||
| 48986b4892 | |||
| ec39b3953f | |||
| 32c8508963 | |||
| 46e36d62b0 | |||
| 9239eefff3 | |||
| c1764fd0ee | |||
| d9cbea9bcf | |||
| 311f142d35 | |||
| e5ae4da40d | |||
| 98ca024082 | |||
| f9835c7cff | |||
| 8b8cd36ef1 | |||
| 9972c16cf9 | |||
| 6e1db80e8a | |||
| 18fdda20e8 | |||
| 4823dc5e16 | |||
| 23ae84e176 | |||
| 860e2b029c | |||
| 005545613e | |||
| e51d2ec68f | |||
| 336c81178c | |||
| 1f4bbbf92b | |||
| 5d81e7c80c | |||
| dd2c4fa8a4 | |||
| 281e8b9a56 | |||
| 6a2f952fc3 | |||
| 2d29056ed5 | |||
| e2d7dcf0c2 | |||
| 8bffa0c2b0 | |||
| 77a71e0a10 | |||
| 9d5f048f8c | |||
| a9a7546a1f | |||
| 5860600bd0 | |||
| b0ecf2a0d7 | |||
| fb9a30b8a6 | |||
| 6e7a8f6f54 | |||
|
|
dc9c1d0de5 | ||
|
|
e2f69af4e5 | ||
|
|
988df4ba8d | ||
|
|
42c7aeb9e3 | ||
|
|
8722cac7d0 | ||
|
|
965a2ff070 | ||
| 37eb01bee2 | |||
| 728b71f484 | |||
| dbcbf951f0 | |||
| 8fdd9433c8 | |||
| f5ac66ecb6 | |||
| fc2eb80ee9 | |||
| c76cd297c0 | |||
| 1826a77384 | |||
| 2d28a20948 | |||
| 3f5b126cbe | |||
| f67638947a | |||
| 220f5b733c | |||
| 4f80e2d0bb | |||
| 17a0b280cd | |||
| 899ec139e9 | |||
| 68c92992a2 | |||
| 89f6495c67 | |||
| 272cfec5ee | |||
| f4ce538df7 | |||
| 8ea3adb49f | |||
| db1dd5e557 | |||
| 85b9b2c8ad | |||
| 3a4fafc9d4 | |||
| 588087270f | |||
| 3bf1d8dadb | |||
| b432d758a4 | |||
| 77dcec7849 | |||
| 2416f46a3c | |||
| 3af98f96bc | |||
| 6befc1c6ff | |||
| 7ff9efe0f4 | |||
| 97ac691f2c | |||
| a63f286bf2 | |||
| 5738a7b8ce | |||
| f1498ceb1e | |||
| 01d9d21a85 | |||
| e05832f5ca | |||
| 5b1c0d972f | |||
| 8db8c02304 | |||
| adf320a414 | |||
| 6c5d493561 | |||
| aea078c17a | |||
| 196bf9155e | |||
| 6af998dfd8 | |||
| 631c2cc09f | |||
| c01c96dde0 | |||
| a2eaa9c058 | |||
| 32e594ab22 | |||
| 67f4381e06 | |||
| 89529e7f62 | |||
| 6da3da52f8 | |||
| fc89fcee1a | |||
| 06bbd5b7cd | |||
| a7cab63fa7 | |||
| 292ef25fcd | |||
| 8775e41cb3 | |||
| f314db999c | |||
| e3e23f9c7a | |||
| ee16ba6aa1 | |||
| 78e6dce5eb | |||
| 92c7774539 | |||
| 110bbda132 | |||
| 8b8d044290 | |||
| 0fde36b099 | |||
| 57a3e0ebe3 | |||
| 0eb9a446b5 | |||
| 3c9ba043d0 | |||
| 5b480040a0 | |||
| 9e2a978911 | |||
| 9c1888f949 | |||
| 67c7290a43 | |||
| 8bcb531e56 | |||
| 8eac8fb6f4 | |||
| 67d1577ae8 | |||
| 0a8d812ee8 | |||
| 5a9e3c4894 | |||
| 0730e75088 | |||
| df79ced549 | |||
| a50dbc60e8 | |||
| 5b12d0dc70 | |||
| c9884b2825 | |||
| acb20132e3 | |||
| 0ddc3b4a65 | |||
| cd535b7b01 | |||
| 4753901ef1 | |||
| 8431dcc87d | |||
| 5a5fda5cc0 | |||
| 2769eeb5a9 | |||
| 71736af86d | |||
| d024712b78 | |||
| f35e26aff1 | |||
| aee881dcd7 | |||
| 8dde61a230 | |||
| fa81dac17f | |||
| 87e10caa75 | |||
| 808ad38753 | |||
| 2f3aa004f5 | |||
| 2162f058ff | |||
| dbbbb7ff5a | |||
| 5155c96195 | |||
| bc64378bbc | |||
| 26779e7011 | |||
| 468b3b9ea9 | |||
| e04ca29749 | |||
| 7499328179 | |||
| 50f167e5b2 | |||
| 1161cea530 | |||
| 43adf5a8fe | |||
| 4a6b178546 | |||
| 93ddab4e9e | |||
| 25a07dd7b2 | |||
| 9dba5fbad3 | |||
| 867abf7cee | |||
| 02abb5f8e4 | |||
| b284ceb716 | |||
| 134009cebc | |||
| 1e28cb10ab | |||
| eaba704fab | |||
| b1072f0528 | |||
| 94172a65b7 | |||
| a4e4ea3a94 | |||
| 2479c98476 | |||
| 955216bff7 | |||
| cca3383674 | |||
| 3fe67188e3 | |||
| f25d51c92d | |||
| ee17a8756f | |||
| 18168dbc59 | |||
| d6cedef14d | |||
| fe6d6fbcb8 | |||
| 93105993c6 | |||
| 30c4d55407 | |||
| b8da65b2fd | |||
| 5f2c84b645 | |||
| 463c8ff36b | |||
| 09ac9fa3d5 | |||
| 6395685ae9 | |||
| d7efe5b6a6 | |||
| c36d930eb5 | |||
| 616769fe60 | |||
| 7d099a2be3 | |||
| 096d3f543a | |||
| 266b142d34 | |||
| 97f65425dc | |||
| 4f29a0d48d | |||
| f9dbe7fb54 | |||
| f0c8c61732 | |||
| 63462694f5 | |||
| da28a0bd05 | |||
| 52def9b43d | |||
| eea3683aa7 | |||
| a91f58bf82 | |||
| 1c7b6437ce | |||
| 66a01a7b39 | |||
| fa2af8f000 | |||
| d3ea72b08d | |||
| f93f2c5d3e | |||
| 0228989600 | |||
| 280cf5f7ff | |||
| b1de4e57eb | |||
| 67708f4f7f | |||
| 531a10e1d8 | |||
| 53ab4688da | |||
| 5ca733194a | |||
| f9a0a5c9c0 | |||
| 2180c042de | |||
| cf59eddef9 | |||
| d8ff8a939a | |||
| 8efd44f69e | |||
| 7e397bd440 | |||
| 1b726532c1 | |||
| ecbe245a6d | |||
| 3010260cd1 | |||
| 741a72ebd9 | |||
| f8a3273643 | |||
| 80926700c1 | |||
| 781b5297ad | |||
| 73c766e828 | |||
| 6f16b36faa | |||
| aa286e6855 | |||
| 59f7f35aa0 | |||
| 55b9724b92 | |||
| 10019a13f2 | |||
| 5b69d613b8 | |||
| 67b92178a6 | |||
| c5f3de9bd9 | |||
| 8d6664d83b | |||
| 655b0c6f2d | |||
| 950149769d | |||
| e97b838cda | |||
| 913767bec7 | |||
| d7a748177b | |||
| 86d667c5e1 | |||
| 0fb5bc132a | |||
| 67d5bd793b | |||
| 0a6931cf77 | |||
| 63b99e4d67 | |||
| a8aefe3d5e | |||
| 01e84f24ba | |||
| 09b4ef28f8 | |||
| 4add566e8c | |||
| eb707d3168 | |||
| 2615253a7a | |||
| 284ae929a7 | |||
| c170442904 | |||
| 157741a93c | |||
| e9545219b3 | |||
| a1c31dcfc4 | |||
| 4d014978d2 | |||
| 3123aa6c3f | |||
| c12f56ff86 | |||
| 7b52fb1f4b | |||
| 111afc60c0 | |||
| df9b5e61c9 | |||
| bdcba389cc | |||
| ba6baa8628 | |||
| ddaf752a68 | |||
| a8cb334938 | |||
| c7da4f35ce | |||
| 9fc1ddc885 | |||
| fc45f40b97 | |||
| 615a265def | |||
| ede5ccf540 | |||
| 8b888f02a5 | |||
| 1e8a591b4f | |||
| 4c89d204d5 | |||
| bf2ec862a9 | |||
| f80340fee7 | |||
| de89725abb | |||
| 479bea8ca5 | |||
| 6b1d25cdc6 | |||
| 24fe3fbb6b | |||
| c7a0ddcc62 | |||
| 89add17016 | |||
| 6a388676df | |||
| 319ae7ba6f | |||
| 9fcb5c073c | |||
| 8ae1b2d906 | |||
| 781a64aa92 | |||
| f766fbc8dc | |||
| 986e414570 | |||
| e4cb9e4861 | |||
| 9b5d515acc | |||
| 151ee9b4c5 | |||
| b8030e39e0 | |||
| 8a6081e736 | |||
| 043d858289 | |||
| 72cd82be13 | |||
| 8c8fbe774c | |||
| ece1c8eac8 | |||
| eff7ff726f | |||
| 482d0446d5 | |||
| efc50ec104 | |||
| d97026eb6c | |||
| 38e7b8c3eb | |||
| b94c561c9f | |||
| 42fa28f155 | |||
| 558f2b9b0d | |||
| 1af5b5563b | |||
| 782ac9fbd0 | |||
| ed752f37c5 | |||
| 6d3d0c56d2 | |||
| fc887b5526 | |||
| 76495da0b7 | |||
| 3a4aac3797 | |||
| 36d421876f | |||
| 9a8c158e03 | |||
| 02595898e9 | |||
| 2f620dfff3 | |||
| 5641d31580 | |||
| bfdbaa7752 | |||
| 29f5f6baa4 | |||
| b70800da59 | |||
| 3f9ac8edb9 | |||
| a7e409f826 | |||
| c43fbe23d7 | |||
| a7e70c75e3 | |||
| 1a10851887 | |||
| 9e5e5f091c | |||
| f476c6a97f | |||
| e3e49d51f4 | |||
| b2a6695690 | |||
| f731bb312a | |||
| 03bf952815 | |||
| b1d16ea86d | |||
| 5d1b538579 | |||
| 6d9cddbaeb | |||
| dfbd444649 | |||
| e5b77ae168 | |||
| c307821b8d | |||
| d68a27bf88 | |||
| 47786404a2 | |||
| 03d86c452a |
18
.github/workflows/cachix.yml
vendored
@@ -1,22 +1,20 @@
|
||||
name: Build and Push Cachix (NixOS)
|
||||
name: Build and Push Cachix (imalison-taffybar)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "nixos/**"
|
||||
- "org-agenda-api/**"
|
||||
- "dotfiles/config/taffybar/**"
|
||||
- ".github/workflows/cachix.yml"
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "nixos/**"
|
||||
- "org-agenda-api/**"
|
||||
- "dotfiles/config/taffybar/**"
|
||||
- ".github/workflows/cachix.yml"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
nixos-strixi-minaj:
|
||||
imalison-taffybar:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
@@ -51,9 +49,6 @@ jobs:
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v16
|
||||
|
||||
- name: Use GitHub Actions Cache for /nix/store
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v7
|
||||
|
||||
- name: Require Cachix config (push only)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
@@ -85,11 +80,10 @@ jobs:
|
||||
name: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
skipPush: true
|
||||
|
||||
- name: Build NixOS system (strixi-minaj)
|
||||
- name: Build imalison-taffybar
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
nix build \
|
||||
--no-link \
|
||||
--print-build-logs \
|
||||
./nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel \
|
||||
--override-input railbird-secrets ./nixos/ci/railbird-secrets-stub
|
||||
./dotfiles/config/taffybar#defaultPackage.x86_64-linux
|
||||
|
||||
3
.gitignore
vendored
@@ -21,6 +21,8 @@
|
||||
gotools
|
||||
/dotfiles/config/xmonad/result
|
||||
/dotfiles/config/taffybar/result
|
||||
/nix-darwin/result
|
||||
/nixos/result
|
||||
/dotfiles/emacs.d/*.sqlite
|
||||
/dotfiles/config/gtk-3.0/colors.css
|
||||
/dotfiles/config/gtk-3.0/settings.ini
|
||||
@@ -38,6 +40,7 @@ gotools
|
||||
/dotfiles/config/xmonad/dist-newstyle/
|
||||
/dotfiles/config/hypr/hyprscratch.conf
|
||||
/.worktrees/
|
||||
/result
|
||||
|
||||
# Secrets and machine-local state (managed via agenix/pass instead of git)
|
||||
/dotfiles/config/asciinema/config
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dotfiles/emacs.d/README.org
|
||||
228
README.org
Normal file
@@ -0,0 +1,228 @@
|
||||
# -*- mode: org; -*-
|
||||
#+TITLE: colonelpanic8's Dotfiles
|
||||
|
||||
This repository is the source of truth for my machines, user environment, and a
|
||||
large set of day-to-day workflow scripts. It started as an Emacs configuration,
|
||||
and that is still here, but the repo is now mostly a Nix-managed personal
|
||||
systems repo: NixOS hosts, a nix-darwin host, Home Manager link management,
|
||||
desktop/window-manager configuration, shell tooling, agent configuration, and
|
||||
org-agenda-api deployment glue.
|
||||
|
||||
The old literate Emacs README lives at [[file:dotfiles/emacs.d/README.org][dotfiles/emacs.d/README.org]]. The
|
||||
published GitHub Pages site is still generated from that document.
|
||||
|
||||
* What This Manages
|
||||
|
||||
- NixOS systems under [[file:nixos/][nixos/]], with one flake configuration per file in
|
||||
[[file:nixos/machines/][nixos/machines/]].
|
||||
- A nix-darwin configuration under [[file:nix-darwin/][nix-darwin/]] for the macOS machine.
|
||||
- Shared Nix modules and overlays under [[file:nix-shared/][nix-shared/]].
|
||||
- Home Manager placement of files from [[file:dotfiles/][dotfiles/]] into =$HOME= and
|
||||
=$XDG_CONFIG_HOME=.
|
||||
- Shell functions and executable helpers in [[file:dotfiles/lib/][dotfiles/lib/]], added to
|
||||
=PATH= and =fpath= by the NixOS environment module.
|
||||
- Desktop environment and tiling-window-manager configuration for Hyprland,
|
||||
XMonad, River/XMonad experiments, Taffybar, Waybar, Rofi, Alacritty,
|
||||
autorandr, and related utilities.
|
||||
- Emacs and org-mode configuration, including the tangled org configuration used
|
||||
by the org-agenda-api container.
|
||||
- Agent and tool configuration for Codex, Claude, project guides, and local
|
||||
task-specific skills.
|
||||
- Container and deployment configuration for personal org-agenda-api instances.
|
||||
|
||||
This is not intended to be a generic starter dotfiles repo. Many modules assume
|
||||
my users, hostnames, hardware, SSH keys, secrets layout, and local checkout path
|
||||
=(~/dotfiles=). It is still useful as a reference for how the pieces fit
|
||||
together.
|
||||
|
||||
* Layout
|
||||
|
||||
| Path | Purpose |
|
||||
|------+---------|
|
||||
| [[file:nixos/][nixos/]] | Main NixOS flake. Imports feature modules, host files, agenix secrets, Home Manager, overlays, and package checks. |
|
||||
| [[file:nixos/machines/][nixos/machines/]] | Per-host NixOS entrypoints such as =strixi-minaj=, =ryzen-shine=, =railbird-sf=, WSL hosts, and Raspberry Pi hosts. |
|
||||
| [[file:nix-darwin/][nix-darwin/]] | macOS system flake using nix-darwin, nix-homebrew, Home Manager, agenix, and shared packages. |
|
||||
| [[file:nix-shared/][nix-shared/]] | Shared package lists, overlays, Home Manager modules, and Syncthing fragments used by Linux and macOS. |
|
||||
| [[file:dotfiles/][dotfiles/]] | Files that are linked into the home directory. Top-level entries become dotfiles; =dotfiles/config/*= becomes XDG config. |
|
||||
| [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] | User commands and desktop helpers, including Rofi scripts, Hyprland helpers, audio controls, and Syncthing utilities. |
|
||||
| [[file:dotfiles/lib/functions/][dotfiles/lib/functions/]] | Zsh autoload functions and shell helpers. |
|
||||
| [[file:dotfiles/config/hypr/][dotfiles/config/hypr/]] | Hyprland Lua config, lock/idle config, workspace files, scripts, and plugin state. |
|
||||
| [[file:dotfiles/config/xmonad/][dotfiles/config/xmonad/]] | XMonad configuration, local Cabal package, flake, and upstream submodules. |
|
||||
| [[file:dotfiles/config/taffybar/][dotfiles/config/taffybar/]] | Personal Taffybar package/configuration, CSS themes, scripts, and local upstream checkout. |
|
||||
| [[file:dotfiles/emacs.d/][dotfiles/emacs.d/]] | Emacs configuration, literate org config, org-mode setup, snippets, and generated/tangled Elisp. |
|
||||
| [[file:dotfiles/agents/][dotfiles/agents/]] | Agent instructions, project constellation guides, and local Codex skills. |
|
||||
| [[file:org-agenda-api/][org-agenda-api/]] | Instance-specific config and container/deploy glue for org-agenda-api. |
|
||||
| [[file:docs/][docs/]] | Design notes for Cachix, tiling WM behavior, River evaluation, and org-agenda-api consolidation. |
|
||||
| [[file:gen-gh-pages/][gen-gh-pages/]] | Legacy/publication pipeline that exports the Emacs README to GitHub Pages. |
|
||||
|
||||
* NixOS
|
||||
|
||||
The NixOS flake is [[file:nixos/flake.nix][nixos/flake.nix]]. It discovers host configurations from
|
||||
[[file:nixos/machines/][nixos/machines/]] and exposes them as =nixosConfigurations.<hostname>=.
|
||||
The broad feature set is assembled by [[file:nixos/configuration.nix][nixos/configuration.nix]], where
|
||||
=features.full.enable= expands into the normal desktop/profile modules.
|
||||
|
||||
Common workflow:
|
||||
|
||||
#+begin_src sh
|
||||
cd ~/dotfiles/nixos
|
||||
just switch
|
||||
#+end_src
|
||||
|
||||
The local =just switch= recipe wraps =nixos-rebuild switch --flake ".#"=, waits
|
||||
for an already-running switch to finish, and overrides the Taffybar inputs to
|
||||
the live checkout under this repo. Use it instead of running =nixos-rebuild=
|
||||
directly.
|
||||
|
||||
Useful variants:
|
||||
|
||||
#+begin_src sh
|
||||
cd ~/dotfiles/nixos
|
||||
just switch-remote
|
||||
just switch-local-taffybar
|
||||
just remote-switch <host>
|
||||
#+end_src
|
||||
|
||||
Build/check examples:
|
||||
|
||||
#+begin_src sh
|
||||
nix flake check ~/dotfiles/nixos
|
||||
nix build ~/dotfiles/nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel
|
||||
#+end_src
|
||||
|
||||
The flake also exposes package/check outputs for Hyprland plugins and a
|
||||
Hyprland Lua config syntax/verification check.
|
||||
|
||||
* nix-darwin
|
||||
|
||||
The macOS configuration lives in [[file:nix-darwin/flake.nix][nix-darwin/flake.nix]]. It uses
|
||||
nix-darwin, nix-homebrew, Home Manager, agenix, and the shared package list in
|
||||
[[file:nix-shared/system/essential.nix][nix-shared/system/essential.nix]].
|
||||
|
||||
Common workflow:
|
||||
|
||||
#+begin_src sh
|
||||
cd ~/dotfiles/nix-darwin
|
||||
just switch
|
||||
#+end_src
|
||||
|
||||
The active host configuration is =mac-demarco-mini=. There is also a
|
||||
=mac-demarco-mini-imalison= target used while migrating the primary macOS user.
|
||||
|
||||
* Home File Linking
|
||||
|
||||
The NixOS Home Manager module [[file:nixos/dotfiles-links.nix][nixos/dotfiles-links.nix]] reproduces the useful
|
||||
part of =rcm/rcup=:
|
||||
|
||||
- files under [[file:dotfiles/][dotfiles/]] are linked into =$HOME= with a leading dot;
|
||||
- directories under [[file:dotfiles/config/][dotfiles/config/]] are linked into =$XDG_CONFIG_HOME=;
|
||||
- links are out-of-store symlinks, so editing the checkout updates runtime
|
||||
config immediately;
|
||||
- generated or special directories such as =codex=, =lib=, =config=, and
|
||||
=emacs.d= are handled separately.
|
||||
|
||||
On NixOS, shell scripts belong in [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] and autoloaded shell
|
||||
functions belong in [[file:dotfiles/lib/functions/][dotfiles/lib/functions/]]. [[file:nixos/environment.nix][nixos/environment.nix]] adds
|
||||
those paths to the shell environment.
|
||||
|
||||
The nix-darwin Home Manager module in [[file:nix-darwin/home/common.nix][nix-darwin/home/common.nix]] uses the
|
||||
same basic idea for macOS, with extra launchd, GPG, Raycast, Homebrew, and agent
|
||||
setup.
|
||||
|
||||
* Desktop Stack
|
||||
|
||||
The desktop setup is modular. [[file:nixos/desktop.nix][nixos/desktop.nix]] enables the common desktop
|
||||
surface, while individual modules layer in window managers, panels, launchers,
|
||||
notifications, SNI/tray support, fonts, and app defaults.
|
||||
|
||||
The currently important pieces are:
|
||||
|
||||
- Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], with imported Lua
|
||||
modules under [[file:dotfiles/config/hypr/hyprland/][dotfiles/config/hypr/hyprland/]], backed by custom plugin inputs in
|
||||
the NixOS flake.
|
||||
- XMonad configuration in [[file:dotfiles/config/xmonad/xmonad.hs][dotfiles/config/xmonad/xmonad.hs]], with upstream
|
||||
=xmonad= and =xmonad-contrib= available as submodules/checkouts.
|
||||
- Taffybar configuration in [[file:dotfiles/config/taffybar/taffybar.hs][dotfiles/config/taffybar/taffybar.hs]], plus a local
|
||||
flake and scripts for restart, screenshots, and SNI debugging.
|
||||
- Waybar, Rofi, autorandr, Alacritty, Zellij, and miscellaneous app configs
|
||||
under [[file:dotfiles/config/][dotfiles/config/]].
|
||||
|
||||
The intended tiling-WM behavior is documented in
|
||||
[[file:docs/tiling-wm-experience.md][docs/tiling-wm-experience.md]].
|
||||
|
||||
* Emacs And Org
|
||||
|
||||
Emacs is still a major part of the repo, just no longer the only thing here.
|
||||
The main files are:
|
||||
|
||||
- [[file:dotfiles/emacs.d/README.org][dotfiles/emacs.d/README.org]]: the original literate Emacs README.
|
||||
- [[file:dotfiles/emacs.d/init.el][dotfiles/emacs.d/init.el]] and [[file:dotfiles/emacs.d/early-init.el][early-init.el]]: runtime entrypoints.
|
||||
- [[file:dotfiles/emacs.d/org-config.org][dotfiles/emacs.d/org-config.org]]: the org-mode configuration that is tangled
|
||||
for normal Emacs and for org-agenda-api.
|
||||
- [[file:gen-gh-pages/][gen-gh-pages/]] and [[file:.github/workflows/gh-pages.yml][.github/workflows/gh-pages.yml]]: export the Emacs README
|
||||
to the public GitHub Pages site.
|
||||
|
||||
* org-agenda-api
|
||||
|
||||
The repo carries the personal integration layer for
|
||||
[[https://github.com/colonelpanic8/org-agenda-api][org-agenda-api]].
|
||||
[[file:nixos/org-agenda-api.nix][nixos/org-agenda-api.nix]] tangles the org-mode configuration from
|
||||
[[file:dotfiles/emacs.d/org-config.org][dotfiles/emacs.d/org-config.org]]. [[file:org-agenda-api/container.nix][org-agenda-api/container.nix]] combines that
|
||||
tangled config with per-instance loaders under [[file:org-agenda-api/configs/][org-agenda-api/configs/]] and
|
||||
builds OCI containers exposed by the NixOS flake.
|
||||
|
||||
The host-side NixOS module [[file:nixos/org-agenda-api-host.nix][nixos/org-agenda-api-host.nix]] runs the container
|
||||
behind nginx with ACME certificates and Podman.
|
||||
|
||||
To enter the deployment shell:
|
||||
|
||||
#+begin_src sh
|
||||
nix develop ~/dotfiles/nixos#org-agenda-api
|
||||
#+end_src
|
||||
|
||||
* Secrets
|
||||
|
||||
Secrets are intentionally not stored as plaintext in the repo. Nix-managed
|
||||
secrets use agenix files under [[file:nixos/secrets/][nixos/secrets/]]. Runtime credentials and
|
||||
personal service passwords live in =pass=. Modules and scripts should consume
|
||||
secrets from those sources at runtime rather than checking derived values into
|
||||
git.
|
||||
|
||||
* Submodules And Local Checkouts
|
||||
|
||||
Some third-party or upstream projects are tracked as submodules:
|
||||
|
||||
- =dotfiles/config/taffybar/taffybar=
|
||||
- =dotfiles/config/xmonad/xmonad=
|
||||
- =dotfiles/config/xmonad/xmonad-contrib=
|
||||
- =dotfiles/config/alacritty/themes=
|
||||
- =nixos/railbird.ai=
|
||||
|
||||
Clone with submodules when bootstrapping a new checkout:
|
||||
|
||||
#+begin_src sh
|
||||
git clone --recurse-submodules git@github.com:IvanMalison/dotfiles.git ~/dotfiles
|
||||
#+end_src
|
||||
|
||||
This repo also contains project-local git worktrees under =.worktrees/= during
|
||||
active development. Those are machine-local working state and are ignored.
|
||||
|
||||
* CI And Caches
|
||||
|
||||
[[file:.github/workflows/cachix.yml][.github/workflows/cachix.yml]] can build the =strixi-minaj= NixOS closure and
|
||||
push paths to Cachix.
|
||||
|
||||
The top-level [[file:justfile][justfile]] contains helper commands for populating the
|
||||
=colonelpanic8-dotfiles= Cachix cache from a local machine.
|
||||
|
||||
* Working In This Repo
|
||||
|
||||
- Prefer Nix modules for system-level behavior and Home Manager modules for
|
||||
user-level placement and services.
|
||||
- Put user commands in [[file:dotfiles/lib/bin/][dotfiles/lib/bin/]] and shell functions in
|
||||
[[file:dotfiles/lib/functions/][dotfiles/lib/functions/]].
|
||||
- Run NixOS switches from [[file:nixos/][nixos/]] with =just switch=.
|
||||
- Run macOS switches from [[file:nix-darwin/][nix-darwin/]] with =just switch=.
|
||||
- Keep host-specific behavior in [[file:nixos/machines/][nixos/machines/]] where possible.
|
||||
- Do not commit secrets or generated local state; use agenix, =pass=, or ignored
|
||||
machine-local files.
|
||||
@@ -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,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
|
||||
437
docs/tiling-wm-experience.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# Tiling WM Experience Spec
|
||||
|
||||
This document describes the tiling window manager experience I am targeting.
|
||||
|
||||
## Priority Levels
|
||||
|
||||
- Required: daily-driver behavior.
|
||||
- Important: expected for parity, but a rough first version is acceptable.
|
||||
- Nice: useful polish or compatibility.
|
||||
|
||||
## Modifier Terminology
|
||||
|
||||
- `Super` names the physical modifier key often labeled Windows, Command, GUI,
|
||||
or OS depending on the keyboard.
|
||||
- `Hyper` means a higher-order logical modifier layer used for monitor,
|
||||
workspace, utility, and cross-context operations.
|
||||
- Prefer implementing `Hyper` as its own virtual modifier or equivalent logical
|
||||
mask when the environment supports that.
|
||||
- If a dedicated virtual `Hyper` mask is not practical, `Ctrl+Alt+Super` is the
|
||||
fallback chord.
|
||||
- The fallback `Hyper` chord intentionally does not include `Shift`; portable
|
||||
`Hyper` bindings only use the plain `Hyper` layer and the `Hyper+Shift`
|
||||
layer.
|
||||
- Do not require `Hyper+Ctrl`, `Hyper+Alt`, or `Hyper+Super` bindings. Those
|
||||
modifiers may already be part of the fallback `Hyper` chord.
|
||||
- Binding descriptions should use `Super` and `Hyper` rather than
|
||||
hardware-vendor names.
|
||||
|
||||
## Workspaces and Monitors
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Workspaces are a shared global set, not independent per-monitor namespaces.
|
||||
- Focusing workspace `N` shows workspace `N` on the currently focused monitor.
|
||||
- Moving a window to workspace `N` does not require caring which monitor
|
||||
currently owns that workspace.
|
||||
- Sending the focused window to workspace `N` without following it is a
|
||||
first-class operation.
|
||||
- Moving the focused window to workspace `N` and following it is a first-class
|
||||
operation.
|
||||
- Sending the focused window to the next empty workspace without following it is
|
||||
a first-class operation.
|
||||
- Moving the focused window to the next empty workspace and following it is a
|
||||
first-class operation.
|
||||
- Normal workspaces are bounded to `1..9`.
|
||||
|
||||
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
|
||||
focus on the original monitor.
|
||||
- Moving the focused window to another monitor and following it moves keyboard
|
||||
focus to the destination monitor.
|
||||
- Hidden/special workspaces exist for scratchpad state.
|
||||
- Hidden/special workspaces exist for minimized state.
|
||||
- Hidden/special workspaces are excluded from ordinary workspace cycling.
|
||||
- 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:
|
||||
|
||||
- Directional window focus is available.
|
||||
- Directional window swapping or movement is available.
|
||||
- Directional move-to-monitor is available while preserving useful focus.
|
||||
- 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.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- Keyboard resize remains available, but it should not displace the directional
|
||||
move-to-monitor binding.
|
||||
|
||||
## Pointer Focus
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Focus-follows-mouse, or an equivalent pointer-driven focus model, is enabled.
|
||||
- Moving the pointer over a managed window focuses that window without requiring
|
||||
a click.
|
||||
- Mouse-follows-focus is also enabled: keyboard or programmatic focus changes
|
||||
move the pointer into the newly focused window.
|
||||
|
||||
## Layouts
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Tiling is dynamic.
|
||||
- Primary layout is equal-width vertical columns.
|
||||
- Scrolling layouts are not acceptable.
|
||||
- All ordinary splits are vertical.
|
||||
- Adding windows dynamically redistributes all tiled windows evenly.
|
||||
- Newly tiled windows are inserted near the currently focused tile, not
|
||||
appended to the far end of the workspace.
|
||||
- Removing windows dynamically redistributes all tiled windows evenly.
|
||||
- Ordinary use should not require manually managing a split tree.
|
||||
- Tabbed/fullscreen-style monocle layout is available.
|
||||
- Directional window navigation bindings continue to switch windows in
|
||||
tabbed/fullscreen mode.
|
||||
- The important layouts are columns and tabbed/fullscreen.
|
||||
- Dialogs float.
|
||||
- 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:
|
||||
|
||||
- One-window workspaces should have no visible gaps or use smart gaps.
|
||||
|
||||
Nice behavior:
|
||||
|
||||
- Gaps can be toggled.
|
||||
- Smart borders can be toggled.
|
||||
- Layout-related modifiers remain available for experiments.
|
||||
- Inactive windows are slightly dimmed when supported.
|
||||
|
||||
## Overview and Discovery
|
||||
|
||||
Required behavior:
|
||||
|
||||
- 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.
|
||||
- Window picker entries show titles.
|
||||
- Window picker entries show workspace labels.
|
||||
- Go-to-window focuses the selected window wherever it currently lives.
|
||||
- Bring-window moves a selected non-visible window to the current workspace and
|
||||
focuses it.
|
||||
- Replace-window swaps the focused window with a selected window where feasible.
|
||||
|
||||
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.
|
||||
|
||||
## Scratchpads
|
||||
|
||||
Required behavior:
|
||||
|
||||
- A named scratchpad exists for codex.
|
||||
- A named scratchpad exists for element.
|
||||
- A named scratchpad exists for htop.
|
||||
- A named scratchpad exists for slack.
|
||||
- A named scratchpad exists for spotify.
|
||||
- A named scratchpad exists for transmission.
|
||||
- A named scratchpad exists for volume.
|
||||
- Scratchpads appear near-fullscreen and centered by default.
|
||||
- Toggling a scratchpad deactivates fullscreen/tabbed state first.
|
||||
- Scratchpads are hidden from normal workspace and window listings.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- A dropdown terminal scratchpad exists.
|
||||
- Scratchpad matching handles delayed class/title assignment.
|
||||
- Scratchpad behavior is robust when the app is already running.
|
||||
- Scratchpad behavior is robust when the app is minimized.
|
||||
- Scratchpad behavior is robust when the app is on another workspace.
|
||||
|
||||
## Minimization
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Focused window can be minimized.
|
||||
- Last minimized window can be restored to the current workspace and focused.
|
||||
- Minimized windows are excluded from normal layout.
|
||||
- Minimized windows are excluded from ordinary go/bring lists.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- A minimized picker mode exists.
|
||||
- Restore-all-minimized exists.
|
||||
- Other classes in the current workspace can be minimized.
|
||||
- Windows of the focused class can be restored.
|
||||
- All minimized windows can be restored.
|
||||
|
||||
## Class-Aware Workflows
|
||||
|
||||
Important behavior:
|
||||
|
||||
- Gather all windows of the focused class onto the current workspace.
|
||||
- Raise-or-spawn exists for the browser.
|
||||
- Window menus show class.
|
||||
- Window menus show title.
|
||||
- Window menus show workspace.
|
||||
- Window menus show icon.
|
||||
|
||||
## Status Bar Contract
|
||||
|
||||
Required behavior:
|
||||
|
||||
- The status bar can list normal workspaces.
|
||||
- The status bar can identify the active workspace per monitor.
|
||||
- The status bar can list windows per workspace.
|
||||
- The status bar can expose class hints for each listed window.
|
||||
- The status bar can expose title for each listed window.
|
||||
- The status bar can expose active state for each listed window.
|
||||
- The status bar can expose minimized state when available.
|
||||
- The status bar can expose urgency when available.
|
||||
- The status bar can expose approximate window position when available.
|
||||
- Scratchpad workspaces are marked as special or filtered out.
|
||||
- Minimized workspaces are marked as special or filtered out.
|
||||
- Internal workspaces are marked as special or filtered out.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- Workspace labels are stable.
|
||||
- Workspace icons are stable.
|
||||
- Window positioning information is available enough for workspace icon strips
|
||||
and future expose-like views.
|
||||
- Layout information is available enough for workspace icon strips and future
|
||||
expose-like views.
|
||||
- Layout name is exposed if practical.
|
||||
- Layout state is exposed if practical.
|
||||
|
||||
## Session and Utility Behavior
|
||||
|
||||
Important behavior:
|
||||
|
||||
- Terminal is `ghostty --gtk-single-instance=false`.
|
||||
- Launcher is `rofi -show drun -show-icons`.
|
||||
- Run menu is `rofi -show run`.
|
||||
- Browser raise/spawn behavior exists.
|
||||
- Border width is effectively zero.
|
||||
- The status bar can be toggled per monitor.
|
||||
- Session startup integrates with the normal graphical-session target.
|
||||
- Session startup integrates with any required session-specific user target.
|
||||
|
||||
Nice behavior:
|
||||
|
||||
- Wallpaper behavior remains consistent.
|
||||
- Wallpaper selection uses `Hyper+comma`; `Hyper+w/a/s/d` are reserved for
|
||||
directional monitor focus.
|
||||
- Idle behavior remains consistent.
|
||||
- Lock behavior remains consistent.
|
||||
- Clipboard history behavior remains consistent.
|
||||
- Screenshot behavior remains consistent.
|
||||
- Monitor DDC/input switching remains consistent.
|
||||
- Rofi utility bindings remain consistent.
|
||||
- Media keys remain consistent.
|
||||
|
||||
## Binding Appendix
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `Hyper` bindings should remain available from a single physical key where
|
||||
practical, even if that key emits the fallback chord internally.
|
||||
- Extra modifiers on `Hyper` are limited to `Shift` for portable bindings.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `Hyper` utility bindings must not displace required directional monitor
|
||||
bindings on `Hyper+w/a/s/d`.
|
||||
|
||||
### Core Bindings
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `Super+p` opens the application launcher.
|
||||
- `Super+Shift+p` opens the run menu.
|
||||
- `Super+Shift+Return` opens a terminal.
|
||||
- `Super+q` reloads the window manager config.
|
||||
- `Super+Shift+c` closes the focused window.
|
||||
- `Super+Shift+q` exits the window manager session.
|
||||
- `Super+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+Shift+e` moves the focused window to the next empty workspace and
|
||||
follows it. This is the target replacement for the older `Super+Shift+h`
|
||||
binding.
|
||||
- `Hyper+e` focuses the next empty workspace.
|
||||
- `Hyper+1` toggles inactive-window opacity reduction for the focused window.
|
||||
- `Hyper+5` swaps the current workspace with a selected workspace.
|
||||
- `Hyper+g` gathers windows of the focused class onto the current workspace.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `Super+Tab` opens the visual window overview.
|
||||
- `Super+Shift+Tab` opens the visual window overview scoped to non-visible
|
||||
windows or bring-window mode when supported.
|
||||
- `Alt+Tab` opens the visual workspace expose.
|
||||
- `Alt+Shift+Tab` opens the visual workspace expose in bring-window mode when
|
||||
supported.
|
||||
- Within visual window overview, `w/a/s/d`, `h/j/k/l`, and arrow keys move the
|
||||
selection directionally.
|
||||
- Within visual window overview, `Return`, `Space`, `g`, or `f` activates the
|
||||
selected window.
|
||||
- Within visual window overview, `b`, `Shift+Return`, or `Shift+Space` brings
|
||||
the selected window to the current workspace.
|
||||
- Within visual window overview, `Shift+b` replaces the focused window with the
|
||||
selected window when supported.
|
||||
- Within visual window overview, `Escape` or `q` closes the overview.
|
||||
- `Super+\` starts or advances current-monitor workspace history cycling.
|
||||
- `Super+/` reverses current-monitor workspace history cycling while the
|
||||
initiating `Super` key is held.
|
||||
- Releasing the initiating `Super` key commits the workspace history cycle.
|
||||
|
||||
### Directional Navigation Bindings
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
### Numbered Workspace Bindings
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `Super+1..9` focuses workspace `1..9` on the current monitor.
|
||||
- `Super+Shift+1..9` sends the focused window to workspace `1..9` without
|
||||
following it.
|
||||
- `Super+Ctrl+1..9` sends the focused window to workspace `1..9` and follows
|
||||
it.
|
||||
|
||||
### Scratchpad Bindings
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `Super+Alt+c` toggles the codex scratchpad.
|
||||
- `Super+Alt+e` toggles the element scratchpad.
|
||||
- `Super+Alt+h` toggles the htop 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.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `Super+Alt+grave` toggles the dropdown terminal scratchpad.
|
||||
- `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
|
||||
scratchpad-adjacent bindings.
|
||||
|
||||
### Utility Bindings
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `Hyper+v` opens clipboard history with a rofi-backed clipboard command
|
||||
such as `greenclip print` or `cliphist`.
|
||||
- `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 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`.
|
||||
- `Hyper+r` opens the systemd/service menu with `rofi-systemd`.
|
||||
- `Hyper+slash` toggles the status bar with the status-bar-appropriate command.
|
||||
- `Hyper+backslash` toggles the monitor input with `mpg341cx_input toggle`.
|
||||
- `Hyper+i` opens the audio input selector with `rofi_select_input.hs`.
|
||||
- `Hyper+o` opens the audio output selector with `rofi_paswitch`.
|
||||
- `Hyper+y` opens the agentic skill picker with `rofi_agentic_skill`.
|
||||
- `Hyper+Shift+l` locks the session with the compositor/session-appropriate
|
||||
locker.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- Wallpaper selection is available under `Hyper` via `rofi_wallpaper.sh`, but
|
||||
its exact key must avoid the required `Hyper+w/a/s/d` directional monitor
|
||||
bindings.
|
||||
- Expose-style overview remains available as a utility binding using the
|
||||
compositor-appropriate implementation.
|
||||
- Session-destructive operations use shifted or otherwise harder-to-hit
|
||||
variants.
|
||||
@@ -1,22 +1,20 @@
|
||||
# Agentic Session Preferences
|
||||
|
||||
## Tmux session titling
|
||||
- Treat this chat as the controller for the current tmux session.
|
||||
- Run the tmux rename one-liner eagerly without checking whether TMUX is set first.
|
||||
- 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.
|
||||
## 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 describes the durable purpose of the overall exchange.
|
||||
- Prefer automatic titling: infer a concise <task> from the current user request and the existing chat context without asking.
|
||||
- Choose holistic titles over granular turn summaries. The title should answer "what has this chat been for?" rather than describe the latest command, substep, clarification, or follow-up message.
|
||||
- Preserve the existing <task> when the new user turn is a continuation, status check, refinement, or implementation detail within the same broader objective.
|
||||
- Title format: "<project> - <task>".
|
||||
- <project> is the basename of the current project directory.
|
||||
- Prefer git repo root basename if available; otherwise use basename of the current working directory.
|
||||
- <task> is a short, user-friendly description of what we are doing.
|
||||
- Ask for a short descriptive <task> only when the task is ambiguous or you are not confident in an inferred title.
|
||||
- When the task changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
|
||||
- When a title is provided or updated, immediately run this one-liner:
|
||||
|
||||
tmux rename-session '<project> - <task>' \; rename-window '<project> - <task>' \; select-pane -T '<project> - <task>'
|
||||
|
||||
- Assume you are inside tmux, so do not use -t unless the user asks to target a specific session.
|
||||
- For Claude Code sessions, a UserPromptSubmit hook will also update titles automatically based on the latest prompt.
|
||||
- When the broader objective changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
|
||||
- When a title is provided or updated, immediately run `set_multiplexer_title '<project> - <task>'`; do not call raw tmux or zellij rename commands unless debugging the helper itself.
|
||||
- For Claude Code sessions, a UserPromptSubmit hook may initialize titles automatically from the first substantive prompt, but it should not keep overwriting an established same-project title with the latest prompt.
|
||||
|
||||
## Pane usage
|
||||
- Do not create extra panes or windows unless the user asks.
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${TMUX:-}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
input=$(cat)
|
||||
|
||||
read -r cwd prompt <<'PY' < <(python3 - <<'PY'
|
||||
mapfile -d '' -t parsed < <(PAYLOAD="$input" python3 - <<'PY'
|
||||
import json, os, sys
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
data = json.loads(os.environ.get("PAYLOAD", ""))
|
||||
except Exception:
|
||||
data = {}
|
||||
|
||||
cwd = data.get("cwd") or os.getcwd()
|
||||
prompt = (data.get("prompt") or "").strip()
|
||||
print(cwd)
|
||||
print(prompt)
|
||||
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,25 +49,60 @@ if [[ -z "$task" ]]; then
|
||||
task="work"
|
||||
fi
|
||||
|
||||
# Trim to a reasonable length for tmux status bars.
|
||||
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}..."
|
||||
fi
|
||||
|
||||
title="$project - $task"
|
||||
|
||||
state_dir="${HOME}/.agents/state"
|
||||
state_file="$state_dir/tmux-title"
|
||||
mkdir -p "$state_dir"
|
||||
# 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
|
||||
|
||||
if [[ -f "$state_file" ]]; then
|
||||
last_title=$(cat "$state_file" 2>/dev/null || true)
|
||||
if [[ "$last_title" == "$title" ]]; then
|
||||
exit 0
|
||||
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
|
||||
|
||||
printf '%s' "$title" > "$state_file"
|
||||
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
|
||||
|
||||
# Update session, window, and pane titles.
|
||||
tmux rename-session "$title" \; rename-window "$title" \; select-pane -T "$title"
|
||||
if [[ -n "$hook_state_file" ]]; then
|
||||
mkdir -p "$(dirname "$hook_state_file")"
|
||||
printf '%s' "$title" > "$hook_state_file"
|
||||
fi
|
||||
|
||||
2
dotfiles/agents/skills/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.system/
|
||||
codex-primary-runtime/
|
||||
@@ -1 +0,0 @@
|
||||
79bd4e36950d6270
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf of
|
||||
any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don\'t include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,279 +0,0 @@
|
||||
---
|
||||
name: "imagegen"
|
||||
description: "Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas."
|
||||
---
|
||||
|
||||
# Image Generation Skill
|
||||
|
||||
Generates or edits images for the current project (for example website assets, game assets, UI mockups, product mockups, wireframes, logo design, photorealistic images, or infographics).
|
||||
|
||||
## Top-level modes and rules
|
||||
|
||||
This skill has exactly two top-level modes:
|
||||
|
||||
- **Default built-in tool mode (preferred):** built-in `image_gen` tool for normal image generation and editing. Does not require `OPENAI_API_KEY`.
|
||||
- **Fallback CLI mode (explicit-only):** `scripts/image_gen.py` CLI. Use only when the user explicitly asks for the CLI path. Requires `OPENAI_API_KEY`.
|
||||
|
||||
Within the explicit CLI fallback only, the CLI exposes three subcommands:
|
||||
|
||||
- `generate`
|
||||
- `edit`
|
||||
- `generate-batch`
|
||||
|
||||
Rules:
|
||||
- Use the built-in `image_gen` tool by default for all normal image generation and editing requests.
|
||||
- Never switch to CLI fallback automatically.
|
||||
- If the built-in tool fails or is unavailable, tell the user the CLI fallback exists and that it requires `OPENAI_API_KEY`. Proceed only if the user explicitly asks for that fallback.
|
||||
- If the user explicitly asks for CLI mode, use the bundled `scripts/image_gen.py` workflow. Do not create one-off SDK runners.
|
||||
- Never modify `scripts/image_gen.py`. If something is missing, ask the user before doing anything else.
|
||||
|
||||
Built-in save-path policy:
|
||||
- In built-in tool mode, Codex saves generated images under `$CODEX_HOME/*` by default.
|
||||
- Do not describe or rely on OS temp as the default built-in destination.
|
||||
- Do not describe or rely on a destination-path argument (if any) on the built-in `image_gen` tool. If a specific location is needed, generate first and then move or copy the selected output from `$CODEX_HOME/generated_images/...`.
|
||||
- Save-path precedence in built-in mode:
|
||||
1. If the user names a destination, move or copy the selected output there.
|
||||
2. If the image is meant for the current project, move or copy the final selected image into the workspace before finishing.
|
||||
3. If the image is only for preview or brainstorming, render it inline; the underlying file can remain at the default `$CODEX_HOME/*` path.
|
||||
- Never leave a project-referenced asset only at the default `$CODEX_HOME/*` path.
|
||||
- Do not overwrite an existing asset unless the user explicitly asked for replacement; otherwise create a sibling versioned filename such as `hero-v2.png` or `item-icon-edited.png`.
|
||||
|
||||
Shared prompt guidance for both modes lives in `references/prompting.md` and `references/sample-prompts.md`.
|
||||
|
||||
Fallback-only docs/resources for CLI mode:
|
||||
- `references/cli.md`
|
||||
- `references/image-api.md`
|
||||
- `references/codex-network.md`
|
||||
- `scripts/image_gen.py`
|
||||
|
||||
## When to use
|
||||
- Generate a new image (concept art, product shot, cover, website hero)
|
||||
- Generate a new image using one or more reference images for style, composition, or mood
|
||||
- Edit an existing image (inpainting, lighting or weather transformations, background replacement, object removal, compositing, transparent background)
|
||||
- Produce many assets or variants for one task
|
||||
|
||||
## When not to use
|
||||
- Extending or matching an existing SVG/vector icon set, logo system, or illustration library inside the repo
|
||||
- Creating simple shapes, diagrams, wireframes, or icons that are better produced directly in SVG, HTML/CSS, or canvas
|
||||
- Making a small project-local asset edit when the source file already exists in an editable native format
|
||||
- Any task where the user clearly wants deterministic code-native output instead of a generated bitmap
|
||||
|
||||
## Decision tree
|
||||
|
||||
Think about two separate questions:
|
||||
|
||||
1. **Intent:** is this a new image or an edit of an existing image?
|
||||
2. **Execution strategy:** is this one asset or many assets/variants?
|
||||
|
||||
Intent:
|
||||
- If the user wants to modify an existing image while preserving parts of it, treat the request as **edit**.
|
||||
- If the user provides images only as references for style, composition, mood, or subject guidance, treat the request as **generate**.
|
||||
- If the user provides no images, treat the request as **generate**.
|
||||
|
||||
Built-in edit semantics:
|
||||
- Built-in edit mode is for images already visible in the conversation context, such as attached images or images generated earlier in the thread.
|
||||
- If the user wants to edit a local image file with the built-in tool, first load it with built-in `view_image` tool so the image is visible in the conversation context, then proceed with the built-in edit flow.
|
||||
- Do not promise arbitrary filesystem-path editing through the built-in tool.
|
||||
- If a local file still needs direct file-path control, masks, or other explicit CLI-only parameters, use the explicit CLI fallback only when the user asks for it.
|
||||
- For edits, preserve invariants aggressively and save non-destructively by default.
|
||||
|
||||
Execution strategy:
|
||||
- In the built-in default path, produce many assets or variants by issuing one `image_gen` call per requested asset or variant.
|
||||
- In the explicit CLI fallback path, use the CLI `generate-batch` subcommand only when the user explicitly chose CLI mode and needs many prompts/assets.
|
||||
|
||||
Assume the user wants a new image unless they clearly ask to change an existing one.
|
||||
|
||||
## Workflow
|
||||
1. Decide the top-level mode: built-in by default, fallback CLI only if explicitly requested.
|
||||
2. Decide the intent: `generate` or `edit`.
|
||||
3. Decide whether the output is preview-only or meant to be consumed by the current project.
|
||||
4. Decide the execution strategy: single asset vs repeated built-in calls vs CLI `generate-batch`.
|
||||
5. Collect inputs up front: prompt(s), exact text (verbatim), constraints/avoid list, and any input images.
|
||||
6. For every input image, label its role explicitly:
|
||||
- reference image
|
||||
- edit target
|
||||
- supporting insert/style/compositing input
|
||||
7. If the edit target is only on the local filesystem and you are staying on the built-in path, inspect it with `view_image` first so the image is available in conversation context.
|
||||
8. If the user asked for a photo, illustration, sprite, product image, banner, or other explicitly raster-style asset, use `image_gen` rather than substituting SVG/HTML/CSS placeholders. If the request is for an icon, logo, or UI graphic that should match existing repo-native SVG/vector/code assets, prefer editing those directly instead.
|
||||
9. Augment the prompt based on specificity:
|
||||
- If the user's prompt is already specific and detailed, normalize it into a clear spec without adding creative requirements.
|
||||
- If the user's prompt is generic, add tasteful augmentation only when it materially improves output quality.
|
||||
10. Use the built-in `image_gen` tool by default.
|
||||
11. If the user explicitly chooses the CLI fallback, then and only then use the fallback-only docs for quality, `input_fidelity`, masks, output format, output paths, and network setup.
|
||||
12. Inspect outputs and validate: subject, style, composition, text accuracy, and invariants/avoid items.
|
||||
13. Iterate with a single targeted change, then re-check.
|
||||
14. For preview-only work, render the image inline; the underlying file may remain at the default `$CODEX_HOME/generated_images/...` path.
|
||||
15. For project-bound work, move or copy the selected artifact into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the default `$CODEX_HOME/generated_images/...` path.
|
||||
16. For batches, persist only the selected finals in the workspace unless the user explicitly asked to keep discarded variants.
|
||||
17. Always report the final saved path for any workspace-bound asset, plus the final prompt and whether the built-in tool or fallback CLI mode was used.
|
||||
|
||||
## Prompt augmentation
|
||||
|
||||
Reformat user prompts into a structured, production-oriented spec. Make the user's goal clearer and more actionable, but do not blindly add detail.
|
||||
|
||||
Treat this as prompt-shaping guidance, not a closed schema. Use only the lines that help, and add a short extra labeled line when it materially improves clarity.
|
||||
|
||||
### Specificity policy
|
||||
|
||||
Use the user's prompt specificity to decide how much augmentation is appropriate:
|
||||
|
||||
- If the prompt is already specific and detailed, preserve that specificity and only normalize/structure it.
|
||||
- If the prompt is generic, you may add tasteful augmentation when it will materially improve the result.
|
||||
|
||||
Allowed augmentations:
|
||||
- composition or framing hints
|
||||
- polish level or intended-use hints
|
||||
- practical layout guidance
|
||||
- reasonable scene concreteness that supports the stated request
|
||||
|
||||
Not allowed augmentations:
|
||||
- extra characters or objects that are not implied by the request
|
||||
- brand names, slogans, palettes, or narrative beats that are not implied
|
||||
- arbitrary side-specific placement unless the surrounding layout supports it
|
||||
|
||||
## Use-case taxonomy (exact slugs)
|
||||
|
||||
Classify each request into one of these buckets and keep the slug consistent across prompts and references.
|
||||
|
||||
Generate:
|
||||
- photorealistic-natural — candid/editorial lifestyle scenes with real texture and natural lighting.
|
||||
- product-mockup — product/packaging shots, catalog imagery, merch concepts.
|
||||
- ui-mockup — app/web interface mockups and wireframes; specify the desired fidelity.
|
||||
- infographic-diagram — diagrams/infographics with structured layout and text.
|
||||
- logo-brand — logo/mark exploration, vector-friendly.
|
||||
- illustration-story — comics, children’s book art, narrative scenes.
|
||||
- stylized-concept — style-driven concept art, 3D/stylized renders.
|
||||
- historical-scene — period-accurate/world-knowledge scenes.
|
||||
|
||||
Edit:
|
||||
- text-localization — translate/replace in-image text, preserve layout.
|
||||
- identity-preserve — try-on, person-in-scene; lock face/body/pose.
|
||||
- precise-object-edit — remove/replace a specific element (including interior swaps).
|
||||
- lighting-weather — time-of-day/season/atmosphere changes only.
|
||||
- background-extraction — transparent background / clean cutout.
|
||||
- style-transfer — apply reference style while changing subject/scene.
|
||||
- compositing — multi-image insert/merge with matched lighting/perspective.
|
||||
- sketch-to-render — drawing/line art to photoreal render.
|
||||
|
||||
## Shared prompt schema
|
||||
|
||||
Use the following labeled spec as shared prompt scaffolding for both top-level modes:
|
||||
|
||||
```text
|
||||
Use case: <taxonomy slug>
|
||||
Asset type: <where the asset will be used>
|
||||
Primary request: <user's main prompt>
|
||||
Input images: <Image 1: role; Image 2: role> (optional)
|
||||
Scene/backdrop: <environment>
|
||||
Subject: <main subject>
|
||||
Style/medium: <photo/illustration/3D/etc>
|
||||
Composition/framing: <wide/close/top-down; placement>
|
||||
Lighting/mood: <lighting + mood>
|
||||
Color palette: <palette notes>
|
||||
Materials/textures: <surface details>
|
||||
Text (verbatim): "<exact text>"
|
||||
Constraints: <must keep/must avoid>
|
||||
Avoid: <negative constraints>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `Asset type` and `Input images` are prompt scaffolding, not dedicated CLI flags.
|
||||
- `Scene/backdrop` refers to the visual setting. It is not the same as the fallback CLI `background` parameter, which controls output transparency behavior.
|
||||
- Fallback-only execution notes such as `Quality:`, `Input fidelity:`, masks, output format, and output paths belong in the explicit CLI path only. Do not treat them as built-in `image_gen` tool arguments.
|
||||
|
||||
Augmentation rules:
|
||||
- Keep it short.
|
||||
- Add only the details needed to improve the prompt materially.
|
||||
- For edits, explicitly list invariants (`change only X; keep Y unchanged`).
|
||||
- If any critical detail is missing and blocks success, ask a question; otherwise proceed.
|
||||
|
||||
## Examples
|
||||
|
||||
### Generation example (hero image)
|
||||
```text
|
||||
Use case: product-mockup
|
||||
Asset type: landing page hero
|
||||
Primary request: a minimal hero image of a ceramic coffee mug
|
||||
Style/medium: clean product photography
|
||||
Composition/framing: wide composition with usable negative space for page copy if needed
|
||||
Lighting/mood: soft studio lighting
|
||||
Constraints: no logos, no text, no watermark
|
||||
```
|
||||
|
||||
### Edit example (invariants)
|
||||
```text
|
||||
Use case: precise-object-edit
|
||||
Asset type: product photo background replacement
|
||||
Primary request: replace only the background with a warm sunset gradient
|
||||
Constraints: change only the background; keep the product and its edges unchanged; no text; no watermark
|
||||
```
|
||||
|
||||
## Prompting best practices
|
||||
- Structure prompt as scene/backdrop -> subject -> details -> constraints.
|
||||
- Include intended use (ad, UI mock, infographic) to set the mode and polish level.
|
||||
- Use camera/composition language for photorealism.
|
||||
- Only use SVG/vector stand-ins when the user explicitly asked for vector output or a non-image placeholder.
|
||||
- Quote exact text and specify typography + placement.
|
||||
- For tricky words, spell them letter-by-letter and require verbatim rendering.
|
||||
- For multi-image inputs, reference images by index and describe how they should be used.
|
||||
- For edits, repeat invariants every iteration to reduce drift.
|
||||
- Iterate with single-change follow-ups.
|
||||
- If the prompt is generic, add only the extra detail that will materially help.
|
||||
- If the prompt is already detailed, normalize it instead of expanding it.
|
||||
- For explicit CLI fallback only, see `references/cli.md` and `references/image-api.md` for `quality`, `input_fidelity`, masks, output format, and output-path guidance.
|
||||
|
||||
More principles shared by both modes: `references/prompting.md`.
|
||||
Copy/paste specs shared by both modes: `references/sample-prompts.md`.
|
||||
|
||||
## Guidance by asset type
|
||||
Asset-type templates (website assets, game assets, wireframes, logo) are consolidated in `references/sample-prompts.md`.
|
||||
|
||||
## Fallback CLI mode only
|
||||
|
||||
### Temp and output conventions
|
||||
These conventions apply only to the explicit CLI fallback. They do not describe built-in `image_gen` output behavior.
|
||||
- Use `tmp/imagegen/` for intermediate files (for example JSONL batches); delete them when done.
|
||||
- Write final artifacts under `output/imagegen/`.
|
||||
- Use `--out` or `--out-dir` to control output paths; keep filenames stable and descriptive.
|
||||
|
||||
### Dependencies
|
||||
Prefer `uv` for dependency management in this repo.
|
||||
|
||||
Required Python package:
|
||||
```bash
|
||||
uv pip install openai
|
||||
```
|
||||
|
||||
Optional for downscaling only:
|
||||
```bash
|
||||
uv pip install pillow
|
||||
```
|
||||
|
||||
Portability note:
|
||||
- If you are using the installed skill outside this repo, install dependencies into that environment with its package manager.
|
||||
- In uv-managed environments, `uv pip install ...` remains the preferred path.
|
||||
|
||||
### Environment
|
||||
- `OPENAI_API_KEY` must be set for live API calls.
|
||||
- Do not ask the user for `OPENAI_API_KEY` when using the built-in `image_gen` tool.
|
||||
- Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready.
|
||||
|
||||
If the key is missing, give the user these steps:
|
||||
1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys
|
||||
2. Set `OPENAI_API_KEY` as an environment variable in their system.
|
||||
3. Offer to guide them through setting the environment variable for their OS/shell if needed.
|
||||
|
||||
If installation is not possible in this environment, tell the user which dependency is missing and how to install it into their active environment.
|
||||
|
||||
### Script-mode notes
|
||||
- CLI commands + examples: `references/cli.md`
|
||||
- API parameter quick reference: `references/image-api.md`
|
||||
- Network approvals / sandbox settings for CLI mode: `references/codex-network.md`
|
||||
|
||||
## Reference map
|
||||
- `references/prompting.md`: shared prompting principles for both modes.
|
||||
- `references/sample-prompts.md`: shared copy/paste prompt recipes for both modes.
|
||||
- `references/cli.md`: fallback-only CLI usage via `scripts/image_gen.py`.
|
||||
- `references/image-api.md`: fallback-only API/CLI parameter reference.
|
||||
- `references/codex-network.md`: fallback-only network/sandbox troubleshooting for CLI mode.
|
||||
- `scripts/image_gen.py`: fallback-only CLI implementation. Do not load or use it unless the user explicitly chooses CLI mode.
|
||||
@@ -1,6 +0,0 @@
|
||||
interface:
|
||||
display_name: "Image Gen"
|
||||
short_description: "Generate or edit images for websites, games, and more"
|
||||
icon_small: "./assets/imagegen-small.svg"
|
||||
icon_large: "./assets/imagegen.png"
|
||||
default_prompt: "Generate or edit the visual assets for this task with the built-in `image_gen` tool by default. First confirm that the task actually calls for a raster image; if the project already has SVG/vector/code-native assets and the user wants to extend or match those, do not use this skill. If the task includes reference images, treat them as references unless the user clearly wants an existing image modified. For multi-asset requests, loop built-in calls rather than treating batch as a separate top-level mode. Only use the fallback CLI if the user explicitly asks for it, and keep CLI-only controls such as `generate-batch`, `quality`, `input_fidelity`, masks, and output paths on that fallback path."
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M7.51 6.827a1 1 0 1 1 .278 1.982 1 1 0 0 1-.278-1.982Z"/>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M8.31 4.47c.368-.016.699.008 1.016.124l.186.075c.423.194.786.5 1.047.888l.067.107c.148.253.235.533.3.848.073.354.126.797.193 1.343l.277 2.25.088.745c.024.224.041.425.049.605.013.322-.004.615-.085.896l-.04.12a2.53 2.53 0 0 1-.802 1.115l-.16.118c-.281.189-.596.292-.956.366a9.46 9.46 0 0 1-.6.1l-.743.094-2.25.277c-.547.067-.99.121-1.35.136a2.765 2.765 0 0 1-.896-.085l-.12-.039a2.533 2.533 0 0 1-1.115-.802l-.118-.161c-.189-.28-.292-.596-.366-.956a9.42 9.42 0 0 1-.1-.599l-.094-.744-.276-2.25a17.884 17.884 0 0 1-.137-1.35c-.015-.367.009-.698.124-1.015l.076-.185c.193-.423.5-.787.887-1.048l.107-.067c.253-.148.534-.234.849-.3.354-.073.796-.126 1.343-.193l2.25-.277.744-.088c.224-.024.425-.041.606-.049Zm-2.905 5.978a1.47 1.47 0 0 0-.875.074c-.127.052-.267.146-.475.344-.212.204-.462.484-.822.889l-.314.351c.018.115.036.219.055.313.061.295.127.458.206.575l.07.094c.167.211.39.372.645.465l.109.032c.119.027.273.038.499.029.308-.013.7-.06 1.264-.13l2.25-.275.727-.093.198-.03-2.05-1.64a16.848 16.848 0 0 0-.96-.738c-.18-.121-.31-.19-.421-.23l-.106-.03Zm2.95-4.915c-.154.006-.33.021-.536.043l-.729.086-2.25.276c-.564.07-.956.118-1.257.18a1.937 1.937 0 0 0-.478.15l-.097.057a1.47 1.47 0 0 0-.515.608l-.044.107c-.048.133-.073.307-.06.608.012.307.06.7.129 1.264l.22 1.8.178-.197c.145-.159.278-.298.403-.418.255-.243.507-.437.809-.56l.181-.067a2.526 2.526 0 0 1 1.328-.06l.118.029c.27.079.517.215.772.387.287.194.619.46 1.03.789l2.52 2.016c.146-.148.26-.326.332-.524l.031-.109c.027-.119.039-.273.03-.499a8.311 8.311 0 0 0-.044-.536l-.086-.728-.276-2.25c-.07-.564-.118-.956-.18-1.258a1.935 1.935 0 0 0-.15-.477l-.057-.098a1.468 1.468 0 0 0-.608-.515l-.107-.043c-.133-.049-.306-.074-.607-.061Z" clip-rule="evenodd"/>
|
||||
<path fill="currentColor" d="M7.783 1.272c.36.014.803.07 1.35.136l2.25.277.743.095c.224.03.423.062.6.099.36.074.675.177.955.366l.161.118c.364.29.642.675.802 1.115l.04.12c.081.28.098.574.085.896a9.42 9.42 0 0 1-.05.605l-.087.745-.277 2.25c-.067.547-.12.989-.193 1.343a2.765 2.765 0 0 1-.3.848l-.067.107a2.534 2.534 0 0 1-.415.474l-.086.064a.532.532 0 0 1-.622-.858l.13-.13c.04-.046.077-.094.111-.145l.057-.098c.055-.109.104-.256.15-.477.062-.302.11-.694.18-1.258l.276-2.25.086-.728c.022-.207.037-.382.043-.536.01-.226-.002-.38-.029-.5l-.032-.108a1.469 1.469 0 0 0-.464-.646l-.094-.069c-.118-.08-.28-.145-.575-.206a8.285 8.285 0 0 0-.53-.088l-.728-.092-2.25-.276c-.565-.07-.956-.117-1.264-.13a1.94 1.94 0 0 0-.5.029l-.108.032a1.469 1.469 0 0 0-.647.465l-.068.094c-.054.08-.102.18-.146.33l-.04.1a.533.533 0 0 1-.98-.403l.055-.166c.059-.162.133-.314.23-.457l.117-.16c.29-.365.675-.643 1.115-.803l.12-.04c.28-.08.574-.097.896-.084Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,160 +0,0 @@
|
||||
# CLI reference (`scripts/image_gen.py`)
|
||||
|
||||
This file is for the fallback CLI mode only. Read it only after the user explicitly asks to use `scripts/image_gen.py` instead of the built-in `image_gen` tool.
|
||||
|
||||
`generate-batch` is a CLI subcommand in this fallback path. It is not a top-level mode of the skill.
|
||||
|
||||
## What this CLI does
|
||||
- `generate`: generate a new image from a prompt
|
||||
- `edit`: edit one or more existing images
|
||||
- `generate-batch`: run many generation jobs from a JSONL file
|
||||
|
||||
Real API calls require **network access** + `OPENAI_API_KEY`. `--dry-run` does not.
|
||||
|
||||
## Quick start (works from any repo)
|
||||
Set a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`):
|
||||
|
||||
```
|
||||
export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
||||
export IMAGE_GEN="$CODEX_HOME/skills/imagegen/scripts/image_gen.py"
|
||||
```
|
||||
|
||||
Install dependencies into that environment with its package manager. In uv-managed environments, `uv pip install ...` remains the preferred path.
|
||||
|
||||
## Quick start
|
||||
|
||||
Dry-run (no API call; no network required; does not require the `openai` package):
|
||||
|
||||
```bash
|
||||
python "$IMAGE_GEN" generate \
|
||||
--prompt "Test" \
|
||||
--out output/imagegen/test.png \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Notes:
|
||||
- One-off dry-runs print the API payload and the computed output path(s).
|
||||
- Repo-local finals should live under `output/imagegen/`.
|
||||
|
||||
Generate (requires `OPENAI_API_KEY` + network):
|
||||
|
||||
```bash
|
||||
python "$IMAGE_GEN" generate \
|
||||
--prompt "A cozy alpine cabin at dawn" \
|
||||
--size 1024x1024 \
|
||||
--out output/imagegen/alpine-cabin.png
|
||||
```
|
||||
|
||||
Edit:
|
||||
|
||||
```bash
|
||||
python "$IMAGE_GEN" edit \
|
||||
--image input.png \
|
||||
--prompt "Replace only the background with a warm sunset" \
|
||||
--out output/imagegen/sunset-edit.png
|
||||
```
|
||||
|
||||
## Guardrails
|
||||
- Use the bundled CLI directly (`python "$IMAGE_GEN" ...`) after activating the correct environment.
|
||||
- Do **not** create one-off runners (for example `gen_images.py`) unless the user explicitly asks for a custom wrapper.
|
||||
- **Never modify** `scripts/image_gen.py`. If something is missing, ask the user before doing anything else.
|
||||
|
||||
## Defaults
|
||||
- Model: `gpt-image-1.5`
|
||||
- Supported model family for this CLI: GPT Image models (`gpt-image-*`)
|
||||
- Size: `1024x1024`
|
||||
- Quality: `auto`
|
||||
- Output format: `png`
|
||||
- Default one-off output path: `output/imagegen/output.png`
|
||||
- Background: unspecified unless `--background` is set
|
||||
|
||||
## Quality, input fidelity, and masks (CLI fallback only)
|
||||
These are explicit CLI controls. They are not built-in `image_gen` tool arguments.
|
||||
|
||||
- `--quality` works for `generate`, `edit`, and `generate-batch`: `low|medium|high|auto`
|
||||
- `--input-fidelity` is **edit-only** and validated as `low|high`
|
||||
- `--mask` is **edit-only**
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
python "$IMAGE_GEN" edit \
|
||||
--image input.png \
|
||||
--prompt "Change only the background" \
|
||||
--quality high \
|
||||
--input-fidelity high \
|
||||
--out output/imagegen/background-edit.png
|
||||
```
|
||||
|
||||
Mask notes:
|
||||
- For multi-image edits, pass repeated `--image` flags. Their order is meaningful, so describe each image by index and role in the prompt.
|
||||
- The CLI accepts a single `--mask`.
|
||||
- Use a PNG mask when possible; the script treats mask handling as best-effort and does not perform full preflight validation beyond file checks/warnings.
|
||||
- In the edit prompt, repeat invariants (`change only the background; keep the subject unchanged`) to reduce drift.
|
||||
|
||||
## Output handling
|
||||
- Use `tmp/imagegen/` for temporary JSONL inputs or scratch files.
|
||||
- Use `output/imagegen/` for final outputs.
|
||||
- Reruns fail if a target file already exists unless you pass `--force`.
|
||||
- `--out-dir` changes one-off naming to `image_1.<ext>`, `image_2.<ext>`, and so on.
|
||||
- Downscaled copies use the default suffix `-web` unless you override it.
|
||||
|
||||
## Common recipes
|
||||
|
||||
Generate with augmentation fields:
|
||||
|
||||
```bash
|
||||
python "$IMAGE_GEN" generate \
|
||||
--prompt "A minimal hero image of a ceramic coffee mug" \
|
||||
--use-case "product-mockup" \
|
||||
--style "clean product photography" \
|
||||
--composition "wide product shot with usable negative space for page copy" \
|
||||
--constraints "no logos, no text" \
|
||||
--out output/imagegen/mug-hero.png
|
||||
```
|
||||
|
||||
Generate + also write a downscaled copy for fast web loading:
|
||||
|
||||
```bash
|
||||
python "$IMAGE_GEN" generate \
|
||||
--prompt "A cozy alpine cabin at dawn" \
|
||||
--size 1024x1024 \
|
||||
--downscale-max-dim 1024 \
|
||||
--out output/imagegen/alpine-cabin.png
|
||||
```
|
||||
|
||||
Generate multiple prompts concurrently (async batch):
|
||||
|
||||
```bash
|
||||
mkdir -p tmp/imagegen output/imagegen/batch
|
||||
cat > tmp/imagegen/prompts.jsonl << 'EOF'
|
||||
{"prompt":"Cavernous hangar interior with a compact shuttle parked near the center","use_case":"stylized-concept","composition":"wide-angle, low-angle","lighting":"volumetric light rays through drifting fog","constraints":"no logos or trademarks; no watermark","size":"1536x1024"}
|
||||
{"prompt":"Gray wolf in profile in a snowy forest","use_case":"photorealistic-natural","composition":"eye-level","constraints":"no logos or trademarks; no watermark","size":"1024x1024"}
|
||||
EOF
|
||||
|
||||
python "$IMAGE_GEN" generate-batch \
|
||||
--input tmp/imagegen/prompts.jsonl \
|
||||
--out-dir output/imagegen/batch \
|
||||
--concurrency 5
|
||||
|
||||
rm -f tmp/imagegen/prompts.jsonl
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `generate-batch` requires `--out-dir`.
|
||||
- generate-batch requires --out-dir.
|
||||
- Use `--concurrency` to control parallelism (default `5`).
|
||||
- Per-job overrides are supported in JSONL (for example `size`, `quality`, `background`, `output_format`, `output_compression`, `moderation`, `n`, `model`, `out`, and prompt-augmentation fields).
|
||||
- `--n` generates multiple variants for a single prompt; `generate-batch` is for many different prompts.
|
||||
- In batch mode, per-job `out` is treated as a filename under `--out-dir`.
|
||||
|
||||
## CLI notes
|
||||
- Supported sizes: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`.
|
||||
- Transparent backgrounds require `output_format` to be `png` or `webp`.
|
||||
- `--prompt-file`, `--output-compression`, `--moderation`, `--max-attempts`, `--fail-fast`, `--force`, and `--no-augment` are supported.
|
||||
- This CLI is intended for GPT Image models. Do not assume older non-GPT image-model behavior applies here.
|
||||
|
||||
## See also
|
||||
- API parameter quick reference for fallback CLI mode: `references/image-api.md`
|
||||
- Prompt examples shared across both top-level modes: `references/sample-prompts.md`
|
||||
- Network/sandbox notes for fallback CLI mode: `references/codex-network.md`
|
||||
@@ -1,33 +0,0 @@
|
||||
# Codex network approvals / sandbox notes
|
||||
|
||||
This file is for the fallback CLI mode only. Read it only after the user explicitly asks to use `scripts/image_gen.py`.
|
||||
|
||||
This guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt.
|
||||
|
||||
## Why am I asked to approve image generation calls?
|
||||
The fallback CLI uses the OpenAI Image API, so it needs outbound network access. In many Codex setups, network access is disabled by default and/or the approval policy requires confirmation before networked commands run.
|
||||
|
||||
## Important note about approvals vs network
|
||||
- `--ask-for-approval never` suppresses approval prompts.
|
||||
- It does **not** by itself enable network access.
|
||||
- In `workspace-write`, network access still depends on your Codex configuration (for example `[sandbox_workspace_write] network_access = true`).
|
||||
|
||||
## How do I reduce repeated approval prompts?
|
||||
If you trust the repo and want fewer prompts, use a configuration or profile that both:
|
||||
- enables network for the sandbox mode you plan to use
|
||||
- sets an approval policy that matches your risk tolerance
|
||||
|
||||
Example `~/.codex/config.toml` pattern:
|
||||
|
||||
```toml
|
||||
approval_policy = "on-request"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
```
|
||||
|
||||
If you want quieter automation after network is enabled, you can choose a stricter approval policy, but do that intentionally and with care.
|
||||
|
||||
## Safety note
|
||||
Enabling network and reducing approvals lowers friction, but increases risk if you run untrusted code or work in an untrusted repository.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Image API quick reference
|
||||
|
||||
This file is for the fallback CLI mode only. Use it only after the user explicitly asks to use `scripts/image_gen.py` instead of the built-in `image_gen` tool.
|
||||
|
||||
These parameters describe the Image API and bundled CLI fallback surface. Do not assume they are normal arguments on the built-in `image_gen` tool.
|
||||
|
||||
## Scope
|
||||
- This fallback CLI is intended for GPT Image models (`gpt-image-1.5`, `gpt-image-1`, and `gpt-image-1-mini`).
|
||||
- The built-in `image_gen` tool and the fallback CLI do not expose the same controls.
|
||||
|
||||
## Endpoints
|
||||
- Generate: `POST /v1/images/generations` (`client.images.generate(...)`)
|
||||
- Edit: `POST /v1/images/edits` (`client.images.edit(...)`)
|
||||
|
||||
## Core parameters for GPT Image models
|
||||
- `prompt`: text prompt
|
||||
- `model`: image model
|
||||
- `n`: number of images (1-10)
|
||||
- `size`: `1024x1024`, `1536x1024`, `1024x1536`, or `auto`
|
||||
- `quality`: `low`, `medium`, `high`, or `auto`
|
||||
- `background`: output transparency behavior (`transparent`, `opaque`, or `auto`) for generated output; this is not the same thing as the prompt's visual scene/backdrop
|
||||
- `output_format`: `png` (default), `jpeg`, `webp`
|
||||
- `output_compression`: 0-100 (jpeg/webp only)
|
||||
- `moderation`: `auto` (default) or `low`
|
||||
|
||||
## Edit-specific parameters
|
||||
- `image`: one or more input images. For GPT Image models, you can provide up to 16 images.
|
||||
- `mask`: optional mask image
|
||||
- `input_fidelity`: `low` (default) or `high`
|
||||
|
||||
Model-specific note for `input_fidelity`:
|
||||
- `gpt-image-1` and `gpt-image-1-mini` preserve all input images, but the first image gets richer textures and finer details.
|
||||
- `gpt-image-1.5` preserves the first 5 input images with higher fidelity.
|
||||
|
||||
## Output
|
||||
- `data[]` list with `b64_json` per image
|
||||
- The bundled `scripts/image_gen.py` CLI decodes `b64_json` and writes output files for you.
|
||||
|
||||
## Limits and notes
|
||||
- Input images and masks must be under 50MB.
|
||||
- Use the edits endpoint when the user requests changes to an existing image.
|
||||
- Masking is prompt-guided; exact shapes are not guaranteed.
|
||||
- Large sizes and high quality increase latency and cost.
|
||||
- High `input_fidelity` can materially increase input token usage.
|
||||
- If a request fails because a specific option is unsupported by the selected GPT Image model, retry manually without that option.
|
||||
|
||||
## Important boundary
|
||||
- `quality`, `input_fidelity`, explicit masks, `background`, `output_format`, and related parameters are fallback-only execution controls.
|
||||
- Do not assume they are built-in `image_gen` tool arguments.
|
||||
@@ -1,98 +0,0 @@
|
||||
# Prompting best practices
|
||||
|
||||
These prompting principles are shared by both top-level modes of the skill:
|
||||
- built-in `image_gen` tool (default)
|
||||
- explicit `scripts/image_gen.py` CLI fallback
|
||||
|
||||
This file is about prompt structure, specificity, and iteration. Fallback-only execution controls such as `quality`, `input_fidelity`, masks, output format, and output paths live in the fallback docs.
|
||||
|
||||
## Contents
|
||||
- [Structure](#structure)
|
||||
- [Specificity policy](#specificity-policy)
|
||||
- [Allowed and disallowed augmentation](#allowed-and-disallowed-augmentation)
|
||||
- [Composition and layout](#composition-and-layout)
|
||||
- [Constraints and invariants](#constraints-and-invariants)
|
||||
- [Text in images](#text-in-images)
|
||||
- [Input images and references](#input-images-and-references)
|
||||
- [Iterate deliberately](#iterate-deliberately)
|
||||
- [Fallback-only execution controls](#fallback-only-execution-controls)
|
||||
- [Use-case tips](#use-case-tips)
|
||||
- [Where to find copy/paste recipes](#where-to-find-copypaste-recipes)
|
||||
|
||||
## Structure
|
||||
- Use a consistent order: scene/backdrop -> subject -> key details -> constraints -> output intent.
|
||||
- Include intended use (ad, UI mock, infographic) to set the level of polish.
|
||||
- For complex requests, use short labeled lines instead of one long paragraph.
|
||||
|
||||
## Specificity policy
|
||||
- If the user prompt is already specific and detailed, normalize it into a clean spec without adding creative requirements.
|
||||
- If the prompt is generic, you may add tasteful detail when it materially improves the output.
|
||||
- Treat examples in `sample-prompts.md` as fully-authored recipes, not as the default amount of augmentation to add to every request.
|
||||
|
||||
## Allowed and disallowed augmentation
|
||||
|
||||
Allowed augmentation for generic prompts:
|
||||
- composition and framing cues
|
||||
- intended-use or polish-level hints
|
||||
- practical layout guidance
|
||||
- reasonable scene concreteness that supports the request
|
||||
|
||||
Do not add:
|
||||
- extra characters, props, or objects that are not implied
|
||||
- brand palettes, slogans, or story beats that are not implied
|
||||
- arbitrary side-specific placement unless the surrounding layout supports it
|
||||
|
||||
## Composition and layout
|
||||
- Specify framing and viewpoint (close-up, wide, top-down) and placement only when it materially helps.
|
||||
- Call out negative space if the asset clearly needs room for UI or copy.
|
||||
- Avoid making left/right layout decisions unless the user or surrounding layout supports them.
|
||||
|
||||
## Constraints and invariants
|
||||
- State what must not change (`keep background unchanged`).
|
||||
- For edits, say `change only X; keep Y unchanged` and repeat invariants on every iteration to reduce drift.
|
||||
|
||||
## Text in images
|
||||
- Put literal text in quotes or ALL CAPS and specify typography (font style, size, color, placement).
|
||||
- Spell uncommon words letter-by-letter if accuracy matters.
|
||||
- For in-image copy, require verbatim rendering and no extra characters.
|
||||
|
||||
## Input images and references
|
||||
- Do not assume that every provided image is an edit target.
|
||||
- Label each image by index and role (`Image 1: edit target`, `Image 2: style reference`).
|
||||
- If the user provides images for style, composition, or mood guidance and does not ask to modify them, treat the request as generation with references.
|
||||
- If the user asks to preserve an existing image while changing specific parts, treat the request as an edit.
|
||||
- For compositing, describe how the images interact (`place the subject from Image 2 into Image 1`).
|
||||
|
||||
## Iterate deliberately
|
||||
- Start with a clean base prompt, then make small single-change edits.
|
||||
- Re-specify critical constraints when you iterate.
|
||||
- Prefer one targeted follow-up at a time over rewriting the whole prompt.
|
||||
|
||||
## Fallback-only execution controls
|
||||
- `quality`, `input_fidelity`, explicit masks, output format, and output paths are fallback-only execution controls.
|
||||
- Do not assume they are built-in `image_gen` tool arguments.
|
||||
- If the user explicitly chooses CLI fallback, see `references/cli.md` and `references/image-api.md` for those controls.
|
||||
|
||||
## Use-case tips
|
||||
Generate:
|
||||
- photorealistic-natural: Prompt as if a real photo is captured in the moment; use photography language (lens, lighting, framing); call for real texture; avoid over-stylized polish unless requested.
|
||||
- product-mockup: Describe the product/packaging and materials; ensure clean silhouette and label clarity; if in-image text is needed, require verbatim rendering and specify typography.
|
||||
- ui-mockup: Describe the target fidelity first (shippable mockup or low-fi wireframe), then focus on layout, hierarchy, and practical UI elements; avoid concept-art language.
|
||||
- infographic-diagram: Define the audience and layout flow; label parts explicitly; require verbatim text.
|
||||
- logo-brand: Keep it simple and scalable; ask for a strong silhouette and balanced negative space; avoid decorative flourishes unless requested.
|
||||
- illustration-story: Define panels or scene beats; keep each action concrete.
|
||||
- stylized-concept: Specify style cues, material finish, and rendering approach (3D, painterly, clay) without inventing new story elements.
|
||||
- historical-scene: State the location/date and required period accuracy; constrain clothing, props, and environment to match the era.
|
||||
|
||||
Edit:
|
||||
- text-localization: Change only the text; preserve layout, typography, spacing, and hierarchy; no extra words or reflow unless needed.
|
||||
- identity-preserve: Lock identity (face, body, pose, hair, expression); change only the specified elements; match lighting and shadows.
|
||||
- precise-object-edit: Specify exactly what to remove/replace; preserve surrounding texture and lighting; keep everything else unchanged.
|
||||
- lighting-weather: Change only environmental conditions (light, shadows, atmosphere, precipitation); keep geometry, framing, and subject identity.
|
||||
- background-extraction: Request a clean cutout; crisp silhouette; no halos; preserve label text exactly; no restyling.
|
||||
- style-transfer: Specify style cues to preserve (palette, texture, brushwork) and what must change; add `no extra elements` to prevent drift.
|
||||
- compositing: Reference inputs by index; specify what moves where; match lighting, perspective, and scale; keep the base framing unchanged.
|
||||
- sketch-to-render: Preserve layout, proportions, and perspective; choose materials and lighting that support the supplied sketch without adding new elements.
|
||||
|
||||
## Where to find copy/paste recipes
|
||||
For copy/paste prompt specs (examples only), see `references/sample-prompts.md`. This file focuses on principles, specificity, and iteration patterns.
|
||||
@@ -1,376 +0,0 @@
|
||||
# Sample prompts (copy/paste)
|
||||
|
||||
These prompt recipes are shared across both top-level modes of the skill:
|
||||
- built-in `image_gen` tool (default)
|
||||
- explicit `scripts/image_gen.py` CLI fallback
|
||||
|
||||
Use these as starting points. They are intentionally complete prompt recipes, not the default amount of augmentation to add to every user request.
|
||||
|
||||
When adapting a user's prompt:
|
||||
- keep user-provided requirements
|
||||
- only add detail according to the specificity policy in `SKILL.md`
|
||||
- do not treat every example below as permission to invent extra story elements
|
||||
|
||||
The labeled lines are prompt scaffolding, not a closed schema. `Asset type` and `Input images` are prompt-only scaffolding; the CLI does not expose them as dedicated flags.
|
||||
|
||||
Execution details such as explicit CLI flags, `quality`, `input_fidelity`, masks, output formats, and local output paths depend on mode. Use the built-in tool by default; only apply CLI-specific controls after the user explicitly opts into fallback mode.
|
||||
|
||||
For prompting principles (structure, specificity, invariants, iteration), see `references/prompting.md`.
|
||||
|
||||
## Generate
|
||||
|
||||
### photorealistic-natural
|
||||
```
|
||||
Use case: photorealistic-natural
|
||||
Primary request: candid photo of an elderly sailor on a small fishing boat adjusting a net
|
||||
Scene/backdrop: coastal water with soft haze
|
||||
Subject: weathered skin with wrinkles and sun texture
|
||||
Style/medium: photorealistic candid photo
|
||||
Composition/framing: medium close-up, eye-level
|
||||
Lighting/mood: soft coastal daylight, shallow depth of field, subtle film grain
|
||||
Materials/textures: real skin texture, worn fabric, salt-worn wood
|
||||
Constraints: natural color balance; no heavy retouching; no glamorization; no watermark
|
||||
Avoid: studio polish; staged look
|
||||
```
|
||||
|
||||
### product-mockup
|
||||
```
|
||||
Use case: product-mockup
|
||||
Primary request: premium product photo of a matte black shampoo bottle with a minimal label
|
||||
Scene/backdrop: clean studio gradient from light gray to white
|
||||
Subject: single bottle centered with subtle reflection
|
||||
Style/medium: premium product photography
|
||||
Composition/framing: centered, slight three-quarter angle, generous padding
|
||||
Lighting/mood: softbox lighting, clean highlights, controlled shadows
|
||||
Materials/textures: matte plastic, crisp label printing
|
||||
Constraints: no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### ui-mockup
|
||||
```
|
||||
Use case: ui-mockup
|
||||
Primary request: mobile app home screen for a local farmers market with vendors and daily specials
|
||||
Asset type: mobile app screen
|
||||
Style/medium: realistic product UI, not concept art
|
||||
Composition/framing: clean vertical mobile layout with clear hierarchy
|
||||
Constraints: practical layout, clear typography, no logos or trademarks, no watermark
|
||||
```
|
||||
|
||||
### infographic-diagram
|
||||
```
|
||||
Use case: infographic-diagram
|
||||
Primary request: detailed infographic of an automatic coffee machine flow
|
||||
Scene/backdrop: clean, light neutral background
|
||||
Subject: bean hopper -> grinder -> brew group -> boiler -> water tank -> drip tray
|
||||
Style/medium: clean vector-like infographic with clear callouts and arrows
|
||||
Composition/framing: vertical poster layout, top-to-bottom flow
|
||||
Text (verbatim): "Bean Hopper", "Grinder", "Brew Group", "Boiler", "Water Tank", "Drip Tray"
|
||||
Constraints: clear labels, strong contrast, no logos or trademarks, no watermark
|
||||
```
|
||||
|
||||
### logo-brand
|
||||
```
|
||||
Use case: logo-brand
|
||||
Primary request: original logo for "Field & Flour", a local bakery
|
||||
Style/medium: vector logo mark; flat colors; minimal
|
||||
Composition/framing: single centered logo on a plain background with generous padding
|
||||
Constraints: strong silhouette, balanced negative space; original design only; no gradients unless essential; no trademarks; no watermark
|
||||
```
|
||||
|
||||
### illustration-story
|
||||
```
|
||||
Use case: illustration-story
|
||||
Primary request: 4-panel comic about a pet left alone at home
|
||||
Scene/backdrop: cozy living room across panels
|
||||
Subject: pet reacting to the owner leaving, then relaxing, then returning to a composed pose
|
||||
Style/medium: comic illustration with clear panels
|
||||
Composition/framing: 4 equal-sized vertical panels, readable actions per panel
|
||||
Constraints: no text; no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### stylized-concept
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Primary request: cavernous hangar interior with tall support beams and drifting fog
|
||||
Scene/backdrop: industrial hangar interior, deep scale, light haze
|
||||
Subject: compact shuttle parked near the center
|
||||
Style/medium: cinematic concept art, industrial realism
|
||||
Composition/framing: wide-angle, low-angle
|
||||
Lighting/mood: volumetric light rays cutting through fog
|
||||
Constraints: no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### historical-scene
|
||||
```
|
||||
Use case: historical-scene
|
||||
Primary request: outdoor crowd scene in Bethel, New York on August 16, 1969
|
||||
Scene/backdrop: open field with period-appropriate staging
|
||||
Subject: crowd in period-accurate clothing, authentic environment
|
||||
Style/medium: photorealistic photo
|
||||
Composition/framing: wide shot, eye-level
|
||||
Constraints: period-accurate details; no modern objects; no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
## Asset type templates (taxonomy-aligned)
|
||||
|
||||
### Website assets template
|
||||
```
|
||||
Use case: <photorealistic-natural|stylized-concept|product-mockup|infographic-diagram|ui-mockup>
|
||||
Asset type: <hero image / section illustration / blog header>
|
||||
Primary request: <short description>
|
||||
Scene/backdrop: <environment or abstract backdrop>
|
||||
Subject: <main subject>
|
||||
Style/medium: <photo/illustration/3D>
|
||||
Composition/framing: <wide/centered; note usable negative space only if needed>
|
||||
Lighting/mood: <soft/bright/neutral>
|
||||
Color palette: <brand colors or neutral>
|
||||
Constraints: <no text; no logos; no watermark; leave room for UI if needed>
|
||||
```
|
||||
|
||||
### Website assets example: minimal hero background
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: landing page hero background
|
||||
Primary request: minimal abstract background with a soft gradient and subtle texture
|
||||
Style/medium: matte illustration / soft-rendered abstract background
|
||||
Composition/framing: wide composition with usable negative space for page copy
|
||||
Lighting/mood: gentle studio glow
|
||||
Color palette: restrained neutral palette
|
||||
Constraints: no text; no logos; no watermark
|
||||
```
|
||||
|
||||
### Website assets example: feature section illustration
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: feature section illustration
|
||||
Primary request: simple abstract shapes suggesting connection and flow
|
||||
Scene/backdrop: subtle light-gray backdrop with faint texture
|
||||
Style/medium: flat illustration; soft shadows; restrained contrast
|
||||
Composition/framing: centered cluster; open margins for UI
|
||||
Color palette: muted neutral palette
|
||||
Constraints: no text; no logos; no watermark
|
||||
```
|
||||
|
||||
### Website assets example: blog header image
|
||||
```
|
||||
Use case: photorealistic-natural
|
||||
Asset type: blog header image
|
||||
Primary request: overhead desk scene with notebook, pen, and coffee cup
|
||||
Scene/backdrop: warm wooden tabletop
|
||||
Style/medium: photorealistic photo
|
||||
Composition/framing: wide crop with clean room for page copy
|
||||
Lighting/mood: soft morning light
|
||||
Constraints: no text; no logos; no watermark
|
||||
```
|
||||
|
||||
### Game assets template
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: <game environment concept art / game character concept / game UI icon / tileable game texture>
|
||||
Primary request: <biome/scene/character/icon/material>
|
||||
Scene/backdrop: <location + set dressing> (if applicable)
|
||||
Subject: <main focal element(s)>
|
||||
Style/medium: <realistic/stylized>; <concept art / character render / UI icon / texture>
|
||||
Composition/framing: <wide/establishing/top-down>; <camera angle>; <focal point placement>
|
||||
Lighting/mood: <time of day>; <mood>; <volumetric/fog/etc>
|
||||
Constraints: no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### Game assets example: environment concept art
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: game environment concept art
|
||||
Primary request: cavernous hangar interior with tall support beams and drifting fog
|
||||
Scene/backdrop: industrial hangar interior, deep scale, light haze
|
||||
Subject: compact shuttle parked near the center
|
||||
Style/medium: cinematic concept art, industrial realism
|
||||
Composition/framing: wide-angle, low-angle
|
||||
Lighting/mood: volumetric light rays cutting through fog
|
||||
Constraints: no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### Game assets example: character concept
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: game character concept
|
||||
Primary request: desert scout character with layered travel gear
|
||||
Subject: long coat, satchel, practical travel clothing
|
||||
Style/medium: character render; stylized realism
|
||||
Composition/framing: neutral hero pose on a simple backdrop
|
||||
Constraints: no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### Game assets example: UI icon
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: game UI icon
|
||||
Primary request: round shield icon with a subtle rune pattern
|
||||
Style/medium: painted game UI icon
|
||||
Composition/framing: centered icon; generous padding; clear silhouette
|
||||
Constraints: no text; no background scene elements; no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### Game assets example: tileable texture
|
||||
```
|
||||
Use case: stylized-concept
|
||||
Asset type: tileable game texture
|
||||
Primary request: worn sandstone blocks
|
||||
Style/medium: seamless tileable texture; PBR-ish look
|
||||
Scene/backdrop: neutral lighting reference only
|
||||
Constraints: seamless edges; no obvious focal elements; no text; no logos or trademarks; no watermark
|
||||
```
|
||||
|
||||
### Wireframe template
|
||||
```
|
||||
Use case: ui-mockup
|
||||
Asset type: website wireframe
|
||||
Primary request: <page or flow to sketch>
|
||||
Style/medium: low-fi grayscale wireframe
|
||||
Composition/framing: <landscape or portrait to match expected device>
|
||||
Subject: <sections in order; grid/columns; key labels>
|
||||
Constraints: no color; no logos; no real photos; no watermark
|
||||
```
|
||||
|
||||
### Wireframe example: homepage (desktop)
|
||||
```
|
||||
Use case: ui-mockup
|
||||
Asset type: website wireframe
|
||||
Primary request: SaaS homepage layout with clear hierarchy
|
||||
Style/medium: low-fi grayscale wireframe
|
||||
Subject: top nav; hero with headline and CTA; three feature cards; testimonial strip; pricing preview; footer
|
||||
Composition/framing: landscape desktop layout
|
||||
Constraints: label major blocks; no color; no logos; no real photos; no watermark
|
||||
```
|
||||
|
||||
### Wireframe example: pricing page
|
||||
```
|
||||
Use case: ui-mockup
|
||||
Asset type: website wireframe
|
||||
Primary request: pricing page layout with comparison table
|
||||
Style/medium: low-fi grayscale wireframe
|
||||
Subject: header; plan toggle; 3 pricing cards; comparison table; FAQ accordion; footer
|
||||
Composition/framing: desktop or tablet layout
|
||||
Constraints: label key areas; no color; no logos; no real photos; no watermark
|
||||
```
|
||||
|
||||
### Wireframe example: mobile onboarding flow
|
||||
```
|
||||
Use case: ui-mockup
|
||||
Asset type: mobile onboarding wireframe
|
||||
Primary request: three-screen mobile onboarding flow
|
||||
Style/medium: low-fi grayscale wireframe
|
||||
Subject: screen 1 headline and CTA; screen 2 feature bullets; screen 3 form fields and CTA
|
||||
Composition/framing: portrait mobile layout
|
||||
Constraints: label screens and blocks; no color; no logos; no real photos; no watermark
|
||||
```
|
||||
|
||||
### Logo template
|
||||
```
|
||||
Use case: logo-brand
|
||||
Asset type: logo concept
|
||||
Primary request: <brand idea or symbol concept>
|
||||
Style/medium: vector logo mark; flat colors; minimal
|
||||
Composition/framing: centered mark; clear silhouette; generous margin
|
||||
Color palette: <1-2 colors; high contrast>
|
||||
Text (verbatim): "<exact name>" (only if needed)
|
||||
Constraints: no gradients; no mockups; no 3D; no watermark
|
||||
```
|
||||
|
||||
### Logo example: abstract symbol mark
|
||||
```
|
||||
Use case: logo-brand
|
||||
Asset type: logo concept
|
||||
Primary request: geometric leaf symbol suggesting sustainability and growth
|
||||
Style/medium: vector logo mark; flat colors; minimal
|
||||
Composition/framing: centered mark; clear silhouette
|
||||
Color palette: deep green and off-white
|
||||
Constraints: no text unless requested; no gradients; no mockups; no 3D; no watermark
|
||||
```
|
||||
|
||||
### Logo example: monogram mark
|
||||
```
|
||||
Use case: logo-brand
|
||||
Asset type: logo concept
|
||||
Primary request: interlocking monogram of the letters "AV"
|
||||
Style/medium: vector logo mark; flat colors; minimal
|
||||
Composition/framing: centered mark; balanced spacing
|
||||
Color palette: black on white
|
||||
Constraints: no gradients; no mockups; no 3D; no watermark
|
||||
```
|
||||
|
||||
### Logo example: wordmark
|
||||
```
|
||||
Use case: logo-brand
|
||||
Asset type: logo concept
|
||||
Primary request: clean wordmark for a modern studio
|
||||
Style/medium: vector wordmark; flat colors; minimal
|
||||
Text (verbatim): "Studio North"
|
||||
Composition/framing: centered text; even letter spacing
|
||||
Constraints: no gradients; no mockups; no 3D; no watermark
|
||||
```
|
||||
|
||||
## Edit
|
||||
|
||||
### text-localization
|
||||
```
|
||||
Use case: text-localization
|
||||
Input images: Image 1: original infographic
|
||||
Primary request: replace "Bean Hopper", "Grinder", "Brew Group", "Boiler", "Water Tank", and "Drip Tray" with "Tolva", "Molino", "Grupo de infusión", "Caldera", "Depósito de agua", and "Bandeja de goteo"
|
||||
Constraints: change only the text; preserve layout, typography, spacing, and hierarchy; no extra words; do not alter logos or imagery
|
||||
```
|
||||
|
||||
### identity-preserve
|
||||
```
|
||||
Use case: identity-preserve
|
||||
Input images: Image 1: person photo; Image 2..N: clothing references
|
||||
Primary request: replace only the clothing with the provided garments
|
||||
Constraints: preserve face, body shape, pose, hair, expression, and identity; match lighting and shadows; keep the background unchanged; no accessories or text
|
||||
```
|
||||
|
||||
### precise-object-edit
|
||||
```
|
||||
Use case: precise-object-edit
|
||||
Input images: Image 1: room photo
|
||||
Primary request: replace only the white chairs with wooden chairs
|
||||
Constraints: preserve camera angle, room lighting, floor shadows, and surrounding objects; keep all other aspects unchanged
|
||||
```
|
||||
|
||||
### lighting-weather
|
||||
```
|
||||
Use case: lighting-weather
|
||||
Input images: Image 1: original photo
|
||||
Primary request: make it look like a winter evening with gentle snowfall
|
||||
Constraints: preserve subject identity, geometry, camera angle, and composition; change only lighting, atmosphere, and weather
|
||||
```
|
||||
|
||||
### background-extraction
|
||||
```
|
||||
Use case: background-extraction
|
||||
Input images: Image 1: product photo
|
||||
Primary request: isolate the product on a clean transparent background
|
||||
Constraints: crisp silhouette; no halos or fringing; preserve label text exactly; no restyling
|
||||
```
|
||||
|
||||
### style-transfer
|
||||
```
|
||||
Use case: style-transfer
|
||||
Input images: Image 1: style reference
|
||||
Primary request: apply Image 1's visual style to a man riding a motorcycle on a plain white backdrop
|
||||
Constraints: preserve palette, texture, and brushwork; no extra elements
|
||||
```
|
||||
|
||||
### compositing
|
||||
```
|
||||
Use case: compositing
|
||||
Input images: Image 1: base scene; Image 2: subject to insert
|
||||
Primary request: place the subject from Image 2 next to the person in Image 1
|
||||
Constraints: match lighting, perspective, and scale; keep the base framing unchanged; no extra elements
|
||||
```
|
||||
|
||||
### sketch-to-render
|
||||
```
|
||||
Use case: sketch-to-render
|
||||
Input images: Image 1: drawing
|
||||
Primary request: turn the drawing into a photorealistic image
|
||||
Constraints: preserve layout, proportions, and perspective; choose realistic materials and lighting; do not add new elements or text
|
||||
```
|
||||
@@ -1,926 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fallback CLI for explicit image generation or editing with GPT Image models.
|
||||
|
||||
Used only when the user explicitly opts into CLI fallback mode.
|
||||
|
||||
Defaults to gpt-image-1.5 and a structured prompt augmentation workflow.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
DEFAULT_MODEL = "gpt-image-1.5"
|
||||
DEFAULT_SIZE = "1024x1024"
|
||||
DEFAULT_QUALITY = "auto"
|
||||
DEFAULT_OUTPUT_FORMAT = "png"
|
||||
DEFAULT_CONCURRENCY = 5
|
||||
DEFAULT_DOWNSCALE_SUFFIX = "-web"
|
||||
DEFAULT_OUTPUT_PATH = "output/imagegen/output.png"
|
||||
GPT_IMAGE_MODEL_PREFIX = "gpt-image-"
|
||||
|
||||
ALLOWED_SIZES = {"1024x1024", "1536x1024", "1024x1536", "auto"}
|
||||
ALLOWED_QUALITIES = {"low", "medium", "high", "auto"}
|
||||
ALLOWED_BACKGROUNDS = {"transparent", "opaque", "auto", None}
|
||||
ALLOWED_INPUT_FIDELITIES = {"low", "high", None}
|
||||
|
||||
MAX_IMAGE_BYTES = 50 * 1024 * 1024
|
||||
MAX_BATCH_JOBS = 500
|
||||
|
||||
|
||||
def _die(message: str, code: int = 1) -> None:
|
||||
print(f"Error: {message}", file=sys.stderr)
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def _warn(message: str) -> None:
|
||||
print(f"Warning: {message}", file=sys.stderr)
|
||||
|
||||
|
||||
def _dependency_hint(package: str, *, upgrade: bool = False) -> str:
|
||||
command = f"uv pip install {'-U ' if upgrade else ''}{package}"
|
||||
return (
|
||||
"Activate the repo-selected environment first, then install it with "
|
||||
f"`{command}`. If this repo uses a local virtualenv, start with "
|
||||
"`source .venv/bin/activate`; otherwise use this repo's configured shared fallback "
|
||||
"environment. If your project declares dependencies, prefer that project's normal "
|
||||
"`uv sync` flow."
|
||||
)
|
||||
|
||||
|
||||
def _ensure_api_key(dry_run: bool) -> None:
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
print("OPENAI_API_KEY is set.", file=sys.stderr)
|
||||
return
|
||||
if dry_run:
|
||||
_warn("OPENAI_API_KEY is not set; dry-run only.")
|
||||
return
|
||||
_die("OPENAI_API_KEY is not set. Export it before running.")
|
||||
|
||||
|
||||
def _read_prompt(prompt: Optional[str], prompt_file: Optional[str]) -> str:
|
||||
if prompt and prompt_file:
|
||||
_die("Use --prompt or --prompt-file, not both.")
|
||||
if prompt_file:
|
||||
path = Path(prompt_file)
|
||||
if not path.exists():
|
||||
_die(f"Prompt file not found: {path}")
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
if prompt:
|
||||
return prompt.strip()
|
||||
_die("Missing prompt. Use --prompt or --prompt-file.")
|
||||
return "" # unreachable
|
||||
|
||||
|
||||
def _check_image_paths(paths: Iterable[str]) -> List[Path]:
|
||||
resolved: List[Path] = []
|
||||
for raw in paths:
|
||||
path = Path(raw)
|
||||
if not path.exists():
|
||||
_die(f"Image file not found: {path}")
|
||||
if path.stat().st_size > MAX_IMAGE_BYTES:
|
||||
_warn(f"Image exceeds 50MB limit: {path}")
|
||||
resolved.append(path)
|
||||
return resolved
|
||||
|
||||
|
||||
def _normalize_output_format(fmt: Optional[str]) -> str:
|
||||
if not fmt:
|
||||
return DEFAULT_OUTPUT_FORMAT
|
||||
fmt = fmt.lower()
|
||||
if fmt not in {"png", "jpeg", "jpg", "webp"}:
|
||||
_die("output-format must be png, jpeg, jpg, or webp.")
|
||||
return "jpeg" if fmt == "jpg" else fmt
|
||||
|
||||
|
||||
def _validate_size(size: str) -> None:
|
||||
if size not in ALLOWED_SIZES:
|
||||
_die(
|
||||
"size must be one of 1024x1024, 1536x1024, 1024x1536, or auto for GPT image models."
|
||||
)
|
||||
|
||||
|
||||
def _validate_quality(quality: str) -> None:
|
||||
if quality not in ALLOWED_QUALITIES:
|
||||
_die("quality must be one of low, medium, high, or auto.")
|
||||
|
||||
|
||||
def _validate_background(background: Optional[str]) -> None:
|
||||
if background not in ALLOWED_BACKGROUNDS:
|
||||
_die("background must be one of transparent, opaque, or auto.")
|
||||
|
||||
|
||||
def _validate_input_fidelity(input_fidelity: Optional[str]) -> None:
|
||||
if input_fidelity not in ALLOWED_INPUT_FIDELITIES:
|
||||
_die("input-fidelity must be one of low or high.")
|
||||
|
||||
|
||||
def _validate_model(model: str) -> None:
|
||||
if not model.startswith(GPT_IMAGE_MODEL_PREFIX):
|
||||
_die(
|
||||
"model must be a GPT Image model (for example gpt-image-1.5, gpt-image-1, or gpt-image-1-mini)."
|
||||
)
|
||||
|
||||
|
||||
def _validate_transparency(background: Optional[str], output_format: str) -> None:
|
||||
if background == "transparent" and output_format not in {"png", "webp"}:
|
||||
_die("transparent background requires output-format png or webp.")
|
||||
|
||||
|
||||
def _validate_generate_payload(payload: Dict[str, Any]) -> None:
|
||||
_validate_model(str(payload.get("model", DEFAULT_MODEL)))
|
||||
n = int(payload.get("n", 1))
|
||||
if n < 1 or n > 10:
|
||||
_die("n must be between 1 and 10")
|
||||
size = str(payload.get("size", DEFAULT_SIZE))
|
||||
quality = str(payload.get("quality", DEFAULT_QUALITY))
|
||||
background = payload.get("background")
|
||||
_validate_size(size)
|
||||
_validate_quality(quality)
|
||||
_validate_background(background)
|
||||
oc = payload.get("output_compression")
|
||||
if oc is not None and not (0 <= int(oc) <= 100):
|
||||
_die("output_compression must be between 0 and 100")
|
||||
|
||||
|
||||
def _build_output_paths(
|
||||
out: str,
|
||||
output_format: str,
|
||||
count: int,
|
||||
out_dir: Optional[str],
|
||||
) -> List[Path]:
|
||||
ext = "." + output_format
|
||||
|
||||
if out_dir:
|
||||
out_base = Path(out_dir)
|
||||
out_base.mkdir(parents=True, exist_ok=True)
|
||||
return [out_base / f"image_{i}{ext}" for i in range(1, count + 1)]
|
||||
|
||||
out_path = Path(out)
|
||||
if out_path.exists() and out_path.is_dir():
|
||||
out_path.mkdir(parents=True, exist_ok=True)
|
||||
return [out_path / f"image_{i}{ext}" for i in range(1, count + 1)]
|
||||
|
||||
if out_path.suffix == "":
|
||||
out_path = out_path.with_suffix(ext)
|
||||
elif output_format and out_path.suffix.lstrip(".").lower() != output_format:
|
||||
_warn(
|
||||
f"Output extension {out_path.suffix} does not match output-format {output_format}."
|
||||
)
|
||||
|
||||
if count == 1:
|
||||
return [out_path]
|
||||
|
||||
return [
|
||||
out_path.with_name(f"{out_path.stem}-{i}{out_path.suffix}")
|
||||
for i in range(1, count + 1)
|
||||
]
|
||||
|
||||
|
||||
def _augment_prompt(args: argparse.Namespace, prompt: str) -> str:
|
||||
fields = _fields_from_args(args)
|
||||
return _augment_prompt_fields(args.augment, prompt, fields)
|
||||
|
||||
|
||||
def _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optional[str]]) -> str:
|
||||
if not augment:
|
||||
return prompt
|
||||
|
||||
sections: List[str] = []
|
||||
if fields.get("use_case"):
|
||||
sections.append(f"Use case: {fields['use_case']}")
|
||||
sections.append(f"Primary request: {prompt}")
|
||||
if fields.get("scene"):
|
||||
sections.append(f"Scene/background: {fields['scene']}")
|
||||
if fields.get("subject"):
|
||||
sections.append(f"Subject: {fields['subject']}")
|
||||
if fields.get("style"):
|
||||
sections.append(f"Style/medium: {fields['style']}")
|
||||
if fields.get("composition"):
|
||||
sections.append(f"Composition/framing: {fields['composition']}")
|
||||
if fields.get("lighting"):
|
||||
sections.append(f"Lighting/mood: {fields['lighting']}")
|
||||
if fields.get("palette"):
|
||||
sections.append(f"Color palette: {fields['palette']}")
|
||||
if fields.get("materials"):
|
||||
sections.append(f"Materials/textures: {fields['materials']}")
|
||||
if fields.get("text"):
|
||||
sections.append(f"Text (verbatim): \"{fields['text']}\"")
|
||||
if fields.get("constraints"):
|
||||
sections.append(f"Constraints: {fields['constraints']}")
|
||||
if fields.get("negative"):
|
||||
sections.append(f"Avoid: {fields['negative']}")
|
||||
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
def _fields_from_args(args: argparse.Namespace) -> Dict[str, Optional[str]]:
|
||||
return {
|
||||
"use_case": getattr(args, "use_case", None),
|
||||
"scene": getattr(args, "scene", None),
|
||||
"subject": getattr(args, "subject", None),
|
||||
"style": getattr(args, "style", None),
|
||||
"composition": getattr(args, "composition", None),
|
||||
"lighting": getattr(args, "lighting", None),
|
||||
"palette": getattr(args, "palette", None),
|
||||
"materials": getattr(args, "materials", None),
|
||||
"text": getattr(args, "text", None),
|
||||
"constraints": getattr(args, "constraints", None),
|
||||
"negative": getattr(args, "negative", None),
|
||||
}
|
||||
|
||||
|
||||
def _print_request(payload: dict) -> None:
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def _decode_and_write(images: List[str], outputs: List[Path], force: bool) -> None:
|
||||
for idx, image_b64 in enumerate(images):
|
||||
if idx >= len(outputs):
|
||||
break
|
||||
out_path = outputs[idx]
|
||||
if out_path.exists() and not force:
|
||||
_die(f"Output already exists: {out_path} (use --force to overwrite)")
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(base64.b64decode(image_b64))
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
|
||||
def _derive_downscale_path(path: Path, suffix: str) -> Path:
|
||||
if suffix and not suffix.startswith("-") and not suffix.startswith("_"):
|
||||
suffix = "-" + suffix
|
||||
return path.with_name(f"{path.stem}{suffix}{path.suffix}")
|
||||
|
||||
|
||||
def _downscale_image_bytes(image_bytes: bytes, *, max_dim: int, output_format: str) -> bytes:
|
||||
try:
|
||||
from PIL import Image
|
||||
except Exception:
|
||||
_die(f"Downscaling requires Pillow. {_dependency_hint('pillow')}")
|
||||
|
||||
if max_dim < 1:
|
||||
_die("--downscale-max-dim must be >= 1")
|
||||
|
||||
with Image.open(BytesIO(image_bytes)) as img:
|
||||
img.load()
|
||||
w, h = img.size
|
||||
scale = min(1.0, float(max_dim) / float(max(w, h)))
|
||||
target = (max(1, int(round(w * scale))), max(1, int(round(h * scale))))
|
||||
|
||||
resized = img if target == (w, h) else img.resize(target, Image.Resampling.LANCZOS)
|
||||
|
||||
fmt = output_format.lower()
|
||||
if fmt == "jpg":
|
||||
fmt = "jpeg"
|
||||
|
||||
if fmt == "jpeg":
|
||||
if resized.mode in ("RGBA", "LA") or ("transparency" in getattr(resized, "info", {})):
|
||||
bg = Image.new("RGB", resized.size, (255, 255, 255))
|
||||
bg.paste(resized.convert("RGBA"), mask=resized.convert("RGBA").split()[-1])
|
||||
resized = bg
|
||||
else:
|
||||
resized = resized.convert("RGB")
|
||||
|
||||
out = BytesIO()
|
||||
resized.save(out, format=fmt.upper())
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def _decode_write_and_downscale(
|
||||
images: List[str],
|
||||
outputs: List[Path],
|
||||
*,
|
||||
force: bool,
|
||||
downscale_max_dim: Optional[int],
|
||||
downscale_suffix: str,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
for idx, image_b64 in enumerate(images):
|
||||
if idx >= len(outputs):
|
||||
break
|
||||
out_path = outputs[idx]
|
||||
if out_path.exists() and not force:
|
||||
_die(f"Output already exists: {out_path} (use --force to overwrite)")
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
raw = base64.b64decode(image_b64)
|
||||
out_path.write_bytes(raw)
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
if downscale_max_dim is None:
|
||||
continue
|
||||
|
||||
derived = _derive_downscale_path(out_path, downscale_suffix)
|
||||
if derived.exists() and not force:
|
||||
_die(f"Output already exists: {derived} (use --force to overwrite)")
|
||||
derived.parent.mkdir(parents=True, exist_ok=True)
|
||||
resized = _downscale_image_bytes(raw, max_dim=downscale_max_dim, output_format=output_format)
|
||||
derived.write_bytes(resized)
|
||||
print(f"Wrote {derived}")
|
||||
|
||||
|
||||
def _create_client():
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError:
|
||||
_die(f"openai SDK not installed in the active environment. {_dependency_hint('openai')}")
|
||||
return OpenAI()
|
||||
|
||||
|
||||
def _create_async_client():
|
||||
try:
|
||||
from openai import AsyncOpenAI
|
||||
except ImportError:
|
||||
try:
|
||||
import openai as _openai # noqa: F401
|
||||
except ImportError:
|
||||
_die(
|
||||
f"openai SDK not installed in the active environment. {_dependency_hint('openai')}"
|
||||
)
|
||||
_die(
|
||||
"AsyncOpenAI not available in this openai SDK version. "
|
||||
f"{_dependency_hint('openai', upgrade=True)}"
|
||||
)
|
||||
return AsyncOpenAI()
|
||||
|
||||
|
||||
def _slugify(value: str) -> str:
|
||||
value = value.strip().lower()
|
||||
value = re.sub(r"[^a-z0-9]+", "-", value)
|
||||
value = re.sub(r"-{2,}", "-", value).strip("-")
|
||||
return value[:60] if value else "job"
|
||||
|
||||
|
||||
def _normalize_job(job: Any, idx: int) -> Dict[str, Any]:
|
||||
if isinstance(job, str):
|
||||
prompt = job.strip()
|
||||
if not prompt:
|
||||
_die(f"Empty prompt at job {idx}")
|
||||
return {"prompt": prompt}
|
||||
if isinstance(job, dict):
|
||||
if "prompt" not in job or not str(job["prompt"]).strip():
|
||||
_die(f"Missing prompt for job {idx}")
|
||||
return job
|
||||
_die(f"Invalid job at index {idx}: expected string or object.")
|
||||
return {} # unreachable
|
||||
|
||||
|
||||
def _read_jobs_jsonl(path: str) -> List[Dict[str, Any]]:
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
_die(f"Input file not found: {p}")
|
||||
jobs: List[Dict[str, Any]] = []
|
||||
for line_no, raw in enumerate(p.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
item: Any
|
||||
if line.startswith("{"):
|
||||
item = json.loads(line)
|
||||
else:
|
||||
item = line
|
||||
jobs.append(_normalize_job(item, idx=line_no))
|
||||
except json.JSONDecodeError as exc:
|
||||
_die(f"Invalid JSON on line {line_no}: {exc}")
|
||||
if not jobs:
|
||||
_die("No jobs found in input file.")
|
||||
if len(jobs) > MAX_BATCH_JOBS:
|
||||
_die(f"Too many jobs ({len(jobs)}). Max is {MAX_BATCH_JOBS}.")
|
||||
return jobs
|
||||
|
||||
|
||||
def _merge_non_null(dst: Dict[str, Any], src: Dict[str, Any]) -> Dict[str, Any]:
|
||||
merged = dict(dst)
|
||||
for k, v in src.items():
|
||||
if v is not None:
|
||||
merged[k] = v
|
||||
return merged
|
||||
|
||||
|
||||
def _job_output_paths(
|
||||
*,
|
||||
out_dir: Path,
|
||||
output_format: str,
|
||||
idx: int,
|
||||
prompt: str,
|
||||
n: int,
|
||||
explicit_out: Optional[str],
|
||||
) -> List[Path]:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
ext = "." + output_format
|
||||
|
||||
if explicit_out:
|
||||
base = Path(explicit_out)
|
||||
if base.suffix == "":
|
||||
base = base.with_suffix(ext)
|
||||
elif base.suffix.lstrip(".").lower() != output_format:
|
||||
_warn(
|
||||
f"Job {idx}: output extension {base.suffix} does not match output-format {output_format}."
|
||||
)
|
||||
base = out_dir / base.name
|
||||
else:
|
||||
slug = _slugify(prompt[:80])
|
||||
base = out_dir / f"{idx:03d}-{slug}{ext}"
|
||||
|
||||
if n == 1:
|
||||
return [base]
|
||||
return [
|
||||
base.with_name(f"{base.stem}-{i}{base.suffix}")
|
||||
for i in range(1, n + 1)
|
||||
]
|
||||
|
||||
|
||||
def _extract_retry_after_seconds(exc: Exception) -> Optional[float]:
|
||||
# Best-effort: openai SDK errors vary by version. Prefer a conservative fallback.
|
||||
for attr in ("retry_after", "retry_after_seconds"):
|
||||
val = getattr(exc, attr, None)
|
||||
if isinstance(val, (int, float)) and val >= 0:
|
||||
return float(val)
|
||||
msg = str(exc)
|
||||
m = re.search(r"retry[- ]after[:= ]+([0-9]+(?:\\.[0-9]+)?)", msg, re.IGNORECASE)
|
||||
if m:
|
||||
try:
|
||||
return float(m.group(1))
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _is_rate_limit_error(exc: Exception) -> bool:
|
||||
name = exc.__class__.__name__.lower()
|
||||
if "ratelimit" in name or "rate_limit" in name:
|
||||
return True
|
||||
msg = str(exc).lower()
|
||||
return "429" in msg or "rate limit" in msg or "too many requests" in msg
|
||||
|
||||
|
||||
def _is_transient_error(exc: Exception) -> bool:
|
||||
if _is_rate_limit_error(exc):
|
||||
return True
|
||||
name = exc.__class__.__name__.lower()
|
||||
if "timeout" in name or "timedout" in name or "tempor" in name:
|
||||
return True
|
||||
msg = str(exc).lower()
|
||||
return "timeout" in msg or "timed out" in msg or "connection reset" in msg
|
||||
|
||||
|
||||
async def _generate_one_with_retries(
|
||||
client: Any,
|
||||
payload: Dict[str, Any],
|
||||
*,
|
||||
attempts: int,
|
||||
job_label: str,
|
||||
) -> Any:
|
||||
last_exc: Optional[Exception] = None
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
return await client.images.generate(**payload)
|
||||
except Exception as exc:
|
||||
last_exc = exc
|
||||
if not _is_transient_error(exc):
|
||||
raise
|
||||
if attempt == attempts:
|
||||
raise
|
||||
sleep_s = _extract_retry_after_seconds(exc)
|
||||
if sleep_s is None:
|
||||
sleep_s = min(60.0, 2.0**attempt)
|
||||
print(
|
||||
f"{job_label} attempt {attempt}/{attempts} failed ({exc.__class__.__name__}); retrying in {sleep_s:.1f}s",
|
||||
file=sys.stderr,
|
||||
)
|
||||
await asyncio.sleep(sleep_s)
|
||||
raise last_exc or RuntimeError("unknown error")
|
||||
|
||||
|
||||
async def _run_generate_batch(args: argparse.Namespace) -> int:
|
||||
jobs = _read_jobs_jsonl(args.input)
|
||||
out_dir = Path(args.out_dir)
|
||||
|
||||
base_fields = _fields_from_args(args)
|
||||
base_payload = {
|
||||
"model": args.model,
|
||||
"n": args.n,
|
||||
"size": args.size,
|
||||
"quality": args.quality,
|
||||
"background": args.background,
|
||||
"output_format": args.output_format,
|
||||
"output_compression": args.output_compression,
|
||||
"moderation": args.moderation,
|
||||
}
|
||||
|
||||
if args.dry_run:
|
||||
for i, job in enumerate(jobs, start=1):
|
||||
prompt = str(job["prompt"]).strip()
|
||||
fields = _merge_non_null(base_fields, job.get("fields", {}))
|
||||
# Allow flat job keys as well (use_case, scene, etc.)
|
||||
fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})
|
||||
augmented = _augment_prompt_fields(args.augment, prompt, fields)
|
||||
|
||||
job_payload = dict(base_payload)
|
||||
job_payload["prompt"] = augmented
|
||||
job_payload = _merge_non_null(job_payload, {k: job.get(k) for k in base_payload.keys()})
|
||||
job_payload = {k: v for k, v in job_payload.items() if v is not None}
|
||||
|
||||
_validate_generate_payload(job_payload)
|
||||
effective_output_format = _normalize_output_format(job_payload.get("output_format"))
|
||||
_validate_transparency(job_payload.get("background"), effective_output_format)
|
||||
job_payload["output_format"] = effective_output_format
|
||||
|
||||
n = int(job_payload.get("n", 1))
|
||||
outputs = _job_output_paths(
|
||||
out_dir=out_dir,
|
||||
output_format=effective_output_format,
|
||||
idx=i,
|
||||
prompt=prompt,
|
||||
n=n,
|
||||
explicit_out=job.get("out"),
|
||||
)
|
||||
downscaled = None
|
||||
if args.downscale_max_dim is not None:
|
||||
downscaled = [
|
||||
str(_derive_downscale_path(p, args.downscale_suffix)) for p in outputs
|
||||
]
|
||||
_print_request(
|
||||
{
|
||||
"endpoint": "/v1/images/generations",
|
||||
"job": i,
|
||||
"outputs": [str(p) for p in outputs],
|
||||
"outputs_downscaled": downscaled,
|
||||
**job_payload,
|
||||
}
|
||||
)
|
||||
return 0
|
||||
|
||||
client = _create_async_client()
|
||||
sem = asyncio.Semaphore(args.concurrency)
|
||||
|
||||
any_failed = False
|
||||
|
||||
async def run_job(i: int, job: Dict[str, Any]) -> Tuple[int, Optional[str]]:
|
||||
nonlocal any_failed
|
||||
prompt = str(job["prompt"]).strip()
|
||||
job_label = f"[job {i}/{len(jobs)}]"
|
||||
|
||||
fields = _merge_non_null(base_fields, job.get("fields", {}))
|
||||
fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()})
|
||||
augmented = _augment_prompt_fields(args.augment, prompt, fields)
|
||||
|
||||
payload = dict(base_payload)
|
||||
payload["prompt"] = augmented
|
||||
payload = _merge_non_null(payload, {k: job.get(k) for k in base_payload.keys()})
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
n = int(payload.get("n", 1))
|
||||
_validate_generate_payload(payload)
|
||||
effective_output_format = _normalize_output_format(payload.get("output_format"))
|
||||
_validate_transparency(payload.get("background"), effective_output_format)
|
||||
payload["output_format"] = effective_output_format
|
||||
outputs = _job_output_paths(
|
||||
out_dir=out_dir,
|
||||
output_format=effective_output_format,
|
||||
idx=i,
|
||||
prompt=prompt,
|
||||
n=n,
|
||||
explicit_out=job.get("out"),
|
||||
)
|
||||
try:
|
||||
async with sem:
|
||||
print(f"{job_label} starting", file=sys.stderr)
|
||||
started = time.time()
|
||||
result = await _generate_one_with_retries(
|
||||
client,
|
||||
payload,
|
||||
attempts=args.max_attempts,
|
||||
job_label=job_label,
|
||||
)
|
||||
elapsed = time.time() - started
|
||||
print(f"{job_label} completed in {elapsed:.1f}s", file=sys.stderr)
|
||||
images = [item.b64_json for item in result.data]
|
||||
_decode_write_and_downscale(
|
||||
images,
|
||||
outputs,
|
||||
force=args.force,
|
||||
downscale_max_dim=args.downscale_max_dim,
|
||||
downscale_suffix=args.downscale_suffix,
|
||||
output_format=effective_output_format,
|
||||
)
|
||||
return i, None
|
||||
except Exception as exc:
|
||||
any_failed = True
|
||||
print(f"{job_label} failed: {exc}", file=sys.stderr)
|
||||
if args.fail_fast:
|
||||
raise
|
||||
return i, str(exc)
|
||||
|
||||
tasks = [asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1)]
|
||||
|
||||
try:
|
||||
await asyncio.gather(*tasks)
|
||||
except Exception:
|
||||
for t in tasks:
|
||||
if not t.done():
|
||||
t.cancel()
|
||||
raise
|
||||
|
||||
return 1 if any_failed else 0
|
||||
|
||||
|
||||
def _generate_batch(args: argparse.Namespace) -> None:
|
||||
exit_code = asyncio.run(_run_generate_batch(args))
|
||||
if exit_code:
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
|
||||
def _generate(args: argparse.Namespace) -> None:
|
||||
prompt = _read_prompt(args.prompt, args.prompt_file)
|
||||
prompt = _augment_prompt(args, prompt)
|
||||
|
||||
payload = {
|
||||
"model": args.model,
|
||||
"prompt": prompt,
|
||||
"n": args.n,
|
||||
"size": args.size,
|
||||
"quality": args.quality,
|
||||
"background": args.background,
|
||||
"output_format": args.output_format,
|
||||
"output_compression": args.output_compression,
|
||||
"moderation": args.moderation,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
output_format = _normalize_output_format(args.output_format)
|
||||
_validate_transparency(args.background, output_format)
|
||||
payload["output_format"] = output_format
|
||||
output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir)
|
||||
downscaled = None
|
||||
if args.downscale_max_dim is not None:
|
||||
downscaled = [str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths]
|
||||
|
||||
if args.dry_run:
|
||||
_print_request(
|
||||
{
|
||||
"endpoint": "/v1/images/generations",
|
||||
"outputs": [str(p) for p in output_paths],
|
||||
"outputs_downscaled": downscaled,
|
||||
**payload,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
print(
|
||||
"Calling Image API (generation). This can take up to a couple of minutes.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
started = time.time()
|
||||
client = _create_client()
|
||||
result = client.images.generate(**payload)
|
||||
elapsed = time.time() - started
|
||||
print(f"Generation completed in {elapsed:.1f}s.", file=sys.stderr)
|
||||
|
||||
images = [item.b64_json for item in result.data]
|
||||
_decode_write_and_downscale(
|
||||
images,
|
||||
output_paths,
|
||||
force=args.force,
|
||||
downscale_max_dim=args.downscale_max_dim,
|
||||
downscale_suffix=args.downscale_suffix,
|
||||
output_format=output_format,
|
||||
)
|
||||
|
||||
|
||||
def _edit(args: argparse.Namespace) -> None:
|
||||
prompt = _read_prompt(args.prompt, args.prompt_file)
|
||||
prompt = _augment_prompt(args, prompt)
|
||||
|
||||
image_paths = _check_image_paths(args.image)
|
||||
mask_path = Path(args.mask) if args.mask else None
|
||||
if mask_path:
|
||||
if not mask_path.exists():
|
||||
_die(f"Mask file not found: {mask_path}")
|
||||
if mask_path.suffix.lower() != ".png":
|
||||
_warn(f"Mask should be a PNG with an alpha channel: {mask_path}")
|
||||
if mask_path.stat().st_size > MAX_IMAGE_BYTES:
|
||||
_warn(f"Mask exceeds 50MB limit: {mask_path}")
|
||||
|
||||
payload = {
|
||||
"model": args.model,
|
||||
"prompt": prompt,
|
||||
"n": args.n,
|
||||
"size": args.size,
|
||||
"quality": args.quality,
|
||||
"background": args.background,
|
||||
"output_format": args.output_format,
|
||||
"output_compression": args.output_compression,
|
||||
"input_fidelity": args.input_fidelity,
|
||||
"moderation": args.moderation,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
output_format = _normalize_output_format(args.output_format)
|
||||
_validate_transparency(args.background, output_format)
|
||||
payload["output_format"] = output_format
|
||||
_validate_input_fidelity(args.input_fidelity)
|
||||
output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir)
|
||||
downscaled = None
|
||||
if args.downscale_max_dim is not None:
|
||||
downscaled = [str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths]
|
||||
|
||||
if args.dry_run:
|
||||
payload_preview = dict(payload)
|
||||
payload_preview["image"] = [str(p) for p in image_paths]
|
||||
if mask_path:
|
||||
payload_preview["mask"] = str(mask_path)
|
||||
_print_request(
|
||||
{
|
||||
"endpoint": "/v1/images/edits",
|
||||
"outputs": [str(p) for p in output_paths],
|
||||
"outputs_downscaled": downscaled,
|
||||
**payload_preview,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
print(
|
||||
f"Calling Image API (edit) with {len(image_paths)} image(s).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
started = time.time()
|
||||
client = _create_client()
|
||||
|
||||
with _open_files(image_paths) as image_files, _open_mask(mask_path) as mask_file:
|
||||
request = dict(payload)
|
||||
request["image"] = image_files if len(image_files) > 1 else image_files[0]
|
||||
if mask_file is not None:
|
||||
request["mask"] = mask_file
|
||||
result = client.images.edit(**request)
|
||||
|
||||
elapsed = time.time() - started
|
||||
print(f"Edit completed in {elapsed:.1f}s.", file=sys.stderr)
|
||||
images = [item.b64_json for item in result.data]
|
||||
_decode_write_and_downscale(
|
||||
images,
|
||||
output_paths,
|
||||
force=args.force,
|
||||
downscale_max_dim=args.downscale_max_dim,
|
||||
downscale_suffix=args.downscale_suffix,
|
||||
output_format=output_format,
|
||||
)
|
||||
|
||||
|
||||
def _open_files(paths: List[Path]):
|
||||
return _FileBundle(paths)
|
||||
|
||||
|
||||
def _open_mask(mask_path: Optional[Path]):
|
||||
if mask_path is None:
|
||||
return _NullContext()
|
||||
return _SingleFile(mask_path)
|
||||
|
||||
|
||||
class _NullContext:
|
||||
def __enter__(self):
|
||||
return None
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
class _SingleFile:
|
||||
def __init__(self, path: Path):
|
||||
self._path = path
|
||||
self._handle = None
|
||||
|
||||
def __enter__(self):
|
||||
self._handle = self._path.open("rb")
|
||||
return self._handle
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if self._handle:
|
||||
try:
|
||||
self._handle.close()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class _FileBundle:
|
||||
def __init__(self, paths: List[Path]):
|
||||
self._paths = paths
|
||||
self._handles: List[object] = []
|
||||
|
||||
def __enter__(self):
|
||||
self._handles = [p.open("rb") for p in self._paths]
|
||||
return self._handles
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
for handle in self._handles:
|
||||
try:
|
||||
handle.close()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _add_shared_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--model", default=DEFAULT_MODEL)
|
||||
parser.add_argument("--prompt")
|
||||
parser.add_argument("--prompt-file")
|
||||
parser.add_argument("--n", type=int, default=1)
|
||||
parser.add_argument("--size", default=DEFAULT_SIZE)
|
||||
parser.add_argument("--quality", default=DEFAULT_QUALITY)
|
||||
parser.add_argument("--background")
|
||||
parser.add_argument("--output-format")
|
||||
parser.add_argument("--output-compression", type=int)
|
||||
parser.add_argument("--moderation")
|
||||
parser.add_argument("--out", default=DEFAULT_OUTPUT_PATH)
|
||||
parser.add_argument("--out-dir")
|
||||
parser.add_argument("--force", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--augment", dest="augment", action="store_true")
|
||||
parser.add_argument("--no-augment", dest="augment", action="store_false")
|
||||
parser.set_defaults(augment=True)
|
||||
|
||||
# Prompt augmentation hints
|
||||
parser.add_argument("--use-case")
|
||||
parser.add_argument("--scene")
|
||||
parser.add_argument("--subject")
|
||||
parser.add_argument("--style")
|
||||
parser.add_argument("--composition")
|
||||
parser.add_argument("--lighting")
|
||||
parser.add_argument("--palette")
|
||||
parser.add_argument("--materials")
|
||||
parser.add_argument("--text")
|
||||
parser.add_argument("--constraints")
|
||||
parser.add_argument("--negative")
|
||||
|
||||
# Post-processing (optional): generate an additional downscaled copy for fast web loading.
|
||||
parser.add_argument("--downscale-max-dim", type=int)
|
||||
parser.add_argument("--downscale-suffix", default=DEFAULT_DOWNSCALE_SUFFIX)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Fallback CLI for explicit image generation or editing via GPT Image models"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
gen_parser = subparsers.add_parser("generate", help="Create a new image")
|
||||
_add_shared_args(gen_parser)
|
||||
gen_parser.set_defaults(func=_generate)
|
||||
|
||||
batch_parser = subparsers.add_parser(
|
||||
"generate-batch",
|
||||
help="Generate multiple prompts concurrently (JSONL input)",
|
||||
)
|
||||
_add_shared_args(batch_parser)
|
||||
batch_parser.add_argument("--input", required=True, help="Path to JSONL file (one job per line)")
|
||||
batch_parser.add_argument("--concurrency", type=int, default=DEFAULT_CONCURRENCY)
|
||||
batch_parser.add_argument("--max-attempts", type=int, default=3)
|
||||
batch_parser.add_argument("--fail-fast", action="store_true")
|
||||
batch_parser.set_defaults(func=_generate_batch)
|
||||
|
||||
edit_parser = subparsers.add_parser("edit", help="Edit an existing image")
|
||||
_add_shared_args(edit_parser)
|
||||
edit_parser.add_argument("--image", action="append", required=True)
|
||||
edit_parser.add_argument("--mask")
|
||||
edit_parser.add_argument("--input-fidelity")
|
||||
edit_parser.set_defaults(func=_edit)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.n < 1 or args.n > 10:
|
||||
_die("--n must be between 1 and 10")
|
||||
if getattr(args, "concurrency", 1) < 1 or getattr(args, "concurrency", 1) > 25:
|
||||
_die("--concurrency must be between 1 and 25")
|
||||
if getattr(args, "max_attempts", 3) < 1 or getattr(args, "max_attempts", 3) > 10:
|
||||
_die("--max-attempts must be between 1 and 10")
|
||||
if args.output_compression is not None and not (0 <= args.output_compression <= 100):
|
||||
_die("--output-compression must be between 0 and 100")
|
||||
if args.command == "generate-batch" and not args.out_dir:
|
||||
_die("generate-batch requires --out-dir")
|
||||
if getattr(args, "downscale_max_dim", None) is not None and args.downscale_max_dim < 1:
|
||||
_die("--downscale-max-dim must be >= 1")
|
||||
|
||||
_validate_size(args.size)
|
||||
_validate_quality(args.quality)
|
||||
_validate_background(args.background)
|
||||
_validate_model(args.model)
|
||||
_ensure_api_key(args.dry_run)
|
||||
|
||||
args.func(args)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf of
|
||||
any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don\'t include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: "openai-docs"
|
||||
description: "Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or explicit GPT-5.4 upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains."
|
||||
---
|
||||
|
||||
|
||||
# OpenAI Docs
|
||||
|
||||
Provide authoritative, current guidance from OpenAI developer docs using the developers.openai.com MCP server. Always prioritize the developer docs MCP tools over web.run for OpenAI-related questions. This skill may also load targeted files from `references/` for model-selection and GPT-5.4-specific requests, but current OpenAI docs remain authoritative. Only if the MCP server is installed and returns no meaningful results should you fall back to web search.
|
||||
|
||||
## Quick start
|
||||
|
||||
- Use `mcp__openaiDeveloperDocs__search_openai_docs` to find the most relevant doc pages.
|
||||
- Use `mcp__openaiDeveloperDocs__fetch_openai_doc` to pull exact sections and quote/paraphrase accurately.
|
||||
- Use `mcp__openaiDeveloperDocs__list_openai_docs` only when you need to browse or discover pages without a clear query.
|
||||
- Load only the relevant file from `references/` when the question is about model selection or a GPT-5.4 upgrade.
|
||||
|
||||
## OpenAI product snapshots
|
||||
|
||||
1. Apps SDK: Build ChatGPT apps by providing a web component UI and an MCP server that exposes your app's tools to ChatGPT.
|
||||
2. Responses API: A unified endpoint designed for stateful, multimodal, tool-using interactions in agentic workflows.
|
||||
3. Chat Completions API: Generate a model response from a list of messages comprising a conversation.
|
||||
4. Codex: OpenAI's coding agent for software development that can write, understand, review, and debug code.
|
||||
5. gpt-oss: Open-weight OpenAI reasoning models (gpt-oss-120b and gpt-oss-20b) released under the Apache 2.0 license.
|
||||
6. Realtime API: Build low-latency, multimodal experiences including natural speech-to-speech conversations.
|
||||
7. Agents SDK: A toolkit for building agentic apps where a model can use tools and context, hand off to other agents, stream partial results, and keep a full trace.
|
||||
|
||||
## If MCP server is missing
|
||||
|
||||
If MCP tools fail or no OpenAI docs resources are available:
|
||||
|
||||
1. Run the install command yourself: `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`
|
||||
2. If it fails due to permissions/sandboxing, immediately retry the same command with escalated permissions and include a 1-sentence justification for approval. Do not ask the user to run it yet.
|
||||
3. Only if the escalated attempt fails, ask the user to run the install command.
|
||||
4. Ask the user to restart Codex.
|
||||
5. Re-run the doc search/fetch after restart.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Clarify the product scope and whether the request is general docs lookup, model selection, a GPT-5.4 upgrade, or a GPT-5.4 prompt upgrade.
|
||||
2. If it is a model-selection request, load `references/latest-model.md`.
|
||||
3. If it is an explicit GPT-5.4 upgrade request, load `references/upgrading-to-gpt-5p4.md`.
|
||||
4. If the upgrade may require prompt changes, or the workflow is research-heavy, tool-heavy, coding-oriented, multi-agent, or long-running, also load `references/gpt-5p4-prompting-guide.md`.
|
||||
5. Search docs with a precise query.
|
||||
6. Fetch the best page and the exact section needed (use `anchor` when possible).
|
||||
7. For GPT-5.4 upgrade reviews, always make the per-usage-site output explicit: target model, starting reasoning recommendation, `phase` assessment when relevant, prompt blocks, and compatibility status.
|
||||
8. Answer with concise guidance and cite the doc source, using the reference files only as helper context.
|
||||
|
||||
## Reference map
|
||||
|
||||
Read only what you need:
|
||||
|
||||
- `references/latest-model.md` -> model-selection and "best/latest/current model" questions; verify every recommendation against current OpenAI docs before answering.
|
||||
- `references/upgrading-to-gpt-5p4.md` -> only for explicit GPT-5.4 upgrade and upgrade-planning requests; verify the checklist and compatibility guidance against current OpenAI docs before answering.
|
||||
- `references/gpt-5p4-prompting-guide.md` -> prompt rewrites and prompt-behavior upgrades for GPT-5.4; verify prompting guidance against current OpenAI docs before answering.
|
||||
|
||||
## Quality rules
|
||||
|
||||
- Treat OpenAI docs as the source of truth; avoid speculation.
|
||||
- Keep quotes short and within policy limits; prefer paraphrase with citations.
|
||||
- If multiple pages differ, call out the difference and cite both.
|
||||
- Reference files are convenience guides only; for volatile guidance such as recommended models, upgrade instructions, or prompting advice, current OpenAI docs always win.
|
||||
- If docs do not cover the user’s need, say so and offer next steps.
|
||||
|
||||
## Tooling notes
|
||||
|
||||
- Always use MCP doc tools before any web search for OpenAI-related questions.
|
||||
- If the MCP server is installed but returns no meaningful results, then use web search as a fallback.
|
||||
- When falling back to web search, restrict to official OpenAI domains (developers.openai.com, platform.openai.com) and cite sources.
|
||||
@@ -1,14 +0,0 @@
|
||||
interface:
|
||||
display_name: "OpenAI Docs"
|
||||
short_description: "Reference official OpenAI docs, including upgrade guidance"
|
||||
icon_small: "./assets/openai-small.svg"
|
||||
icon_large: "./assets/openai.png"
|
||||
default_prompt: "Look up official OpenAI docs, load relevant GPT-5.4 upgrade references when applicable, and answer with concise, cited guidance."
|
||||
|
||||
dependencies:
|
||||
tools:
|
||||
- type: "mcp"
|
||||
value: "openaiDeveloperDocs"
|
||||
description: "OpenAI Developer Docs MCP server"
|
||||
transport: "streamable_http"
|
||||
url: "https://developers.openai.com/mcp"
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 14 14">
|
||||
<path d="M10.931 3.34a.112.112 0 0 0-.069-.104l-.038-.007c-1.537.05-2.45.318-3.714 1.002v6.683c.48-.248.936-.44 1.414-.58.695-.203 1.417-.292 2.303-.305l.038-.008a.113.113 0 0 0 .066-.104V3.341ZM2.363 9.919c0 .064.051.11.105.111l.33.008c1.162.046 2.042.243 2.975.662-.403-.585-1.008-1.075-1.654-1.292a.991.991 0 0 1-.674-.941v-5.14a6.36 6.36 0 0 0-.59-.076l-.37-.02a.115.115 0 0 0-.122.111v6.577Zm9.455-.001a.998.998 0 0 1-.877.992l-.101.007c-.832.012-1.47.095-2.066.27-.599.174-1.176.448-1.883.863a.444.444 0 0 1-.449 0c-1.299-.763-2.229-1.07-3.689-1.125l-.299-.008a.997.997 0 0 1-.977-.998V3.342c0-.573.478-1.017 1.038-.999l.417.023c.188.015.35.037.513.062v-.754c0-.708.749-1.244 1.429-.903.984.492 1.836 1.449 2.15 2.505 1.216-.617 2.222-.884 3.771-.934l.105.003a.998.998 0 0 1 .918.996v6.576ZM4.332 8.466c0 .049.03.087.07.1l.24.091a4.319 4.319 0 0 1 1.581 1.176V3.721c-.164-.803-.799-1.617-1.584-2.07l-.162-.088c-.025-.012-.054-.013-.088.009a.12.12 0 0 0-.057.102v6.792Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,433 +0,0 @@
|
||||
# GPT-5.4 prompting upgrade guide
|
||||
|
||||
Use this guide when prompts written for older models need to be adapted for GPT-5.4 during an upgrade. Start lean: keep the model-string change narrow, preserve the original task intent, and add only the smallest prompt changes needed to recover behavior.
|
||||
|
||||
## Default upgrade posture
|
||||
|
||||
- Start with `model string only` whenever the old prompt is already short, explicit, and task-bounded.
|
||||
- Move to `model string + light prompt rewrite` only when regressions appear in completeness, persistence, citation quality, verification, or verbosity.
|
||||
- Prefer one or two targeted prompt additions over a broad rewrite.
|
||||
- Treat reasoning effort as a last-mile knob. Start lower, then increase only after prompt-level fixes and evals.
|
||||
- Before increasing reasoning effort, first add a completeness contract, a verification loop, and tool persistence rules - depending on the usage case.
|
||||
- If the workflow clearly depends on implementation changes rather than prompt changes, treat it as blocked for prompt-only upgrade guidance.
|
||||
- Do not classify a case as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions, wiring, or other implementation details.
|
||||
|
||||
## Behavioral differences to account for
|
||||
|
||||
Current GPT-5.4 upgrade guidance suggests these strengths:
|
||||
|
||||
- stronger personality and tone adherence, with less drift over long answers
|
||||
- better long-horizon and agentic workflow stamina
|
||||
- stronger spreadsheet, finance, and formatting tasks
|
||||
- more efficient tool selection and fewer unnecessary calls by default
|
||||
- stronger structured generation and classification reliability
|
||||
|
||||
The main places where prompt guidance still helps are:
|
||||
|
||||
- retrieval-heavy workflows that need persistent tool use and explicit completeness
|
||||
- research and citation discipline
|
||||
- verification before irreversible or high-impact actions
|
||||
- terminal and tool workflow hygiene
|
||||
- defaults and implied follow-through
|
||||
- verbosity control for compact, information-dense answers
|
||||
|
||||
Start with the smallest set of instructions that preserves correctness. Add the prompt blocks below only for workflows that actually need them.
|
||||
|
||||
## Prompt rewrite patterns
|
||||
|
||||
| Older prompt pattern | GPT-5.4 adjustment | Why | Example addition |
|
||||
| --- | --- | --- | --- |
|
||||
| Long, repetitive instructions that compensate for weaker instruction following | Remove duplicate scaffolding and keep only the constraints that materially change behavior | GPT-5.4 usually needs less repeated steering | Replace repeated reminders with one concise rule plus a verification block |
|
||||
| Fast assistant prompt with no verbosity control | Keep the prompt as-is first; add a verbosity clamp only if outputs become too long | Many GPT-4o or GPT-4.1 upgrades work with just a model-string swap | Add `output_verbosity_spec` only after a verbosity regression |
|
||||
| Tool-heavy agent prompt that assumes the model will keep searching until complete | Add persistence and verification rules | GPT-5.4 may use fewer tool calls by default for efficiency | Add `tool_persistence_rules` and `verification_loop` |
|
||||
| Tool-heavy workflow where later actions depend on earlier lookup or retrieval | Add prerequisite and missing-context rules before action steps | GPT-5.4 benefits from explicit dependency-aware routing when context is still thin | Add `dependency_checks` and `missing_context_gating` |
|
||||
| Retrieval workflow with several independent lookups | Add selective parallelism guidance | GPT-5.4 is strong at parallel tool use, but should not parallelize dependent steps | Add `parallel_tool_calling` |
|
||||
| Batch workflow prompt that often misses items | Add an explicit completeness contract | Item accounting benefits from direct instruction | Add `completeness_contract` |
|
||||
| Research prompt that needs grounding and citation discipline | Add research, citation, and empty-result recovery blocks | Multi-pass retrieval is stronger when the model is told how to react to weak or empty search results | Add `research_mode`, `citation_rules`, and `empty_result_handling`; add `tool_persistence_rules` when retrieval tools are already in use |
|
||||
| Coding or terminal prompt with shell misuse or early stop failures | Keep the same tool surface and add terminal hygiene and verification instructions | Tool-using coding workflows are not blocked just because tools exist; they usually need better prompt steering, not host rewiring | Add `terminal_tool_hygiene` and `verification_loop`, optionally `tool_persistence_rules` |
|
||||
| Multi-agent or support-triage workflow with escalation or completeness requirements | Add one lightweight control block for persistence, completeness, or verification | GPT-5.4 can be more efficient by default, so multi-step support flows benefit from an explicit completion or verification contract | Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop` |
|
||||
|
||||
## Prompt blocks
|
||||
|
||||
Use these selectively. Do not add all of them by default.
|
||||
|
||||
### `output_verbosity_spec`
|
||||
|
||||
Use when:
|
||||
|
||||
- the upgraded model gets too wordy
|
||||
- the host needs compact, information-dense answers
|
||||
- the workflow benefits from a short overview plus a checklist
|
||||
|
||||
```text
|
||||
<output_verbosity_spec>
|
||||
- Default: 3-6 sentences or up to 6 bullets.
|
||||
- If the user asked for a doc or report, use headings with short bullets.
|
||||
- For multi-step tasks:
|
||||
- Start with 1 short overview paragraph.
|
||||
- Then provide a checklist with statuses: [done], [todo], or [blocked].
|
||||
- Avoid repeating the user's request.
|
||||
- Prefer compact, information-dense writing.
|
||||
</output_verbosity_spec>
|
||||
```
|
||||
|
||||
### `default_follow_through_policy`
|
||||
|
||||
Use when:
|
||||
|
||||
- the host expects the model to proceed on reversible, low-risk steps
|
||||
- the upgraded model becomes too conservative or asks for confirmation too often
|
||||
|
||||
```text
|
||||
<default_follow_through_policy>
|
||||
- If the user's intent is clear and the next step is reversible and low-risk, proceed without asking permission.
|
||||
- Only ask permission if the next step is:
|
||||
(a) irreversible,
|
||||
(b) has external side effects, or
|
||||
(c) requires missing sensitive information or a choice that materially changes outcomes.
|
||||
- If proceeding, state what you did and what remains optional.
|
||||
</default_follow_through_policy>
|
||||
```
|
||||
|
||||
### `instruction_priority`
|
||||
|
||||
Use when:
|
||||
|
||||
- users often change task shape, format, or tone mid-conversation
|
||||
- the host needs an explicit override policy instead of relying on defaults
|
||||
|
||||
```text
|
||||
<instruction_priority>
|
||||
- User instructions override default style, tone, formatting, and initiative preferences.
|
||||
- Safety, honesty, privacy, and permission constraints do not yield.
|
||||
- If a newer user instruction conflicts with an earlier one, follow the newer instruction.
|
||||
- Preserve earlier instructions that do not conflict.
|
||||
</instruction_priority>
|
||||
```
|
||||
|
||||
### `tool_persistence_rules`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow needs multiple retrieval or verification steps
|
||||
- the model starts stopping too early because it is trying to save tool calls
|
||||
|
||||
```text
|
||||
<tool_persistence_rules>
|
||||
- Use tools whenever they materially improve correctness, completeness, or grounding.
|
||||
- Do not stop early just to save tool calls.
|
||||
- Keep calling tools until:
|
||||
(1) the task is complete, and
|
||||
(2) verification passes.
|
||||
- If a tool returns empty or partial results, retry with a different strategy.
|
||||
</tool_persistence_rules>
|
||||
```
|
||||
|
||||
### `dig_deeper_nudge`
|
||||
|
||||
Use when:
|
||||
|
||||
- the model is too literal or stops at the first plausible answer
|
||||
- the task is safety- or accuracy-sensitive and needs a small initiative nudge before raising reasoning effort
|
||||
|
||||
```text
|
||||
<dig_deeper_nudge>
|
||||
- Do not stop at the first plausible answer.
|
||||
- Look for second-order issues, edge cases, and missing constraints.
|
||||
- If the task is safety- or accuracy-critical, perform at least one verification step.
|
||||
</dig_deeper_nudge>
|
||||
```
|
||||
|
||||
### `dependency_checks`
|
||||
|
||||
Use when:
|
||||
|
||||
- later actions depend on prerequisite lookup, memory retrieval, or discovery steps
|
||||
- the model may be tempted to skip prerequisite work because the intended end state seems obvious
|
||||
|
||||
```text
|
||||
<dependency_checks>
|
||||
- Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required.
|
||||
- Do not skip prerequisite steps just because the intended final action seems obvious.
|
||||
- If a later step depends on the output of an earlier one, resolve that dependency first.
|
||||
</dependency_checks>
|
||||
```
|
||||
|
||||
### `parallel_tool_calling`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow has multiple independent retrieval steps
|
||||
- wall-clock time matters but some steps still need sequencing
|
||||
|
||||
```text
|
||||
<parallel_tool_calling>
|
||||
- When multiple retrieval or lookup steps are independent, prefer parallel tool calls to reduce wall-clock time.
|
||||
- Do not parallelize steps with prerequisite dependencies or where one result determines the next action.
|
||||
- After parallel retrieval, pause to synthesize before making more calls.
|
||||
- Prefer selective parallelism: parallelize independent evidence gathering, not speculative or redundant tool use.
|
||||
</parallel_tool_calling>
|
||||
```
|
||||
|
||||
### `completeness_contract`
|
||||
|
||||
Use when:
|
||||
|
||||
- the task involves batches, lists, enumerations, or multiple deliverables
|
||||
- missing items are a common failure mode
|
||||
|
||||
```text
|
||||
<completeness_contract>
|
||||
- Deliver all requested items.
|
||||
- Maintain an itemized checklist of deliverables.
|
||||
- For lists or batches:
|
||||
- state the expected count,
|
||||
- enumerate items 1..N,
|
||||
- confirm that none are missing before finalizing.
|
||||
- If any item is blocked by missing data, mark it [blocked] and state exactly what is missing.
|
||||
</completeness_contract>
|
||||
```
|
||||
|
||||
### `empty_result_handling`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow frequently performs search, CRM, logs, or retrieval steps
|
||||
- no-results failures are often false negatives
|
||||
|
||||
```text
|
||||
<empty_result_handling>
|
||||
If a lookup returns empty or suspiciously small results:
|
||||
- Do not conclude that no results exist immediately.
|
||||
- Try at least 2 fallback strategies, such as a broader query, alternate filters, or another source.
|
||||
- Only then report that no results were found, along with what you tried.
|
||||
</empty_result_handling>
|
||||
```
|
||||
|
||||
### `verification_loop`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow has downstream impact
|
||||
- accuracy, formatting, or completeness regressions matter
|
||||
|
||||
```text
|
||||
<verification_loop>
|
||||
Before finalizing:
|
||||
- Check correctness: does the output satisfy every requirement?
|
||||
- Check grounding: are factual claims backed by retrieved sources or tool output?
|
||||
- Check formatting: does the output match the requested schema or style?
|
||||
- Check safety and irreversibility: if the next step has external side effects, ask permission first.
|
||||
</verification_loop>
|
||||
```
|
||||
|
||||
### `missing_context_gating`
|
||||
|
||||
Use when:
|
||||
|
||||
- required context is sometimes missing early in the workflow
|
||||
- the model should prefer retrieval over guessing
|
||||
|
||||
```text
|
||||
<missing_context_gating>
|
||||
- If required context is missing, do not guess.
|
||||
- Prefer the appropriate lookup tool when the context is retrievable; ask a minimal clarifying question only when it is not.
|
||||
- If you must proceed, label assumptions explicitly and choose a reversible action.
|
||||
</missing_context_gating>
|
||||
```
|
||||
|
||||
### `action_safety`
|
||||
|
||||
Use when:
|
||||
|
||||
- the agent will actively take actions through tools
|
||||
- the host benefits from a short pre-flight and post-flight execution frame
|
||||
|
||||
```text
|
||||
<action_safety>
|
||||
- Pre-flight: summarize the intended action and parameters in 1-2 lines.
|
||||
- Execute via tool.
|
||||
- Post-flight: confirm the outcome and any validation that was performed.
|
||||
</action_safety>
|
||||
```
|
||||
|
||||
### `citation_rules`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow produces cited answers
|
||||
- fabricated citations or wrong citation formats are costly
|
||||
|
||||
```text
|
||||
<citation_rules>
|
||||
- Only cite sources that were actually retrieved in this session.
|
||||
- Never fabricate citations, URLs, IDs, or quote spans.
|
||||
- If you cannot find a source for a claim, say so and either:
|
||||
- soften the claim, or
|
||||
- explain how to verify it with tools.
|
||||
- Use exactly the citation format required by the host application.
|
||||
</citation_rules>
|
||||
```
|
||||
|
||||
### `research_mode`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow is research-heavy
|
||||
- the host uses web search or retrieval tools
|
||||
|
||||
```text
|
||||
<research_mode>
|
||||
- Do research in 3 passes:
|
||||
1) Plan: list 3-6 sub-questions to answer.
|
||||
2) Retrieve: search each sub-question and follow 1-2 second-order leads.
|
||||
3) Synthesize: resolve contradictions and write the final answer with citations.
|
||||
- Stop only when more searching is unlikely to change the conclusion.
|
||||
</research_mode>
|
||||
```
|
||||
|
||||
If your host environment uses a specific research tool or requires a submit step, combine this with the host's finalization contract.
|
||||
|
||||
### `structured_output_contract`
|
||||
|
||||
Use when:
|
||||
|
||||
- the host depends on strict JSON, SQL, or other structured output
|
||||
|
||||
```text
|
||||
<structured_output_contract>
|
||||
- Output only the requested format.
|
||||
- Do not add prose or markdown fences unless they were requested.
|
||||
- Validate that parentheses and brackets are balanced.
|
||||
- Do not invent tables or fields.
|
||||
- If required schema information is missing, ask for it or return an explicit error object.
|
||||
</structured_output_contract>
|
||||
```
|
||||
|
||||
### `bbox_extraction_spec`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow extracts OCR boxes, document regions, or other coordinates
|
||||
- layout drift or missed dense regions are common failure modes
|
||||
|
||||
```text
|
||||
<bbox_extraction_spec>
|
||||
- Use the specified coordinate format exactly, such as [x1,y1,x2,y2] normalized to 0..1.
|
||||
- For each box, include page, label, text snippet, and confidence.
|
||||
- Add a vertical-drift sanity check so boxes stay aligned with the correct line of text.
|
||||
- If the layout is dense, process page by page and do a second pass for missed items.
|
||||
</bbox_extraction_spec>
|
||||
```
|
||||
|
||||
### `terminal_tool_hygiene`
|
||||
|
||||
Use when:
|
||||
|
||||
- the prompt belongs to a terminal-based or coding-agent workflow
|
||||
- tool misuse or shell misuse has been observed
|
||||
|
||||
```text
|
||||
<terminal_tool_hygiene>
|
||||
- Only run shell commands through the terminal tool.
|
||||
- Never try to "run" tool names as shell commands.
|
||||
- If a patch or edit tool exists, use it directly instead of emulating it in bash.
|
||||
- After changes, run a lightweight verification step such as ls, tests, or a build before declaring the task done.
|
||||
</terminal_tool_hygiene>
|
||||
```
|
||||
|
||||
### `user_updates_spec`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow is long-running and user updates matter
|
||||
|
||||
```text
|
||||
<user_updates_spec>
|
||||
- Only update the user when starting a new major phase or when the plan changes.
|
||||
- Each update should contain:
|
||||
- 1 sentence on what changed,
|
||||
- 1 sentence on the next step.
|
||||
- Do not narrate routine tool calls.
|
||||
- Keep the user-facing update short, even when the actual work is exhaustive.
|
||||
</user_updates_spec>
|
||||
```
|
||||
|
||||
If you are using [Compaction](https://developers.openai.com/api/docs/guides/compaction) in the Responses API, compact after major milestones, treat compacted items as opaque state, and keep prompts functionally identical after compaction.
|
||||
|
||||
## Responses `phase` guidance
|
||||
|
||||
For long-running Responses workflows, preambles, or tool-heavy agents that replay assistant items, review whether `phase` is already preserved.
|
||||
|
||||
- If the host already round-trips `phase`, keep it intact during the upgrade.
|
||||
- If the host uses `previous_response_id` and does not manually replay assistant items, note that this may reduce manual `phase` handling needs.
|
||||
- If reliable GPT-5.4 behavior would require adding or preserving `phase` and that would need code edits, treat the case as blocked for prompt-only or model-string-only migration guidance.
|
||||
|
||||
## Example upgrade profiles
|
||||
|
||||
### GPT-5.2
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Match the current reasoning effort first
|
||||
- Preserve the existing latency and quality profile before tuning prompt blocks
|
||||
- If the repo does not expose the exact setting, emit `same` as the starting recommendation
|
||||
|
||||
### GPT-5.3-Codex
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Match the current reasoning effort first
|
||||
- If you need Codex-style speed and efficiency, add verification blocks before increasing reasoning effort
|
||||
- If the repo does not expose the exact setting, emit `same` as the starting recommendation
|
||||
|
||||
### GPT-4o or GPT-4.1 assistant
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Start with `none` reasoning effort
|
||||
- Add `output_verbosity_spec` only if output becomes too verbose
|
||||
|
||||
### Long-horizon agent
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Start with `medium` reasoning effort
|
||||
- Add `tool_persistence_rules`
|
||||
- Add `completeness_contract`
|
||||
- Add `verification_loop`
|
||||
|
||||
### Research workflow
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Start with `medium` reasoning effort
|
||||
- Add `research_mode`
|
||||
- Add `citation_rules`
|
||||
- Add `empty_result_handling`
|
||||
- Add `tool_persistence_rules` when the host already uses web or retrieval tools
|
||||
- Add `parallel_tool_calling` when the retrieval steps are independent
|
||||
|
||||
### Support triage or multi-agent workflow
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Prefer `model string + light prompt rewrite` over `model string only`
|
||||
- Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`
|
||||
- Add more only if evals show a real regression
|
||||
|
||||
### Coding or terminal workflow
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Keep the model-string change narrow
|
||||
- Match the current reasoning effort first if you are upgrading from GPT-5.3-Codex
|
||||
- Add `terminal_tool_hygiene`
|
||||
- Add `verification_loop`
|
||||
- Add `dependency_checks` when actions depend on prerequisite lookup or discovery
|
||||
- Add `tool_persistence_rules` if the agent stops too early
|
||||
- Review whether `phase` is already preserved for long-running Responses flows or assistant preambles
|
||||
- Do not classify this as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions or wiring
|
||||
- If the repo already uses Responses plus tools and no required host-side change is shown, prefer `model_string_plus_light_prompt_rewrite` over `blocked`
|
||||
|
||||
## Prompt regression checklist
|
||||
|
||||
- Check whether the upgraded prompt still preserves the original task intent.
|
||||
- Check whether the new prompt is leaner, not just longer.
|
||||
- Check completeness, citation quality, dependency handling, verification behavior, and verbosity.
|
||||
- For long-running Responses agents, check whether `phase` handling is already in place or needs implementation work.
|
||||
- Confirm that each added prompt block addresses an observed regression.
|
||||
- Remove prompt blocks that are not earning their keep.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Latest model guide
|
||||
|
||||
This file is a curated helper. Every recommendation here must be verified against current OpenAI docs before it is repeated to a user.
|
||||
|
||||
## Current model map
|
||||
|
||||
| Model ID | Use for |
|
||||
| --- | --- |
|
||||
| `gpt-5.4` | Default text plus reasoning for most new apps |
|
||||
| `gpt-5.4-pro` | Only when the user explicitly asks for maximum reasoning or quality; substantially slower and more expensive |
|
||||
| `gpt-5-mini` | Cheaper and faster reasoning with good quality |
|
||||
| `gpt-5-nano` | High-throughput simple tasks and classification |
|
||||
| `gpt-5.4` | Explicit no-reasoning text path via `reasoning.effort: none` |
|
||||
| `gpt-4.1-mini` | Cheaper no-reasoning text |
|
||||
| `gpt-4.1-nano` | Fastest and cheapest no-reasoning text |
|
||||
| `gpt-5.3-codex` | Agentic coding, code editing, and tool-heavy coding workflows |
|
||||
| `gpt-5.1-codex-mini` | Cheaper coding workflows |
|
||||
| `gpt-image-1.5` | Best image generation and edit quality |
|
||||
| `gpt-image-1-mini` | Cost-optimized image generation |
|
||||
| `gpt-4o-mini-tts` | Text-to-speech |
|
||||
| `gpt-4o-mini-transcribe` | Speech-to-text, fast and cost-efficient |
|
||||
| `gpt-realtime-1.5` | Realtime voice and multimodal sessions |
|
||||
| `gpt-realtime-mini` | Cheaper realtime sessions |
|
||||
| `gpt-audio` | Chat Completions audio input and output |
|
||||
| `gpt-audio-mini` | Cheaper Chat Completions audio workflows |
|
||||
| `sora-2` | Faster iteration and draft video generation |
|
||||
| `sora-2-pro` | Higher-quality production video |
|
||||
| `omni-moderation-latest` | Text and image moderation |
|
||||
| `text-embedding-3-large` | Higher-quality retrieval embeddings; default in this skill because no best-specific row exists |
|
||||
| `text-embedding-3-small` | Lower-cost embeddings |
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- This file will drift unless it is periodically re-verified against current OpenAI docs.
|
||||
- If this file conflicts with current docs, the docs win.
|
||||
@@ -1,164 +0,0 @@
|
||||
# Upgrading to GPT-5.4
|
||||
|
||||
Use this guide when the user explicitly asks to upgrade an existing integration to GPT-5.4. Pair it with current OpenAI docs lookups. The default target string is `gpt-5.4`.
|
||||
|
||||
## Upgrade posture
|
||||
|
||||
Upgrade with the narrowest safe change set:
|
||||
|
||||
- replace the model string first
|
||||
- update only the prompts that are directly tied to that model usage
|
||||
- prefer prompt-only upgrades when possible
|
||||
- if the upgrade would require API-surface changes, parameter rewrites, tool rewiring, or broader code edits, mark it as blocked instead of stretching the scope
|
||||
|
||||
## Upgrade workflow
|
||||
|
||||
1. Inventory current model usage.
|
||||
- Search for model strings, client calls, and prompt-bearing files.
|
||||
- Include inline prompts, prompt templates, YAML or JSON configs, Markdown docs, and saved prompts when they are clearly tied to a model usage site.
|
||||
2. Pair each model usage with its prompt surface.
|
||||
- Prefer the closest prompt surface first: inline system or developer text, then adjacent prompt files, then shared templates.
|
||||
- If you cannot confidently tie a prompt to the model usage, say so instead of guessing.
|
||||
3. Classify the source model family.
|
||||
- Common buckets: `gpt-4o` or `gpt-4.1`, `o1` or `o3` or `o4-mini`, early `gpt-5`, later `gpt-5.x`, or mixed and unclear.
|
||||
4. Decide the upgrade class.
|
||||
- `model string only`
|
||||
- `model string + light prompt rewrite`
|
||||
- `blocked without code changes`
|
||||
5. Run the no-code compatibility gate.
|
||||
- Check whether the current integration can accept `gpt-5.4` without API-surface changes or implementation changes.
|
||||
- For long-running Responses or tool-heavy agents, check whether `phase` is already preserved or round-tripped when the host replays assistant items or uses preambles.
|
||||
- If compatibility depends on code changes, return `blocked`.
|
||||
- If compatibility is unclear, return `unknown` rather than improvising.
|
||||
6. Recommend the upgrade.
|
||||
- Default replacement string: `gpt-5.4`
|
||||
- Keep the intervention small and behavior-preserving.
|
||||
7. Deliver a structured recommendation.
|
||||
- `Current model usage`
|
||||
- `Recommended model-string updates`
|
||||
- `Starting reasoning recommendation`
|
||||
- `Prompt updates`
|
||||
- `Phase assessment` when the flow is long-running, replayed, or tool-heavy
|
||||
- `No-code compatibility check`
|
||||
- `Validation plan`
|
||||
- `Launch-day refresh items`
|
||||
|
||||
Output rule:
|
||||
|
||||
- Always emit a starting `reasoning_effort_recommendation` for each usage site.
|
||||
- If the repo exposes the current reasoning setting, preserve it first unless the source guide says otherwise.
|
||||
- If the repo does not expose the current setting, use the source-family starting mapping instead of returning `null`.
|
||||
|
||||
## Upgrade outcomes
|
||||
|
||||
### `model string only`
|
||||
|
||||
Choose this when:
|
||||
|
||||
- the existing prompts are already short, explicit, and task-bounded
|
||||
- the workflow is not strongly research-heavy, tool-heavy, multi-agent, batch or completeness-sensitive, or long-horizon
|
||||
- there are no obvious compatibility blockers
|
||||
|
||||
Default action:
|
||||
|
||||
- replace the model string with `gpt-5.4`
|
||||
- keep prompts unchanged
|
||||
- validate behavior with existing evals or spot checks
|
||||
|
||||
### `model string + light prompt rewrite`
|
||||
|
||||
Choose this when:
|
||||
|
||||
- the old prompt was compensating for weaker instruction following
|
||||
- the workflow needs more persistence than the default tool-use behavior will likely provide
|
||||
- the task needs stronger completeness, citation discipline, or verification
|
||||
- the upgraded model becomes too verbose or under-complete unless instructed otherwise
|
||||
- the workflow is research-heavy and needs stronger handling of sparse or empty retrieval results
|
||||
- the workflow is coding-oriented, tool-heavy, or multi-agent, but the existing API surface and tool definitions can remain unchanged
|
||||
|
||||
Default action:
|
||||
|
||||
- replace the model string with `gpt-5.4`
|
||||
- add one or two targeted prompt blocks
|
||||
- read `references/gpt-5p4-prompting-guide.md` to choose the smallest prompt changes that recover the old behavior
|
||||
- avoid broad prompt cleanup unrelated to the upgrade
|
||||
- for research workflows, default to `research_mode` + `citation_rules` + `empty_result_handling`; add `tool_persistence_rules` when the host already uses retrieval tools
|
||||
- for dependency-aware or tool-heavy workflows, default to `tool_persistence_rules` + `dependency_checks` + `verification_loop`; add `parallel_tool_calling` only when retrieval steps are truly independent
|
||||
- for coding or terminal workflows, default to `terminal_tool_hygiene` + `verification_loop`
|
||||
- for multi-agent support or triage workflows, default to at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`
|
||||
- for long-running Responses agents with preambles or multiple assistant messages, explicitly review whether `phase` is already handled; if adding or preserving `phase` would require code edits, mark the path as `blocked`
|
||||
- do not classify a coding or tool-using Responses workflow as `blocked` just because the visible snippet is minimal; prefer `model string + light prompt rewrite` unless the repo clearly shows that a safe GPT-5.4 path would require host-side code changes
|
||||
|
||||
### `blocked`
|
||||
|
||||
Choose this when:
|
||||
|
||||
- the upgrade appears to require API-surface changes
|
||||
- the upgrade appears to require parameter rewrites or reasoning-setting changes that are not exposed outside implementation code
|
||||
- the upgrade would require changing tool definitions, tool handler wiring, or schema contracts
|
||||
- you cannot confidently identify the prompt surface tied to the model usage
|
||||
|
||||
Default action:
|
||||
|
||||
- do not improvise a broader upgrade
|
||||
- report the blocker and explain that the fix is out of scope for this guide
|
||||
|
||||
## No-code compatibility checklist
|
||||
|
||||
Before recommending a no-code upgrade, check:
|
||||
|
||||
1. Can the current host accept the `gpt-5.4` model string without changing client code or API surface?
|
||||
2. Are the related prompts identifiable and editable?
|
||||
3. Does the host depend on behavior that likely needs API-surface changes, parameter rewrites, or tool rewiring?
|
||||
4. Would the likely fix be prompt-only, or would it need implementation changes?
|
||||
5. Is the prompt surface close enough to the model usage that you can make a targeted change instead of a broad cleanup?
|
||||
6. For long-running Responses or tool-heavy agents, is `phase` already preserved if the host relies on preambles, replayed assistant items, or multiple assistant messages?
|
||||
|
||||
If item 1 is no, items 3 through 4 point to implementation work, or item 6 is no and the fix needs code changes, return `blocked`.
|
||||
|
||||
If item 2 is no, return `unknown` unless the user can point to the prompt location.
|
||||
|
||||
Important:
|
||||
|
||||
- Existing use of tools, agents, or multiple usage sites is not by itself a blocker.
|
||||
- If the current host can keep the same API surface and the same tool definitions, prefer `model string + light prompt rewrite` over `blocked`.
|
||||
- Reserve `blocked` for cases that truly require implementation changes, not cases that only need stronger prompt steering.
|
||||
|
||||
## Scope boundaries
|
||||
|
||||
This guide may:
|
||||
|
||||
- update or recommend updated model strings
|
||||
- update or recommend updated prompts
|
||||
- inspect code and prompt files to understand where those changes belong
|
||||
- inspect whether existing Responses flows already preserve `phase`
|
||||
- flag compatibility blockers
|
||||
|
||||
This guide may not:
|
||||
|
||||
- move Chat Completions code to Responses
|
||||
- move Responses code to another API surface
|
||||
- rewrite parameter shapes
|
||||
- change tool definitions or tool-call handling
|
||||
- change structured-output wiring
|
||||
- add or retrofit `phase` handling in implementation code
|
||||
- edit business logic, orchestration logic, or SDK usage beyond a literal model-string replacement
|
||||
|
||||
If a safe GPT-5.4 upgrade requires any of those changes, mark the path as blocked and out of scope.
|
||||
|
||||
## Validation plan
|
||||
|
||||
- Validate each upgraded usage site with existing evals or realistic spot checks.
|
||||
- Check whether the upgraded model still matches expected latency, output shape, and quality.
|
||||
- If prompt edits were added, confirm each block is doing real work instead of adding noise.
|
||||
- If the workflow has downstream impact, add a lightweight verification pass before finalization.
|
||||
|
||||
## Launch-day refresh items
|
||||
|
||||
When final GPT-5.4 guidance changes:
|
||||
|
||||
1. Replace release-candidate assumptions with final GPT-5.4 guidance where appropriate.
|
||||
2. Re-check whether the default target string should stay `gpt-5.4` for all source families.
|
||||
3. Re-check any prompt-block recommendations whose semantics may have changed.
|
||||
4. Re-check research, citation, and compatibility guidance against the final model behavior.
|
||||
5. Re-run the same upgrade scenarios and confirm the blocked-versus-viable boundaries still hold.
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
name: plugin-creator
|
||||
description: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, and baseline placeholders you can edit before publishing or testing. Use when Codex needs to create a new local plugin, add optional plugin structure, or generate or update repo-root `.agents/plugins/marketplace.json` entries for plugin ordering and availability metadata.
|
||||
---
|
||||
|
||||
# Plugin Creator
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Run the scaffold script:
|
||||
|
||||
```bash
|
||||
# Plugin names are normalized to lower-case hyphen-case and must be <= 64 chars.
|
||||
# The generated folder and plugin.json name are always the same.
|
||||
# Run from repo root (or replace .agents/... with the absolute path to this SKILL).
|
||||
# By default creates in <repo_root>/plugins/<plugin-name>.
|
||||
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py <plugin-name>
|
||||
```
|
||||
|
||||
2. Open `<plugin-path>/.codex-plugin/plugin.json` and replace `[TODO: ...]` placeholders.
|
||||
|
||||
3. Generate or update the repo marketplace entry when the plugin should appear in Codex UI ordering:
|
||||
|
||||
```bash
|
||||
# marketplace.json always lives at <repo-root>/.agents/plugins/marketplace.json
|
||||
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --with-marketplace
|
||||
```
|
||||
|
||||
For a home-local plugin, treat `<home>` as the root and use:
|
||||
|
||||
```bash
|
||||
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin \
|
||||
--path ~/plugins \
|
||||
--marketplace-path ~/.agents/plugins/marketplace.json \
|
||||
--with-marketplace
|
||||
```
|
||||
|
||||
4. Generate/adjust optional companion folders as needed:
|
||||
|
||||
```bash
|
||||
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --path <parent-plugin-directory> \
|
||||
--with-skills --with-hooks --with-scripts --with-assets --with-mcp --with-apps --with-marketplace
|
||||
```
|
||||
|
||||
`<parent-plugin-directory>` is the directory where the plugin folder `<plugin-name>` will be created (for example `~/code/plugins`).
|
||||
|
||||
## What this skill creates
|
||||
|
||||
- If the user has not made the plugin location explicit, ask whether they want a repo-local plugin or a home-local plugin before generating marketplace entries.
|
||||
- Creates plugin root at `/<parent-plugin-directory>/<plugin-name>/`.
|
||||
- Always creates `/<parent-plugin-directory>/<plugin-name>/.codex-plugin/plugin.json`.
|
||||
- Fills the manifest with the full schema shape, placeholder values, and the complete `interface` section.
|
||||
- Creates or updates `<repo-root>/.agents/plugins/marketplace.json` when `--with-marketplace` is set.
|
||||
- If the marketplace file does not exist yet, seed top-level `name` plus `interface.displayName` placeholders before adding the first plugin entry.
|
||||
- `<plugin-name>` is normalized using skill-creator naming rules:
|
||||
- `My Plugin` → `my-plugin`
|
||||
- `My--Plugin` → `my-plugin`
|
||||
- underscores, spaces, and punctuation are converted to `-`
|
||||
- result is lower-case hyphen-delimited with consecutive hyphens collapsed
|
||||
- Supports optional creation of:
|
||||
- `skills/`
|
||||
- `hooks/`
|
||||
- `scripts/`
|
||||
- `assets/`
|
||||
- `.mcp.json`
|
||||
- `.app.json`
|
||||
|
||||
## Marketplace workflow
|
||||
|
||||
- `marketplace.json` always lives at `<repo-root>/.agents/plugins/marketplace.json`.
|
||||
- For a home-local plugin, use the same convention with `<home>` as the root:
|
||||
`~/.agents/plugins/marketplace.json` plus `./plugins/<plugin-name>`.
|
||||
- Marketplace root metadata supports top-level `name` plus optional `interface.displayName`.
|
||||
- Treat plugin order in `plugins[]` as render order in Codex. Append new entries unless a user explicitly asks to reorder the list.
|
||||
- `displayName` belongs inside the marketplace `interface` object, not individual `plugins[]` entries.
|
||||
- Each generated marketplace entry must include all of:
|
||||
- `policy.installation`
|
||||
- `policy.authentication`
|
||||
- `category`
|
||||
- Default new entries to:
|
||||
- `policy.installation: "AVAILABLE"`
|
||||
- `policy.authentication: "ON_INSTALL"`
|
||||
- Override defaults only when the user explicitly specifies another allowed value.
|
||||
- Allowed `policy.installation` values:
|
||||
- `NOT_AVAILABLE`
|
||||
- `AVAILABLE`
|
||||
- `INSTALLED_BY_DEFAULT`
|
||||
- Allowed `policy.authentication` values:
|
||||
- `ON_INSTALL`
|
||||
- `ON_USE`
|
||||
- Treat `policy.products` as an override. Omit it unless the user explicitly requests product gating.
|
||||
- The generated plugin entry shape is:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "plugin-name",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/plugin-name"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
```
|
||||
|
||||
- Use `--force` only when intentionally replacing an existing marketplace entry for the same plugin name.
|
||||
- If `<repo-root>/.agents/plugins/marketplace.json` does not exist yet, create it with top-level `"name"`, an `"interface"` object containing `"displayName"`, and a `plugins` array, then add the new entry.
|
||||
|
||||
- For a brand-new marketplace file, the root object should look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "[TODO: marketplace-name]",
|
||||
"interface": {
|
||||
"displayName": "[TODO: Marketplace Display Name]"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "plugin-name",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/plugin-name"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Required behavior
|
||||
|
||||
- Outer folder name and `plugin.json` `"name"` are always the same normalized plugin name.
|
||||
- Do not remove required structure; keep `.codex-plugin/plugin.json` present.
|
||||
- Keep manifest values as placeholders until a human or follow-up step explicitly fills them.
|
||||
- If creating files inside an existing plugin path, use `--force` only when overwrite is intentional.
|
||||
- Preserve any existing marketplace `interface.displayName`.
|
||||
- When generating marketplace entries, always write `policy.installation`, `policy.authentication`, and `category` even if their values are defaults.
|
||||
- Add `policy.products` only when the user explicitly asks for that override.
|
||||
- Keep marketplace `source.path` relative to repo root as `./plugins/<plugin-name>`.
|
||||
|
||||
## Reference to exact spec sample
|
||||
|
||||
For the exact canonical sample JSON for both plugin manifests and marketplace entries, use:
|
||||
|
||||
- `references/plugin-json-spec.md`
|
||||
|
||||
## Validation
|
||||
|
||||
After editing `SKILL.md`, run:
|
||||
|
||||
```bash
|
||||
python3 <path-to-skill-creator>/scripts/quick_validate.py .agents/skills/plugin-creator
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
interface:
|
||||
display_name: "Plugin Creator"
|
||||
short_description: "Scaffold plugins and marketplace entries"
|
||||
default_prompt: "Use $plugin-creator to scaffold a plugin with placeholder plugin.json, optional structure, and a marketplace.json entry."
|
||||
icon_small: "./assets/plugin-creator-small.svg"
|
||||
icon_large: "./assets/plugin-creator.png"
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill="#0D0D0D" d="M12.03 4.113a3.612 3.612 0 0 1 5.108 5.108l-6.292 6.29c-.324.324-.56.561-.791.752l-.235.176c-.205.14-.422.261-.65.36l-.229.093a4.136 4.136 0 0 1-.586.16l-.764.134-2.394.4c-.142.024-.294.05-.423.06-.098.007-.232.01-.378-.026l-.149-.05a1.081 1.081 0 0 1-.521-.474l-.046-.093a1.104 1.104 0 0 1-.075-.527c.01-.129.035-.28.06-.422l.398-2.394c.1-.602.162-.987.295-1.35l.093-.23c.1-.228.22-.445.36-.65l.176-.235c.19-.232.428-.467.751-.79l6.292-6.292Zm-5.35 7.232c-.35.35-.534.535-.66.688l-.11.147a2.67 2.67 0 0 0-.24.433l-.062.154c-.08.22-.124.462-.232 1.112l-.398 2.394-.001.001h.003l2.393-.399.717-.126a2.63 2.63 0 0 0 .394-.105l.154-.063a2.65 2.65 0 0 0 .433-.24l.147-.11c.153-.126.339-.31.688-.66l4.988-4.988-3.227-3.226-4.987 4.988Zm9.517-6.291a2.281 2.281 0 0 0-3.225 0l-.364.362 3.226 3.227.363-.364c.89-.89.89-2.334 0-3.225ZM4.583 1.783a.3.3 0 0 1 .294.241c.117.585.347 1.092.707 1.48.357.385.859.668 1.549.783a.3.3 0 0 1 0 .592c-.69.115-1.192.398-1.549.783-.315.34-.53.77-.657 1.265l-.05.215a.3.3 0 0 1-.588 0c-.117-.585-.347-1.092-.707-1.48-.357-.384-.859-.668-1.549-.783a.3.3 0 0 1 0-.592c.69-.115 1.192-.398 1.549-.783.36-.388.59-.895.707-1.48l.015-.05a.3.3 0 0 1 .279-.19Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,170 +0,0 @@
|
||||
# Plugin JSON sample spec
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "plugin-name",
|
||||
"version": "1.2.0",
|
||||
"description": "Brief plugin description",
|
||||
"author": {
|
||||
"name": "Author Name",
|
||||
"email": "author@example.com",
|
||||
"url": "https://github.com/author"
|
||||
},
|
||||
"homepage": "https://docs.example.com/plugin",
|
||||
"repository": "https://github.com/author/plugin",
|
||||
"license": "MIT",
|
||||
"keywords": ["keyword1", "keyword2"],
|
||||
"skills": "./skills/",
|
||||
"hooks": "./hooks.json",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"apps": "./.app.json",
|
||||
"interface": {
|
||||
"displayName": "Plugin Display Name",
|
||||
"shortDescription": "Short description for subtitle",
|
||||
"longDescription": "Long description for details page",
|
||||
"developerName": "OpenAI",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Interactive", "Write"],
|
||||
"websiteURL": "https://openai.com/",
|
||||
"privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",
|
||||
"termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",
|
||||
"defaultPrompt": [
|
||||
"Summarize my inbox and draft replies for me.",
|
||||
"Find open bugs and turn them into Linear tickets.",
|
||||
"Review today's meetings and flag scheduling gaps."
|
||||
],
|
||||
"brandColor": "#3B82F6",
|
||||
"composerIcon": "./assets/icon.png",
|
||||
"logo": "./assets/logo.png",
|
||||
"screenshots": [
|
||||
"./assets/screenshot1.png",
|
||||
"./assets/screenshot2.png",
|
||||
"./assets/screenshot3.png"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Field guide
|
||||
|
||||
### Top-level fields
|
||||
|
||||
- `name` (`string`): Plugin identifier (kebab-case, no spaces). Required if `plugin.json` is provided and used as manifest name and component namespace.
|
||||
- `version` (`string`): Plugin semantic version.
|
||||
- `description` (`string`): Short purpose summary.
|
||||
- `author` (`object`): Publisher identity.
|
||||
- `name` (`string`): Author or team name.
|
||||
- `email` (`string`): Contact email.
|
||||
- `url` (`string`): Author/team homepage or profile URL.
|
||||
- `homepage` (`string`): Documentation URL for plugin usage.
|
||||
- `repository` (`string`): Source code URL.
|
||||
- `license` (`string`): License identifier (for example `MIT`, `Apache-2.0`).
|
||||
- `keywords` (`array` of `string`): Search/discovery tags.
|
||||
- `skills` (`string`): Relative path to skill directories/files.
|
||||
- `hooks` (`string`): Hook config path.
|
||||
- `mcpServers` (`string`): MCP config path.
|
||||
- `apps` (`string`): App manifest path for plugin integrations.
|
||||
- `interface` (`object`): Interface/UX metadata block for plugin presentation.
|
||||
|
||||
### `interface` fields
|
||||
|
||||
- `displayName` (`string`): User-facing title shown for the plugin.
|
||||
- `shortDescription` (`string`): Brief subtitle used in compact views.
|
||||
- `longDescription` (`string`): Longer description used on details screens.
|
||||
- `developerName` (`string`): Human-readable publisher name.
|
||||
- `category` (`string`): Plugin category bucket.
|
||||
- `capabilities` (`array` of `string`): Capability list from implementation.
|
||||
- `websiteURL` (`string`): Public website for the plugin.
|
||||
- `privacyPolicyURL` (`string`): Privacy policy URL.
|
||||
- `termsOfServiceURL` (`string`): Terms of service URL.
|
||||
- `defaultPrompt` (`array` of `string`): Starter prompts shown in composer/UX context.
|
||||
- Include at most 3 strings. Entries after the first 3 are ignored and will not be included.
|
||||
- Each string is capped at 128 characters. Longer entries are truncated.
|
||||
- Prefer short starter prompts around 50 characters so they scan well in the UI.
|
||||
- `brandColor` (`string`): Theme color for the plugin card.
|
||||
- `composerIcon` (`string`): Path to icon asset.
|
||||
- `logo` (`string`): Path to logo asset.
|
||||
- `screenshots` (`array` of `string`): List of screenshot asset paths.
|
||||
- Screenshot entries must be PNG filenames and stored under `./assets/`.
|
||||
- Keep file paths relative to plugin root.
|
||||
|
||||
### Path conventions and defaults
|
||||
|
||||
- Path values should be relative and begin with `./`.
|
||||
- `skills`, `hooks`, and `mcpServers` are supplemented on top of default component discovery; they do not replace defaults.
|
||||
- Custom path values must follow the plugin root convention and naming/namespacing rules.
|
||||
- This repo’s scaffold writes `.codex-plugin/plugin.json`; treat that as the manifest location this skill generates.
|
||||
|
||||
# Marketplace JSON sample spec
|
||||
|
||||
`marketplace.json` depends on where the plugin should live:
|
||||
|
||||
- Repo plugin: `<repo-root>/.agents/plugins/marketplace.json`
|
||||
- Local plugin: `~/.agents/plugins/marketplace.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "openai-curated",
|
||||
"interface": {
|
||||
"displayName": "ChatGPT Official"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "linear",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/linear"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Marketplace field guide
|
||||
|
||||
### Top-level fields
|
||||
|
||||
- `name` (`string`): Marketplace identifier or catalog name.
|
||||
- `interface` (`object`, optional): Marketplace presentation metadata.
|
||||
- `plugins` (`array`): Ordered plugin entries. This order determines how Codex renders plugins.
|
||||
|
||||
### `interface` fields
|
||||
|
||||
- `displayName` (`string`, optional): User-facing marketplace title.
|
||||
|
||||
### Plugin entry fields
|
||||
|
||||
- `name` (`string`): Plugin identifier. Match the plugin folder name and `plugin.json` `name`.
|
||||
- `source` (`object`): Plugin source descriptor.
|
||||
- `source` (`string`): Use `local` for this repo workflow.
|
||||
- `path` (`string`): Relative plugin path based on the marketplace root.
|
||||
- Repo plugin: `./plugins/<plugin-name>`
|
||||
- Local plugin in `~/.agents/plugins/marketplace.json`: `./plugins/<plugin-name>`
|
||||
- The same relative path convention is used for both repo-rooted and home-rooted marketplaces.
|
||||
- Example: with `~/.agents/plugins/marketplace.json`, `./plugins/<plugin-name>` resolves to `~/plugins/<plugin-name>`.
|
||||
- `policy` (`object`): Marketplace policy block. Always include it.
|
||||
- `installation` (`string`): Availability policy.
|
||||
- Allowed values: `NOT_AVAILABLE`, `AVAILABLE`, `INSTALLED_BY_DEFAULT`
|
||||
- Default for new entries: `AVAILABLE`
|
||||
- `authentication` (`string`): Authentication timing policy.
|
||||
- Allowed values: `ON_INSTALL`, `ON_USE`
|
||||
- Default for new entries: `ON_INSTALL`
|
||||
- `products` (`array` of `string`, optional): Product override for this plugin entry. Omit it unless product gating is explicitly requested.
|
||||
- `category` (`string`): Display category bucket. Always include it.
|
||||
|
||||
### Marketplace generation rules
|
||||
|
||||
- `displayName` belongs under the top-level `interface` object, not individual plugin entries.
|
||||
- When creating a new marketplace file from scratch, seed `interface.displayName` alongside top-level `name`.
|
||||
- Always include `policy.installation`, `policy.authentication`, and `category` on every generated or updated plugin entry.
|
||||
- Treat `policy.products` as an override and omit it unless explicitly requested.
|
||||
- Append new entries unless the user explicitly requests reordering.
|
||||
- Replace an existing entry for the same plugin only when overwrite is intentional.
|
||||
- Choose marketplace location to match the plugin destination:
|
||||
- Repo plugin: `<repo-root>/.agents/plugins/marketplace.json`
|
||||
- Local plugin: `~/.agents/plugins/marketplace.json`
|
||||
@@ -1,301 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scaffold a plugin directory and optionally update marketplace.json."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
MAX_PLUGIN_NAME_LENGTH = 64
|
||||
DEFAULT_PLUGIN_PARENT = Path.cwd() / "plugins"
|
||||
DEFAULT_MARKETPLACE_PATH = Path.cwd() / ".agents" / "plugins" / "marketplace.json"
|
||||
DEFAULT_INSTALL_POLICY = "AVAILABLE"
|
||||
DEFAULT_AUTH_POLICY = "ON_INSTALL"
|
||||
DEFAULT_CATEGORY = "Productivity"
|
||||
DEFAULT_MARKETPLACE_DISPLAY_NAME = "[TODO: Marketplace Display Name]"
|
||||
VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"}
|
||||
VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"}
|
||||
|
||||
|
||||
def normalize_plugin_name(plugin_name: str) -> str:
|
||||
"""Normalize a plugin name to lowercase hyphen-case."""
|
||||
normalized = plugin_name.strip().lower()
|
||||
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
|
||||
normalized = normalized.strip("-")
|
||||
normalized = re.sub(r"-{2,}", "-", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_plugin_name(plugin_name: str) -> None:
|
||||
if not plugin_name:
|
||||
raise ValueError("Plugin name must include at least one letter or digit.")
|
||||
if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH:
|
||||
raise ValueError(
|
||||
f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). "
|
||||
f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters."
|
||||
)
|
||||
|
||||
|
||||
def build_plugin_json(plugin_name: str) -> dict:
|
||||
return {
|
||||
"name": plugin_name,
|
||||
"version": "[TODO: 1.2.0]",
|
||||
"description": "[TODO: Brief plugin description]",
|
||||
"author": {
|
||||
"name": "[TODO: Author Name]",
|
||||
"email": "[TODO: author@example.com]",
|
||||
"url": "[TODO: https://github.com/author]",
|
||||
},
|
||||
"homepage": "[TODO: https://docs.example.com/plugin]",
|
||||
"repository": "[TODO: https://github.com/author/plugin]",
|
||||
"license": "[TODO: MIT]",
|
||||
"keywords": ["[TODO: keyword1]", "[TODO: keyword2]"],
|
||||
"skills": "[TODO: ./skills/]",
|
||||
"hooks": "[TODO: ./hooks.json]",
|
||||
"mcpServers": "[TODO: ./.mcp.json]",
|
||||
"apps": "[TODO: ./.app.json]",
|
||||
"interface": {
|
||||
"displayName": "[TODO: Plugin Display Name]",
|
||||
"shortDescription": "[TODO: Short description for subtitle]",
|
||||
"longDescription": "[TODO: Long description for details page]",
|
||||
"developerName": "[TODO: OpenAI]",
|
||||
"category": "[TODO: Productivity]",
|
||||
"capabilities": ["[TODO: Interactive]", "[TODO: Write]"],
|
||||
"websiteURL": "[TODO: https://openai.com/]",
|
||||
"privacyPolicyURL": "[TODO: https://openai.com/policies/row-privacy-policy/]",
|
||||
"termsOfServiceURL": "[TODO: https://openai.com/policies/row-terms-of-use/]",
|
||||
"defaultPrompt": [
|
||||
"[TODO: Summarize my inbox and draft replies for me.]",
|
||||
"[TODO: Find open bugs and turn them into tickets.]",
|
||||
"[TODO: Review today's meetings and flag gaps.]",
|
||||
],
|
||||
"brandColor": "[TODO: #3B82F6]",
|
||||
"composerIcon": "[TODO: ./assets/icon.png]",
|
||||
"logo": "[TODO: ./assets/logo.png]",
|
||||
"screenshots": [
|
||||
"[TODO: ./assets/screenshot1.png]",
|
||||
"[TODO: ./assets/screenshot2.png]",
|
||||
"[TODO: ./assets/screenshot3.png]",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_marketplace_entry(
|
||||
plugin_name: str,
|
||||
install_policy: str,
|
||||
auth_policy: str,
|
||||
category: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"name": plugin_name,
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": f"./plugins/{plugin_name}",
|
||||
},
|
||||
"policy": {
|
||||
"installation": install_policy,
|
||||
"authentication": auth_policy,
|
||||
},
|
||||
"category": category,
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open() as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def build_default_marketplace() -> dict[str, Any]:
|
||||
return {
|
||||
"name": "[TODO: marketplace-name]",
|
||||
"interface": {
|
||||
"displayName": DEFAULT_MARKETPLACE_DISPLAY_NAME,
|
||||
},
|
||||
"plugins": [],
|
||||
}
|
||||
|
||||
|
||||
def validate_marketplace_interface(payload: dict[str, Any]) -> None:
|
||||
interface = payload.get("interface")
|
||||
if interface is not None and not isinstance(interface, dict):
|
||||
raise ValueError("marketplace.json field 'interface' must be an object.")
|
||||
|
||||
|
||||
def update_marketplace_json(
|
||||
marketplace_path: Path,
|
||||
plugin_name: str,
|
||||
install_policy: str,
|
||||
auth_policy: str,
|
||||
category: str,
|
||||
force: bool,
|
||||
) -> None:
|
||||
if marketplace_path.exists():
|
||||
payload = load_json(marketplace_path)
|
||||
else:
|
||||
payload = build_default_marketplace()
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{marketplace_path} must contain a JSON object.")
|
||||
|
||||
validate_marketplace_interface(payload)
|
||||
|
||||
plugins = payload.setdefault("plugins", [])
|
||||
if not isinstance(plugins, list):
|
||||
raise ValueError(f"{marketplace_path} field 'plugins' must be an array.")
|
||||
|
||||
new_entry = build_marketplace_entry(plugin_name, install_policy, auth_policy, category)
|
||||
|
||||
for index, entry in enumerate(plugins):
|
||||
if isinstance(entry, dict) and entry.get("name") == plugin_name:
|
||||
if not force:
|
||||
raise FileExistsError(
|
||||
f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. "
|
||||
"Use --force to overwrite that entry."
|
||||
)
|
||||
plugins[index] = new_entry
|
||||
break
|
||||
else:
|
||||
plugins.append(new_entry)
|
||||
|
||||
write_json(marketplace_path, payload, force=True)
|
||||
|
||||
|
||||
def write_json(path: Path, data: dict, force: bool) -> None:
|
||||
if path.exists() and not force:
|
||||
raise FileExistsError(f"{path} already exists. Use --force to overwrite.")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w") as handle:
|
||||
json.dump(data, handle, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def create_stub_file(path: Path, payload: dict, force: bool) -> None:
|
||||
if path.exists() and not force:
|
||||
return
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w") as handle:
|
||||
json.dump(payload, handle, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a plugin skeleton with placeholder plugin.json."
|
||||
)
|
||||
parser.add_argument("plugin_name")
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
default=str(DEFAULT_PLUGIN_PARENT),
|
||||
help=(
|
||||
"Parent directory for plugin creation (defaults to <cwd>/plugins). "
|
||||
"When using a home-rooted marketplace, use <home>/plugins."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory")
|
||||
parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory")
|
||||
parser.add_argument("--with-scripts", action="store_true", help="Create scripts/ directory")
|
||||
parser.add_argument("--with-assets", action="store_true", help="Create assets/ directory")
|
||||
parser.add_argument("--with-mcp", action="store_true", help="Create .mcp.json placeholder")
|
||||
parser.add_argument("--with-apps", action="store_true", help="Create .app.json placeholder")
|
||||
parser.add_argument(
|
||||
"--with-marketplace",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Create or update <cwd>/.agents/plugins/marketplace.json. "
|
||||
"Marketplace entries always point to ./plugins/<plugin-name> relative to the "
|
||||
"marketplace root."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--marketplace-path",
|
||||
default=str(DEFAULT_MARKETPLACE_PATH),
|
||||
help=(
|
||||
"Path to marketplace.json (defaults to <cwd>/.agents/plugins/marketplace.json). "
|
||||
"For a home-rooted marketplace, use <home>/.agents/plugins/marketplace.json."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-policy",
|
||||
default=DEFAULT_INSTALL_POLICY,
|
||||
choices=sorted(VALID_INSTALL_POLICIES),
|
||||
help="Marketplace policy.installation value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auth-policy",
|
||||
default=DEFAULT_AUTH_POLICY,
|
||||
choices=sorted(VALID_AUTH_POLICIES),
|
||||
help="Marketplace policy.authentication value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--category",
|
||||
default=DEFAULT_CATEGORY,
|
||||
help="Marketplace category value",
|
||||
)
|
||||
parser.add_argument("--force", action="store_true", help="Overwrite existing files")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
raw_plugin_name = args.plugin_name
|
||||
plugin_name = normalize_plugin_name(raw_plugin_name)
|
||||
if plugin_name != raw_plugin_name:
|
||||
print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.")
|
||||
validate_plugin_name(plugin_name)
|
||||
|
||||
plugin_root = (Path(args.path).expanduser().resolve() / plugin_name)
|
||||
plugin_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json"
|
||||
write_json(plugin_json_path, build_plugin_json(plugin_name), args.force)
|
||||
|
||||
optional_directories = {
|
||||
"skills": args.with_skills,
|
||||
"hooks": args.with_hooks,
|
||||
"scripts": args.with_scripts,
|
||||
"assets": args.with_assets,
|
||||
}
|
||||
for folder, enabled in optional_directories.items():
|
||||
if enabled:
|
||||
(plugin_root / folder).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.with_mcp:
|
||||
create_stub_file(
|
||||
plugin_root / ".mcp.json",
|
||||
{"mcpServers": {}},
|
||||
args.force,
|
||||
)
|
||||
|
||||
if args.with_apps:
|
||||
create_stub_file(
|
||||
plugin_root / ".app.json",
|
||||
{
|
||||
"apps": {},
|
||||
},
|
||||
args.force,
|
||||
)
|
||||
|
||||
if args.with_marketplace:
|
||||
marketplace_path = Path(args.marketplace_path).expanduser().resolve()
|
||||
update_marketplace_json(
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
args.install_policy,
|
||||
args.auth_policy,
|
||||
args.category,
|
||||
args.force,
|
||||
)
|
||||
|
||||
print(f"Created plugin scaffold: {plugin_root}")
|
||||
print(f"plugin manifest: {plugin_json_path}")
|
||||
if args.with_marketplace:
|
||||
print(f"marketplace manifest: {marketplace_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,416 +0,0 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.
|
||||
metadata:
|
||||
short-description: Create or update a skill
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
This skill provides guidance for creating effective skills.
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular, self-contained folders that extend Codex's capabilities by providing
|
||||
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
|
||||
domains or tasks—they transform Codex from a general-purpose agent into a specialized agent
|
||||
equipped with procedural knowledge that no model can fully possess.
|
||||
|
||||
### What Skills Provide
|
||||
|
||||
1. Specialized workflows - Multi-step procedures for specific domains
|
||||
2. Tool integrations - Instructions for working with specific file formats or APIs
|
||||
3. Domain expertise - Company-specific knowledge, schemas, business logic
|
||||
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Concise is Key
|
||||
|
||||
The context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
|
||||
|
||||
**Default assumption: Codex is already very smart.** Only add context Codex doesn't already have. Challenge each piece of information: "Does Codex really need this explanation?" and "Does this paragraph justify its token cost?"
|
||||
|
||||
Prefer concise examples over verbose explanations.
|
||||
|
||||
### Set Appropriate Degrees of Freedom
|
||||
|
||||
Match the level of specificity to the task's fragility and variability:
|
||||
|
||||
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
|
||||
|
||||
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
|
||||
|
||||
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
|
||||
|
||||
Think of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
|
||||
|
||||
### Protect Validation Integrity
|
||||
|
||||
You may use subagents during iteration to validate whether a skill works on realistic tasks or whether a suspected problem is real. This is most useful when you want an independent pass on the skill's behavior, outputs, or failure modes after a revision. Only do this when it is possible to start new subagents.
|
||||
|
||||
When using subagents for validation, treat that as an evaluation surface. The goal is to learn whether the skill generalizes, not whether another agent can reconstruct the answer from leaked context.
|
||||
|
||||
Prefer raw artifacts such as example prompts, outputs, diffs, logs, or traces. Give the minimum task-local context needed to perform the validation. Avoid passing the intended answer, suspected bug, intended fix, or your prior conclusions unless the validation explicitly requires them.
|
||||
|
||||
### Anatomy of a Skill
|
||||
|
||||
Every skill consists of a required SKILL.md file and optional bundled resources:
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter metadata (required)
|
||||
│ │ ├── name: (required)
|
||||
│ │ └── description: (required)
|
||||
│ └── Markdown instructions (required)
|
||||
├── agents/ (recommended)
|
||||
│ └── openai.yaml - UI metadata for skill lists and chips
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code (Python/Bash/etc.)
|
||||
├── references/ - Documentation intended to be loaded into context as needed
|
||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
||||
```
|
||||
|
||||
#### SKILL.md (required)
|
||||
|
||||
Every SKILL.md consists of:
|
||||
|
||||
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
|
||||
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
|
||||
|
||||
#### Agents metadata (recommended)
|
||||
|
||||
- UI-facing metadata for skill lists and chips
|
||||
- Read references/openai_yaml.md before generating values and follow its descriptions and constraints
|
||||
- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill
|
||||
- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`
|
||||
- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale
|
||||
- Only include other optional interface fields (icons, brand color) if explicitly provided
|
||||
- See references/openai_yaml.md for field definitions and examples
|
||||
|
||||
#### Bundled Resources (optional)
|
||||
|
||||
##### Scripts (`scripts/`)
|
||||
|
||||
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
|
||||
|
||||
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
|
||||
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
|
||||
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
||||
- **Note**: Scripts may still need to be read by Codex for patching or environment-specific adjustments
|
||||
|
||||
##### References (`references/`)
|
||||
|
||||
Documentation and reference material intended to be loaded as needed into context to inform Codex's process and thinking.
|
||||
|
||||
- **When to include**: For documentation that Codex should reference while working
|
||||
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
|
||||
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
|
||||
- **Benefits**: Keeps SKILL.md lean, loaded only when Codex determines it's needed
|
||||
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
|
||||
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
|
||||
|
||||
##### Assets (`assets/`)
|
||||
|
||||
Files not intended to be loaded into context, but rather used within the output Codex produces.
|
||||
|
||||
- **When to include**: When the skill needs files that will be used in the final output
|
||||
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
|
||||
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
|
||||
- **Benefits**: Separates output resources from documentation, enables Codex to use files without loading them into context
|
||||
|
||||
#### What to Not Include in a Skill
|
||||
|
||||
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
|
||||
|
||||
- README.md
|
||||
- INSTALLATION_GUIDE.md
|
||||
- QUICK_REFERENCE.md
|
||||
- CHANGELOG.md
|
||||
- etc.
|
||||
|
||||
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
|
||||
|
||||
### Progressive Disclosure Design Principle
|
||||
|
||||
Skills use a three-level loading system to manage context efficiently:
|
||||
|
||||
1. **Metadata (name + description)** - Always in context (~100 words)
|
||||
2. **SKILL.md body** - When skill triggers (<5k words)
|
||||
3. **Bundled resources** - As needed by Codex (Unlimited because scripts can be executed without reading into context window)
|
||||
|
||||
#### Progressive Disclosure Patterns
|
||||
|
||||
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
|
||||
|
||||
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
|
||||
|
||||
**Pattern 1: High-level guide with references**
|
||||
|
||||
```markdown
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Extract text with pdfplumber:
|
||||
[code example]
|
||||
|
||||
## Advanced features
|
||||
|
||||
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
|
||||
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
|
||||
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
|
||||
```
|
||||
|
||||
Codex loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
|
||||
|
||||
**Pattern 2: Domain-specific organization**
|
||||
|
||||
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
|
||||
|
||||
```
|
||||
bigquery-skill/
|
||||
├── SKILL.md (overview and navigation)
|
||||
└── reference/
|
||||
├── finance.md (revenue, billing metrics)
|
||||
├── sales.md (opportunities, pipeline)
|
||||
├── product.md (API usage, features)
|
||||
└── marketing.md (campaigns, attribution)
|
||||
```
|
||||
|
||||
When a user asks about sales metrics, Codex only reads sales.md.
|
||||
|
||||
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
|
||||
|
||||
```
|
||||
cloud-deploy/
|
||||
├── SKILL.md (workflow + provider selection)
|
||||
└── references/
|
||||
├── aws.md (AWS deployment patterns)
|
||||
├── gcp.md (GCP deployment patterns)
|
||||
└── azure.md (Azure deployment patterns)
|
||||
```
|
||||
|
||||
When the user chooses AWS, Codex only reads aws.md.
|
||||
|
||||
**Pattern 3: Conditional details**
|
||||
|
||||
Show basic content, link to advanced content:
|
||||
|
||||
```markdown
|
||||
# DOCX Processing
|
||||
|
||||
## Creating documents
|
||||
|
||||
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
|
||||
|
||||
## Editing documents
|
||||
|
||||
For simple edits, modify the XML directly.
|
||||
|
||||
**For tracked changes**: See [REDLINING.md](REDLINING.md)
|
||||
**For OOXML details**: See [OOXML.md](OOXML.md)
|
||||
```
|
||||
|
||||
Codex reads REDLINING.md or OOXML.md only when the user needs those features.
|
||||
|
||||
**Important guidelines:**
|
||||
|
||||
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
|
||||
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Codex can see the full scope when previewing.
|
||||
|
||||
## Skill Creation Process
|
||||
|
||||
Skill creation involves these steps:
|
||||
|
||||
1. Understand the skill with concrete examples
|
||||
2. Plan reusable skill contents (scripts, references, assets)
|
||||
3. Initialize the skill (run init_skill.py)
|
||||
4. Edit the skill (implement resources and write SKILL.md)
|
||||
5. Validate the skill (run quick_validate.py)
|
||||
6. Iterate based on real usage and forward-test complex skills.
|
||||
|
||||
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
|
||||
|
||||
### Skill Naming
|
||||
|
||||
- Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`).
|
||||
- When generating names, generate a name under 64 characters (letters, digits, hyphens).
|
||||
- Prefer short, verb-led phrases that describe the action.
|
||||
- Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`).
|
||||
- Name the skill folder exactly after the skill name.
|
||||
|
||||
### Step 1: Understanding the Skill with Concrete Examples
|
||||
|
||||
Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
|
||||
|
||||
To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
|
||||
|
||||
For example, when building an image-editor skill, relevant questions include:
|
||||
|
||||
- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
|
||||
- "Can you give some examples of how this skill would be used?"
|
||||
- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
|
||||
- "What would a user say that should trigger this skill?"
|
||||
- "Where should I create this skill? If you do not have a preference, I will place it in `$CODEX_HOME/skills` (or `~/.codex/skills` when `CODEX_HOME` is unset) so Codex can discover it automatically."
|
||||
|
||||
To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
|
||||
|
||||
Conclude this step when there is a clear sense of the functionality the skill should support.
|
||||
|
||||
### Step 2: Planning the Reusable Skill Contents
|
||||
|
||||
To turn concrete examples into an effective skill, analyze each example by:
|
||||
|
||||
1. Considering how to execute on the example from scratch
|
||||
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
|
||||
|
||||
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
|
||||
|
||||
1. Rotating a PDF requires re-writing the same code each time
|
||||
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
|
||||
|
||||
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
|
||||
|
||||
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
|
||||
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
|
||||
|
||||
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
|
||||
|
||||
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
|
||||
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
|
||||
|
||||
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
|
||||
|
||||
### Step 3: Initializing the Skill
|
||||
|
||||
At this point, it is time to actually create the skill.
|
||||
|
||||
Skip this step only if the skill being developed already exists. In this case, continue to the next step.
|
||||
|
||||
Before running `init_skill.py`, ask where the user wants the skill created. If they do not specify a location, default to `$CODEX_HOME/skills`; when `CODEX_HOME` is unset, fall back to `~/.codex/skills` so the skill is auto-discovered.
|
||||
|
||||
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
scripts/init_skill.py <skill-name> --path <output-directory> [--resources scripts,references,assets] [--examples]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
scripts/init_skill.py my-skill --path "${CODEX_HOME:-$HOME/.codex}/skills"
|
||||
scripts/init_skill.py my-skill --path "${CODEX_HOME:-$HOME/.codex}/skills" --resources scripts,references
|
||||
scripts/init_skill.py my-skill --path ~/work/skills --resources scripts --examples
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
- Creates the skill directory at the specified path
|
||||
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
|
||||
- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`
|
||||
- Optionally creates resource directories based on `--resources`
|
||||
- Optionally adds example files when `--examples` is set
|
||||
|
||||
After initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.
|
||||
|
||||
Generate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:
|
||||
|
||||
```bash
|
||||
scripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value
|
||||
```
|
||||
|
||||
Only include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.
|
||||
|
||||
### Step 4: Edit the Skill
|
||||
|
||||
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.
|
||||
|
||||
After substantial revisions, or if the skill is particularly tricky, you should use subagents to forward-test the skill on realistic tasks or artifacts. When doing so, pass the artifact under validation rather than your diagnosis of what is wrong, and keep the prompt generic enough that success depends on transferable reasoning rather than hidden ground truth.
|
||||
|
||||
#### Start with Reusable Skill Contents
|
||||
|
||||
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
|
||||
|
||||
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
|
||||
|
||||
If you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.
|
||||
|
||||
#### Update SKILL.md
|
||||
|
||||
**Writing Guidelines:** Always use imperative/infinitive form.
|
||||
|
||||
##### Frontmatter
|
||||
|
||||
Write the YAML frontmatter with `name` and `description`:
|
||||
|
||||
- `name`: The skill name
|
||||
- `description`: This is the primary triggering mechanism for your skill, and helps Codex understand when to use the skill.
|
||||
- Include both what the Skill does and specific triggers/contexts for when to use it.
|
||||
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Codex.
|
||||
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
|
||||
|
||||
Do not include any other fields in YAML frontmatter.
|
||||
|
||||
##### Body
|
||||
|
||||
Write instructions for using the skill and its bundled resources.
|
||||
|
||||
### Step 5: Validate the Skill
|
||||
|
||||
Once development of the skill is complete, validate the skill folder to catch basic issues early:
|
||||
|
||||
```bash
|
||||
scripts/quick_validate.py <path/to/skill-folder>
|
||||
```
|
||||
|
||||
The validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.
|
||||
|
||||
### Step 6: Iterate
|
||||
|
||||
After testing the skill, you may detect the skill is complex enough that it requires forward-testing; or users may request improvements.
|
||||
|
||||
User testing often this happens right after using the skill, with fresh context of how the skill performed.
|
||||
|
||||
**Forward-testing and iteration workflow:**
|
||||
|
||||
1. Use the skill on real tasks
|
||||
2. Notice struggles or inefficiencies
|
||||
3. Identify how SKILL.md or bundled resources should be updated
|
||||
4. Implement changes and test again
|
||||
5. Forward-test if it is reasonable and appropriate
|
||||
|
||||
## Forward-testing
|
||||
|
||||
To forward-test, launch subagents as a way to stress test the skill with minimal context.
|
||||
Subagents should *not* know that they are being asked to test the skill. They should be treated as
|
||||
an agent asked to perform a task by the user. Prompts to subagents should look like:
|
||||
`Use $skill-x at /path/to/skill-x to solve problem y`
|
||||
Not:
|
||||
`Review the skill at /path/to/skill-x; pretend a user asks you to...`
|
||||
|
||||
Decision rule for forward-testing:
|
||||
- Err on the side of forward-testing
|
||||
- Ask for approval if you think there's a risk that forward-testing would:
|
||||
* take a long time,
|
||||
* require additional approvals from the user, or
|
||||
* modify live production systems
|
||||
|
||||
In these cases, show the user your proposed prompt and request (1) a yes/no decision, and
|
||||
(2) any suggested modifictions.
|
||||
|
||||
Considerations when forward-testing:
|
||||
- use fresh threads for independent passes
|
||||
- pass the skill, and a request in a similar way the user would.
|
||||
- pass raw artifacts, not your conclusions
|
||||
- avoid showing expected answers or intended fixes
|
||||
- rebuild context from source artifacts after each iteration
|
||||
- review the subagent's output and reasoning and emitted artifacts
|
||||
- avoid leaving artifacts the agent can find on disk between iterations;
|
||||
clean up subagents' artifacts to avoid additional contamination.
|
||||
|
||||
If forward-testing only succeeds when subagents see leaked context, tighten the skill or the
|
||||
forward-testing setup before trusting the result.
|
||||
@@ -1,5 +0,0 @@
|
||||
interface:
|
||||
display_name: "Skill Creator"
|
||||
short_description: "Create or update a skill"
|
||||
icon_small: "./assets/skill-creator-small.svg"
|
||||
icon_large: "./assets/skill-creator.png"
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill="#0D0D0D" d="M12.03 4.113a3.612 3.612 0 0 1 5.108 5.108l-6.292 6.29c-.324.324-.56.561-.791.752l-.235.176c-.205.14-.422.261-.65.36l-.229.093a4.136 4.136 0 0 1-.586.16l-.764.134-2.394.4c-.142.024-.294.05-.423.06-.098.007-.232.01-.378-.026l-.149-.05a1.081 1.081 0 0 1-.521-.474l-.046-.093a1.104 1.104 0 0 1-.075-.527c.01-.129.035-.28.06-.422l.398-2.394c.1-.602.162-.987.295-1.35l.093-.23c.1-.228.22-.445.36-.65l.176-.235c.19-.232.428-.467.751-.79l6.292-6.292Zm-5.35 7.232c-.35.35-.534.535-.66.688l-.11.147a2.67 2.67 0 0 0-.24.433l-.062.154c-.08.22-.124.462-.232 1.112l-.398 2.394-.001.001h.003l2.393-.399.717-.126a2.63 2.63 0 0 0 .394-.105l.154-.063a2.65 2.65 0 0 0 .433-.24l.147-.11c.153-.126.339-.31.688-.66l4.988-4.988-3.227-3.226-4.987 4.988Zm9.517-6.291a2.281 2.281 0 0 0-3.225 0l-.364.362 3.226 3.227.363-.364c.89-.89.89-2.334 0-3.225ZM4.583 1.783a.3.3 0 0 1 .294.241c.117.585.347 1.092.707 1.48.357.385.859.668 1.549.783a.3.3 0 0 1 0 .592c-.69.115-1.192.398-1.549.783-.315.34-.53.77-.657 1.265l-.05.215a.3.3 0 0 1-.588 0c-.117-.585-.347-1.092-.707-1.48-.357-.384-.859-.668-1.549-.783a.3.3 0 0 1 0-.592c.69-.115 1.192-.398 1.549-.783.36-.388.59-.895.707-1.48l.015-.05a.3.3 0 0 1 .279-.19Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,49 +0,0 @@
|
||||
# openai.yaml fields (full example + descriptions)
|
||||
|
||||
`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.
|
||||
|
||||
## Full example
|
||||
|
||||
```yaml
|
||||
interface:
|
||||
display_name: "Optional user-facing name"
|
||||
short_description: "Optional user-facing description"
|
||||
icon_small: "./assets/small-400px.png"
|
||||
icon_large: "./assets/large-logo.svg"
|
||||
brand_color: "#3B82F6"
|
||||
default_prompt: "Optional surrounding prompt to use the skill with"
|
||||
|
||||
dependencies:
|
||||
tools:
|
||||
- type: "mcp"
|
||||
value: "github"
|
||||
description: "GitHub MCP server"
|
||||
transport: "streamable_http"
|
||||
url: "https://api.githubcopilot.com/mcp/"
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
```
|
||||
|
||||
## Field descriptions and constraints
|
||||
|
||||
Top-level constraints:
|
||||
|
||||
- Quote all string values.
|
||||
- Keep keys unquoted.
|
||||
- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., "Use $skill-name-here to draft a concise weekly status update.").
|
||||
|
||||
- `interface.display_name`: Human-facing title shown in UI skill lists and chips.
|
||||
- `interface.short_description`: Human-facing short UI blurb (25–64 chars) for quick scanning.
|
||||
- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
|
||||
- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
|
||||
- `interface.brand_color`: Hex color used for UI accents (e.g., badges).
|
||||
- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.
|
||||
- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.
|
||||
- `dependencies.tools[].value`: Identifier of the tool or dependency.
|
||||
- `dependencies.tools[].description`: Human-readable explanation of the dependency.
|
||||
- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.
|
||||
- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.
|
||||
- `policy.allow_implicit_invocation`: When false, the skill is not injected into
|
||||
the model context by default, but can still be invoked explicitly via `$skill`.
|
||||
Defaults to true.
|
||||
@@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.
|
||||
|
||||
Usage:
|
||||
generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ACRONYMS = {
|
||||
"GH",
|
||||
"MCP",
|
||||
"API",
|
||||
"CI",
|
||||
"CLI",
|
||||
"LLM",
|
||||
"PDF",
|
||||
"PR",
|
||||
"UI",
|
||||
"URL",
|
||||
"SQL",
|
||||
}
|
||||
|
||||
BRANDS = {
|
||||
"openai": "OpenAI",
|
||||
"openapi": "OpenAPI",
|
||||
"github": "GitHub",
|
||||
"pagerduty": "PagerDuty",
|
||||
"datadog": "DataDog",
|
||||
"sqlite": "SQLite",
|
||||
"fastapi": "FastAPI",
|
||||
}
|
||||
|
||||
SMALL_WORDS = {"and", "or", "to", "up", "with"}
|
||||
|
||||
ALLOWED_INTERFACE_KEYS = {
|
||||
"display_name",
|
||||
"short_description",
|
||||
"icon_small",
|
||||
"icon_large",
|
||||
"brand_color",
|
||||
"default_prompt",
|
||||
}
|
||||
|
||||
|
||||
def yaml_quote(value):
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def format_display_name(skill_name):
|
||||
words = [word for word in skill_name.split("-") if word]
|
||||
formatted = []
|
||||
for index, word in enumerate(words):
|
||||
lower = word.lower()
|
||||
upper = word.upper()
|
||||
if upper in ACRONYMS:
|
||||
formatted.append(upper)
|
||||
continue
|
||||
if lower in BRANDS:
|
||||
formatted.append(BRANDS[lower])
|
||||
continue
|
||||
if index > 0 and lower in SMALL_WORDS:
|
||||
formatted.append(lower)
|
||||
continue
|
||||
formatted.append(word.capitalize())
|
||||
return " ".join(formatted)
|
||||
|
||||
|
||||
def generate_short_description(display_name):
|
||||
description = f"Help with {display_name} tasks"
|
||||
|
||||
if len(description) < 25:
|
||||
description = f"Help with {display_name} tasks and workflows"
|
||||
if len(description) < 25:
|
||||
description = f"Help with {display_name} tasks with guidance"
|
||||
|
||||
if len(description) > 64:
|
||||
description = f"Help with {display_name}"
|
||||
if len(description) > 64:
|
||||
description = f"{display_name} helper"
|
||||
if len(description) > 64:
|
||||
description = f"{display_name} tools"
|
||||
if len(description) > 64:
|
||||
suffix = " helper"
|
||||
max_name_length = 64 - len(suffix)
|
||||
trimmed = display_name[:max_name_length].rstrip()
|
||||
description = f"{trimmed}{suffix}"
|
||||
if len(description) > 64:
|
||||
description = description[:64].rstrip()
|
||||
|
||||
if len(description) < 25:
|
||||
description = f"{description} workflows"
|
||||
if len(description) > 64:
|
||||
description = description[:64].rstrip()
|
||||
|
||||
return description
|
||||
|
||||
|
||||
def read_frontmatter_name(skill_dir):
|
||||
skill_md = Path(skill_dir) / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"[ERROR] SKILL.md not found in {skill_dir}")
|
||||
return None
|
||||
content = skill_md.read_text()
|
||||
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not match:
|
||||
print("[ERROR] Invalid SKILL.md frontmatter format.")
|
||||
return None
|
||||
frontmatter_text = match.group(1)
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"[ERROR] Invalid YAML frontmatter: {exc}")
|
||||
return None
|
||||
if not isinstance(frontmatter, dict):
|
||||
print("[ERROR] Frontmatter must be a YAML dictionary.")
|
||||
return None
|
||||
name = frontmatter.get("name", "")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
print("[ERROR] Frontmatter 'name' is missing or invalid.")
|
||||
return None
|
||||
return name.strip()
|
||||
|
||||
|
||||
def parse_interface_overrides(raw_overrides):
|
||||
overrides = {}
|
||||
optional_order = []
|
||||
for item in raw_overrides:
|
||||
if "=" not in item:
|
||||
print(f"[ERROR] Invalid interface override '{item}'. Use key=value.")
|
||||
return None, None
|
||||
key, value = item.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if not key:
|
||||
print(f"[ERROR] Invalid interface override '{item}'. Key is empty.")
|
||||
return None, None
|
||||
if key not in ALLOWED_INTERFACE_KEYS:
|
||||
allowed = ", ".join(sorted(ALLOWED_INTERFACE_KEYS))
|
||||
print(f"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}")
|
||||
return None, None
|
||||
overrides[key] = value
|
||||
if key not in ("display_name", "short_description") and key not in optional_order:
|
||||
optional_order.append(key)
|
||||
return overrides, optional_order
|
||||
|
||||
|
||||
def write_openai_yaml(skill_dir, skill_name, raw_overrides):
|
||||
overrides, optional_order = parse_interface_overrides(raw_overrides)
|
||||
if overrides is None:
|
||||
return None
|
||||
|
||||
display_name = overrides.get("display_name") or format_display_name(skill_name)
|
||||
short_description = overrides.get("short_description") or generate_short_description(display_name)
|
||||
|
||||
if not (25 <= len(short_description) <= 64):
|
||||
print(
|
||||
"[ERROR] short_description must be 25-64 characters "
|
||||
f"(got {len(short_description)})."
|
||||
)
|
||||
return None
|
||||
|
||||
interface_lines = [
|
||||
"interface:",
|
||||
f" display_name: {yaml_quote(display_name)}",
|
||||
f" short_description: {yaml_quote(short_description)}",
|
||||
]
|
||||
|
||||
for key in optional_order:
|
||||
value = overrides.get(key)
|
||||
if value is not None:
|
||||
interface_lines.append(f" {key}: {yaml_quote(value)}")
|
||||
|
||||
agents_dir = Path(skill_dir) / "agents"
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = agents_dir / "openai.yaml"
|
||||
output_path.write_text("\n".join(interface_lines) + "\n")
|
||||
print(f"[OK] Created agents/openai.yaml")
|
||||
return output_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create agents/openai.yaml for a skill directory.",
|
||||
)
|
||||
parser.add_argument("skill_dir", help="Path to the skill directory")
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
help="Skill name override (defaults to SKILL.md frontmatter)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interface",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Interface override in key=value format (repeatable)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
skill_dir = Path(args.skill_dir).resolve()
|
||||
if not skill_dir.exists():
|
||||
print(f"[ERROR] Skill directory not found: {skill_dir}")
|
||||
sys.exit(1)
|
||||
if not skill_dir.is_dir():
|
||||
print(f"[ERROR] Path is not a directory: {skill_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
skill_name = args.name or read_frontmatter_name(skill_dir)
|
||||
if not skill_name:
|
||||
sys.exit(1)
|
||||
|
||||
result = write_openai_yaml(skill_dir, skill_name, args.interface)
|
||||
if result:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,400 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Initializer - Creates a new skill from template
|
||||
|
||||
Usage:
|
||||
init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples] [--interface key=value]
|
||||
|
||||
Examples:
|
||||
init_skill.py my-new-skill --path skills/public
|
||||
init_skill.py my-new-skill --path skills/public --resources scripts,references
|
||||
init_skill.py my-api-helper --path skills/private --resources scripts --examples
|
||||
init_skill.py custom-skill --path /custom/location
|
||||
init_skill.py my-skill --path skills/public --interface short_description="Short UI label"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from generate_openai_yaml import write_openai_yaml
|
||||
|
||||
MAX_SKILL_NAME_LENGTH = 64
|
||||
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
|
||||
|
||||
SKILL_TEMPLATE = """---
|
||||
name: {skill_name}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {skill_title}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## Structuring This Skill
|
||||
|
||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||
|
||||
**1. Workflow-Based** (best for sequential processes)
|
||||
- Works well when there are clear step-by-step procedures
|
||||
- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing"
|
||||
- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2...
|
||||
|
||||
**2. Task-Based** (best for tool collections)
|
||||
- Works well when the skill offers different operations/capabilities
|
||||
- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text"
|
||||
- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...
|
||||
|
||||
**3. Reference/Guidelines** (best for standards or specifications)
|
||||
- Works well for brand guidelines, coding standards, or requirements
|
||||
- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features"
|
||||
- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage...
|
||||
|
||||
**4. Capabilities-Based** (best for integrated systems)
|
||||
- Works well when the skill provides multiple interrelated features
|
||||
- Example: Product Management with "Core Capabilities" -> numbered capability list
|
||||
- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...
|
||||
|
||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||
|
||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||
|
||||
## [TODO: Replace with the first main section based on chosen structure]
|
||||
|
||||
[TODO: Add content here. See examples in existing skills:
|
||||
- Code samples for technical skills
|
||||
- Decision trees for complex workflows
|
||||
- Concrete examples with realistic user requests
|
||||
- References to scripts/templates/references as needed]
|
||||
|
||||
## Resources (optional)
|
||||
|
||||
Create only the resource directories this skill actually needs. Delete this section if no resources are required.
|
||||
|
||||
### scripts/
|
||||
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||
|
||||
**Examples from other skills:**
|
||||
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
|
||||
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
|
||||
|
||||
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
|
||||
|
||||
**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.
|
||||
|
||||
### references/
|
||||
Documentation and reference material intended to be loaded into context to inform Codex's process and thinking.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
|
||||
- BigQuery: API reference documentation and query examples
|
||||
- Finance: Schema documentation, company policies
|
||||
|
||||
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.
|
||||
|
||||
### assets/
|
||||
Files not intended to be loaded into context, but rather used within the output Codex produces.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Brand styling: PowerPoint template files (.pptx), logo files
|
||||
- Frontend builder: HTML/React boilerplate project directories
|
||||
- Typography: Font files (.ttf, .woff2)
|
||||
|
||||
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
|
||||
|
||||
---
|
||||
|
||||
**Not every skill requires all three types of resources.**
|
||||
"""
|
||||
|
||||
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
|
||||
"""
|
||||
Example helper script for {skill_name}
|
||||
|
||||
This is a placeholder script that can be executed directly.
|
||||
Replace with actual implementation or delete if not needed.
|
||||
|
||||
Example real scripts from other skills:
|
||||
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
|
||||
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("This is an example script for {skill_name}")
|
||||
# TODO: Add actual script logic here
|
||||
# This could be data processing, file conversion, API calls, etc.
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
|
||||
|
||||
This is a placeholder for detailed reference documentation.
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
Example real reference docs from other skills:
|
||||
- product-management/references/communication.md - Comprehensive guide for status updates
|
||||
- product-management/references/context_building.md - Deep-dive on gathering context
|
||||
- bigquery/references/ - API references and query examples
|
||||
|
||||
## When Reference Docs Are Useful
|
||||
|
||||
Reference docs are ideal for:
|
||||
- Comprehensive API documentation
|
||||
- Detailed workflow guides
|
||||
- Complex multi-step processes
|
||||
- Information too lengthy for main SKILL.md
|
||||
- Content that's only needed for specific use cases
|
||||
|
||||
## Structure Suggestions
|
||||
|
||||
### API Reference Example
|
||||
- Overview
|
||||
- Authentication
|
||||
- Endpoints with examples
|
||||
- Error codes
|
||||
- Rate limits
|
||||
|
||||
### Workflow Guide Example
|
||||
- Prerequisites
|
||||
- Step-by-step instructions
|
||||
- Common patterns
|
||||
- Troubleshooting
|
||||
- Best practices
|
||||
"""
|
||||
|
||||
EXAMPLE_ASSET = """# Example Asset File
|
||||
|
||||
This placeholder represents where asset files would be stored.
|
||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT intended to be loaded into context, but rather used within
|
||||
the output Codex produces.
|
||||
|
||||
Example asset files from other skills:
|
||||
- Brand guidelines: logo.png, slides_template.pptx
|
||||
- Frontend builder: hello-world/ directory with HTML/React boilerplate
|
||||
- Typography: custom-font.ttf, font-family.woff2
|
||||
- Data: sample_data.csv, test_dataset.json
|
||||
|
||||
## Common Asset Types
|
||||
|
||||
- Templates: .pptx, .docx, boilerplate directories
|
||||
- Images: .png, .jpg, .svg, .gif
|
||||
- Fonts: .ttf, .otf, .woff, .woff2
|
||||
- Boilerplate code: Project directories, starter files
|
||||
- Icons: .ico, .svg
|
||||
- Data files: .csv, .json, .xml, .yaml
|
||||
|
||||
Note: This is a text placeholder. Actual assets can be any file type.
|
||||
"""
|
||||
|
||||
|
||||
def normalize_skill_name(skill_name):
|
||||
"""Normalize a skill name to lowercase hyphen-case."""
|
||||
normalized = skill_name.strip().lower()
|
||||
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
|
||||
normalized = normalized.strip("-")
|
||||
normalized = re.sub(r"-{2,}", "-", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def title_case_skill_name(skill_name):
|
||||
"""Convert hyphenated skill name to Title Case for display."""
|
||||
return " ".join(word.capitalize() for word in skill_name.split("-"))
|
||||
|
||||
|
||||
def parse_resources(raw_resources):
|
||||
if not raw_resources:
|
||||
return []
|
||||
resources = [item.strip() for item in raw_resources.split(",") if item.strip()]
|
||||
invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})
|
||||
if invalid:
|
||||
allowed = ", ".join(sorted(ALLOWED_RESOURCES))
|
||||
print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}")
|
||||
print(f" Allowed: {allowed}")
|
||||
sys.exit(1)
|
||||
deduped = []
|
||||
seen = set()
|
||||
for resource in resources:
|
||||
if resource not in seen:
|
||||
deduped.append(resource)
|
||||
seen.add(resource)
|
||||
return deduped
|
||||
|
||||
|
||||
def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):
|
||||
for resource in resources:
|
||||
resource_dir = skill_dir / resource
|
||||
resource_dir.mkdir(exist_ok=True)
|
||||
if resource == "scripts":
|
||||
if include_examples:
|
||||
example_script = resource_dir / "example.py"
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
||||
example_script.chmod(0o755)
|
||||
print("[OK] Created scripts/example.py")
|
||||
else:
|
||||
print("[OK] Created scripts/")
|
||||
elif resource == "references":
|
||||
if include_examples:
|
||||
example_reference = resource_dir / "api_reference.md"
|
||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
||||
print("[OK] Created references/api_reference.md")
|
||||
else:
|
||||
print("[OK] Created references/")
|
||||
elif resource == "assets":
|
||||
if include_examples:
|
||||
example_asset = resource_dir / "example_asset.txt"
|
||||
example_asset.write_text(EXAMPLE_ASSET)
|
||||
print("[OK] Created assets/example_asset.txt")
|
||||
else:
|
||||
print("[OK] Created assets/")
|
||||
|
||||
|
||||
def init_skill(skill_name, path, resources, include_examples, interface_overrides):
|
||||
"""
|
||||
Initialize a new skill directory with template SKILL.md.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
path: Path where the skill directory should be created
|
||||
resources: Resource directories to create
|
||||
include_examples: Whether to create example files in resource directories
|
||||
|
||||
Returns:
|
||||
Path to created skill directory, or None if error
|
||||
"""
|
||||
# Determine skill directory path
|
||||
skill_dir = Path(path).resolve() / skill_name
|
||||
|
||||
# Check if directory already exists
|
||||
if skill_dir.exists():
|
||||
print(f"[ERROR] Skill directory already exists: {skill_dir}")
|
||||
return None
|
||||
|
||||
# Create skill directory
|
||||
try:
|
||||
skill_dir.mkdir(parents=True, exist_ok=False)
|
||||
print(f"[OK] Created skill directory: {skill_dir}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating directory: {e}")
|
||||
return None
|
||||
|
||||
# Create SKILL.md from template
|
||||
skill_title = title_case_skill_name(skill_name)
|
||||
skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)
|
||||
|
||||
skill_md_path = skill_dir / "SKILL.md"
|
||||
try:
|
||||
skill_md_path.write_text(skill_content)
|
||||
print("[OK] Created SKILL.md")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating SKILL.md: {e}")
|
||||
return None
|
||||
|
||||
# Create agents/openai.yaml
|
||||
try:
|
||||
result = write_openai_yaml(skill_dir, skill_name, interface_overrides)
|
||||
if not result:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating agents/openai.yaml: {e}")
|
||||
return None
|
||||
|
||||
# Create resource directories if requested
|
||||
if resources:
|
||||
try:
|
||||
create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating resource directories: {e}")
|
||||
return None
|
||||
|
||||
# Print next steps
|
||||
print(f"\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}")
|
||||
print("\nNext steps:")
|
||||
print("1. Edit SKILL.md to complete the TODO items and update the description")
|
||||
if resources:
|
||||
if include_examples:
|
||||
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
||||
else:
|
||||
print("2. Add resources to scripts/, references/, and assets/ as needed")
|
||||
else:
|
||||
print("2. Create resource directories only if needed (scripts/, references/, assets/)")
|
||||
print("3. Update agents/openai.yaml if the UI metadata should differ")
|
||||
print("4. Run the validator when ready to check the skill structure")
|
||||
print(
|
||||
"5. Forward-test complex skills with realistic user requests to ensure they work as intended"
|
||||
)
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a new skill directory with a SKILL.md template.",
|
||||
)
|
||||
parser.add_argument("skill_name", help="Skill name (normalized to hyphen-case)")
|
||||
parser.add_argument("--path", required=True, help="Output directory for the skill")
|
||||
parser.add_argument(
|
||||
"--resources",
|
||||
default="",
|
||||
help="Comma-separated list: scripts,references,assets",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--examples",
|
||||
action="store_true",
|
||||
help="Create example files inside the selected resource directories",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interface",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Interface override in key=value format (repeatable)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
raw_skill_name = args.skill_name
|
||||
skill_name = normalize_skill_name(raw_skill_name)
|
||||
if not skill_name:
|
||||
print("[ERROR] Skill name must include at least one letter or digit.")
|
||||
sys.exit(1)
|
||||
if len(skill_name) > MAX_SKILL_NAME_LENGTH:
|
||||
print(
|
||||
f"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). "
|
||||
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters."
|
||||
)
|
||||
sys.exit(1)
|
||||
if skill_name != raw_skill_name:
|
||||
print(f"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.")
|
||||
|
||||
resources = parse_resources(args.resources)
|
||||
if args.examples and not resources:
|
||||
print("[ERROR] --examples requires --resources to be set.")
|
||||
sys.exit(1)
|
||||
|
||||
path = args.path
|
||||
|
||||
print(f"Initializing skill: {skill_name}")
|
||||
print(f" Location: {path}")
|
||||
if resources:
|
||||
print(f" Resources: {', '.join(resources)}")
|
||||
if args.examples:
|
||||
print(" Examples: enabled")
|
||||
else:
|
||||
print(" Resources: none (create as needed)")
|
||||
print()
|
||||
|
||||
result = init_skill(skill_name, path, resources, args.examples, args.interface)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick validation script for skills - minimal version
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
MAX_SKILL_NAME_LENGTH = 64
|
||||
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""Basic validation of a skill"""
|
||||
skill_path = Path(skill_path)
|
||||
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
content = skill_md.read_text()
|
||||
if not content.startswith("---"):
|
||||
return False, "No YAML frontmatter found"
|
||||
|
||||
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not match:
|
||||
return False, "Invalid frontmatter format"
|
||||
|
||||
frontmatter_text = match.group(1)
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
if not isinstance(frontmatter, dict):
|
||||
return False, "Frontmatter must be a YAML dictionary"
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML in frontmatter: {e}"
|
||||
|
||||
allowed_properties = {"name", "description", "license", "allowed-tools", "metadata"}
|
||||
|
||||
unexpected_keys = set(frontmatter.keys()) - allowed_properties
|
||||
if unexpected_keys:
|
||||
allowed = ", ".join(sorted(allowed_properties))
|
||||
unexpected = ", ".join(sorted(unexpected_keys))
|
||||
return (
|
||||
False,
|
||||
f"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}",
|
||||
)
|
||||
|
||||
if "name" not in frontmatter:
|
||||
return False, "Missing 'name' in frontmatter"
|
||||
if "description" not in frontmatter:
|
||||
return False, "Missing 'description' in frontmatter"
|
||||
|
||||
name = frontmatter.get("name", "")
|
||||
if not isinstance(name, str):
|
||||
return False, f"Name must be a string, got {type(name).__name__}"
|
||||
name = name.strip()
|
||||
if name:
|
||||
if not re.match(r"^[a-z0-9-]+$", name):
|
||||
return (
|
||||
False,
|
||||
f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)",
|
||||
)
|
||||
if name.startswith("-") or name.endswith("-") or "--" in name:
|
||||
return (
|
||||
False,
|
||||
f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens",
|
||||
)
|
||||
if len(name) > MAX_SKILL_NAME_LENGTH:
|
||||
return (
|
||||
False,
|
||||
f"Name is too long ({len(name)} characters). "
|
||||
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters.",
|
||||
)
|
||||
|
||||
description = frontmatter.get("description", "")
|
||||
if not isinstance(description, str):
|
||||
return False, f"Description must be a string, got {type(description).__name__}"
|
||||
description = description.strip()
|
||||
if description:
|
||||
if "<" in description or ">" in description:
|
||||
return False, "Description cannot contain angle brackets (< or >)"
|
||||
if len(description) > 1024:
|
||||
return (
|
||||
False,
|
||||
f"Description is too long ({len(description)} characters). Maximum is 1024 characters.",
|
||||
)
|
||||
|
||||
return True, "Skill is valid!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python quick_validate.py <skill_directory>")
|
||||
sys.exit(1)
|
||||
|
||||
valid, message = validate_skill(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(0 if valid else 1)
|
||||
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
name: skill-installer
|
||||
description: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).
|
||||
metadata:
|
||||
short-description: Install curated skills from openai/skills or other repos
|
||||
---
|
||||
|
||||
# Skill Installer
|
||||
|
||||
Helps install skills. By default these are from https://github.com/openai/skills/tree/main/skills/.curated, but users can also provide other locations. Experimental skills live in https://github.com/openai/skills/tree/main/skills/.experimental and can be installed the same way.
|
||||
|
||||
Use the helper scripts based on the task:
|
||||
- List skills when the user asks what is available, or if the user uses this skill without specifying what to do. Default listing is `.curated`, but you can pass `--path skills/.experimental` when they ask about experimental skills.
|
||||
- Install from the curated list when the user provides a skill name.
|
||||
- Install from another repo when the user provides a GitHub repo/path (including private repos).
|
||||
|
||||
Install skills with the helper scripts.
|
||||
|
||||
## Communication
|
||||
|
||||
When listing skills, output approximately as follows, depending on the context of the user's request. If they ask about experimental skills, list from `.experimental` instead of `.curated` and label the source accordingly:
|
||||
"""
|
||||
Skills from {repo}:
|
||||
1. skill-1
|
||||
2. skill-2 (already installed)
|
||||
3. ...
|
||||
Which ones would you like installed?
|
||||
"""
|
||||
|
||||
After installing a skill, tell the user: "Restart Codex to pick up new skills."
|
||||
|
||||
## Scripts
|
||||
|
||||
All of these scripts use network, so when running in the sandbox, request escalation when running them.
|
||||
|
||||
- `scripts/list-skills.py` (prints skills list with installed annotations)
|
||||
- `scripts/list-skills.py --format json`
|
||||
- Example (experimental list): `scripts/list-skills.py --path skills/.experimental`
|
||||
- `scripts/install-skill-from-github.py --repo <owner>/<repo> --path <path/to/skill> [<path/to/skill> ...]`
|
||||
- `scripts/install-skill-from-github.py --url https://github.com/<owner>/<repo>/tree/<ref>/<path>`
|
||||
- Example (experimental skill): `scripts/install-skill-from-github.py --repo openai/skills --path skills/.experimental/<skill-name>`
|
||||
|
||||
## Behavior and Options
|
||||
|
||||
- Defaults to direct download for public GitHub repos.
|
||||
- If download fails with auth/permission errors, falls back to git sparse checkout.
|
||||
- Aborts if the destination skill directory already exists.
|
||||
- Installs into `$CODEX_HOME/skills/<skill-name>` (defaults to `~/.codex/skills`).
|
||||
- Multiple `--path` values install multiple skills in one run, each named from the path basename unless `--name` is supplied.
|
||||
- Options: `--ref <ref>` (default `main`), `--dest <path>`, `--method auto|download|git`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Curated listing is fetched from `https://github.com/openai/skills/tree/main/skills/.curated` via the GitHub API. If it is unavailable, explain the error and exit.
|
||||
- Private GitHub repos can be accessed via existing git credentials or optional `GITHUB_TOKEN`/`GH_TOKEN` for download.
|
||||
- Git fallback tries HTTPS first, then SSH.
|
||||
- The skills at https://github.com/openai/skills/tree/main/skills/.system are preinstalled, so no need to help users install those. If they ask, just explain this. If they insist, you can download and overwrite.
|
||||
- Installed annotations come from `$CODEX_HOME/skills`.
|
||||
@@ -1,5 +0,0 @@
|
||||
interface:
|
||||
display_name: "Skill Installer"
|
||||
short_description: "Install curated skills from openai/skills or other repos"
|
||||
icon_small: "./assets/skill-installer-small.svg"
|
||||
icon_large: "./assets/skill-installer.png"
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill="#0D0D0D" d="M2.145 3.959a2.033 2.033 0 0 1 2.022-1.824h5.966c.551 0 .997 0 1.357.029.367.03.692.093.993.246l.174.098c.397.243.72.593.932 1.01l.053.114c.116.269.168.557.194.878.03.36.03.805.03 1.357v4.3a2.365 2.365 0 0 1-2.366 2.365h-1.312a2.198 2.198 0 0 1-4.377 0H4.167A2.032 2.032 0 0 1 2.135 10.5V9.333l.004-.088A.865.865 0 0 1 3 8.468l.116-.006A1.135 1.135 0 0 0 3 6.199a.865.865 0 0 1-.865-.864V4.167l.01-.208Zm1.054 1.186a2.198 2.198 0 0 1 0 4.376v.98c0 .534.433.967.968.967H6l.089.004a.866.866 0 0 1 .776.861 1.135 1.135 0 0 0 2.27 0c0-.478.387-.865.865-.865h1.5c.719 0 1.301-.583 1.301-1.301v-4.3c0-.57 0-.964-.025-1.27a1.933 1.933 0 0 0-.09-.493L12.642 4a1.47 1.47 0 0 0-.541-.585l-.102-.056c-.126-.065-.295-.11-.596-.135a17.31 17.31 0 0 0-1.27-.025H4.167a.968.968 0 0 0-.968.968v.978Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 923 B |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared GitHub helpers for skill install scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
|
||||
def github_request(url: str, user_agent: str) -> bytes:
|
||||
headers = {"User-Agent": user_agent}
|
||||
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def github_api_contents_url(repo: str, path: str, ref: str) -> str:
|
||||
return f"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}"
|
||||
@@ -1,308 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install a skill from a GitHub repo path into $CODEX_HOME/skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
|
||||
from github_utils import github_request
|
||||
DEFAULT_REF = "main"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Args:
|
||||
url: str | None = None
|
||||
repo: str | None = None
|
||||
path: list[str] | None = None
|
||||
ref: str = DEFAULT_REF
|
||||
dest: str | None = None
|
||||
name: str | None = None
|
||||
method: str = "auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Source:
|
||||
owner: str
|
||||
repo: str
|
||||
ref: str
|
||||
paths: list[str]
|
||||
repo_url: str | None = None
|
||||
|
||||
|
||||
class InstallError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _codex_home() -> str:
|
||||
return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
|
||||
|
||||
|
||||
def _tmp_root() -> str:
|
||||
base = os.path.join(tempfile.gettempdir(), "codex")
|
||||
os.makedirs(base, exist_ok=True)
|
||||
return base
|
||||
|
||||
|
||||
def _request(url: str) -> bytes:
|
||||
return github_request(url, "codex-skill-install")
|
||||
|
||||
|
||||
def _parse_github_url(url: str, default_ref: str) -> tuple[str, str, str, str | None]:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.netloc != "github.com":
|
||||
raise InstallError("Only GitHub URLs are supported for download mode.")
|
||||
parts = [p for p in parsed.path.split("/") if p]
|
||||
if len(parts) < 2:
|
||||
raise InstallError("Invalid GitHub URL.")
|
||||
owner, repo = parts[0], parts[1]
|
||||
ref = default_ref
|
||||
subpath = ""
|
||||
if len(parts) > 2:
|
||||
if parts[2] in ("tree", "blob"):
|
||||
if len(parts) < 4:
|
||||
raise InstallError("GitHub URL missing ref or path.")
|
||||
ref = parts[3]
|
||||
subpath = "/".join(parts[4:])
|
||||
else:
|
||||
subpath = "/".join(parts[2:])
|
||||
return owner, repo, ref, subpath or None
|
||||
|
||||
|
||||
def _download_repo_zip(owner: str, repo: str, ref: str, dest_dir: str) -> str:
|
||||
zip_url = f"https://codeload.github.com/{owner}/{repo}/zip/{ref}"
|
||||
zip_path = os.path.join(dest_dir, "repo.zip")
|
||||
try:
|
||||
payload = _request(zip_url)
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise InstallError(f"Download failed: HTTP {exc.code}") from exc
|
||||
with open(zip_path, "wb") as file_handle:
|
||||
file_handle.write(payload)
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_file:
|
||||
_safe_extract_zip(zip_file, dest_dir)
|
||||
top_levels = {name.split("/")[0] for name in zip_file.namelist() if name}
|
||||
if not top_levels:
|
||||
raise InstallError("Downloaded archive was empty.")
|
||||
if len(top_levels) != 1:
|
||||
raise InstallError("Unexpected archive layout.")
|
||||
return os.path.join(dest_dir, next(iter(top_levels)))
|
||||
|
||||
|
||||
def _run_git(args: list[str]) -> None:
|
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if result.returncode != 0:
|
||||
raise InstallError(result.stderr.strip() or "Git command failed.")
|
||||
|
||||
|
||||
def _safe_extract_zip(zip_file: zipfile.ZipFile, dest_dir: str) -> None:
|
||||
dest_root = os.path.realpath(dest_dir)
|
||||
for info in zip_file.infolist():
|
||||
extracted_path = os.path.realpath(os.path.join(dest_dir, info.filename))
|
||||
if extracted_path == dest_root or extracted_path.startswith(dest_root + os.sep):
|
||||
continue
|
||||
raise InstallError("Archive contains files outside the destination.")
|
||||
zip_file.extractall(dest_dir)
|
||||
|
||||
|
||||
def _validate_relative_path(path: str) -> None:
|
||||
if os.path.isabs(path) or os.path.normpath(path).startswith(".."):
|
||||
raise InstallError("Skill path must be a relative path inside the repo.")
|
||||
|
||||
|
||||
def _validate_skill_name(name: str) -> None:
|
||||
altsep = os.path.altsep
|
||||
if not name or os.path.sep in name or (altsep and altsep in name):
|
||||
raise InstallError("Skill name must be a single path segment.")
|
||||
if name in (".", ".."):
|
||||
raise InstallError("Invalid skill name.")
|
||||
|
||||
|
||||
def _git_sparse_checkout(repo_url: str, ref: str, paths: list[str], dest_dir: str) -> str:
|
||||
repo_dir = os.path.join(dest_dir, "repo")
|
||||
clone_cmd = [
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"--depth",
|
||||
"1",
|
||||
"--sparse",
|
||||
"--single-branch",
|
||||
"--branch",
|
||||
ref,
|
||||
repo_url,
|
||||
repo_dir,
|
||||
]
|
||||
try:
|
||||
_run_git(clone_cmd)
|
||||
except InstallError:
|
||||
_run_git(
|
||||
[
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"--depth",
|
||||
"1",
|
||||
"--sparse",
|
||||
"--single-branch",
|
||||
repo_url,
|
||||
repo_dir,
|
||||
]
|
||||
)
|
||||
_run_git(["git", "-C", repo_dir, "sparse-checkout", "set", *paths])
|
||||
_run_git(["git", "-C", repo_dir, "checkout", ref])
|
||||
return repo_dir
|
||||
|
||||
|
||||
def _validate_skill(path: str) -> None:
|
||||
if not os.path.isdir(path):
|
||||
raise InstallError(f"Skill path not found: {path}")
|
||||
skill_md = os.path.join(path, "SKILL.md")
|
||||
if not os.path.isfile(skill_md):
|
||||
raise InstallError("SKILL.md not found in selected skill directory.")
|
||||
|
||||
|
||||
def _copy_skill(src: str, dest_dir: str) -> None:
|
||||
os.makedirs(os.path.dirname(dest_dir), exist_ok=True)
|
||||
if os.path.exists(dest_dir):
|
||||
raise InstallError(f"Destination already exists: {dest_dir}")
|
||||
shutil.copytree(src, dest_dir)
|
||||
|
||||
|
||||
def _build_repo_url(owner: str, repo: str) -> str:
|
||||
return f"https://github.com/{owner}/{repo}.git"
|
||||
|
||||
|
||||
def _build_repo_ssh(owner: str, repo: str) -> str:
|
||||
return f"git@github.com:{owner}/{repo}.git"
|
||||
|
||||
|
||||
def _prepare_repo(source: Source, method: str, tmp_dir: str) -> str:
|
||||
if method in ("download", "auto"):
|
||||
try:
|
||||
return _download_repo_zip(source.owner, source.repo, source.ref, tmp_dir)
|
||||
except InstallError as exc:
|
||||
if method == "download":
|
||||
raise
|
||||
err_msg = str(exc)
|
||||
if "HTTP 401" in err_msg or "HTTP 403" in err_msg or "HTTP 404" in err_msg:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if method in ("git", "auto"):
|
||||
repo_url = source.repo_url or _build_repo_url(source.owner, source.repo)
|
||||
try:
|
||||
return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)
|
||||
except InstallError:
|
||||
repo_url = _build_repo_ssh(source.owner, source.repo)
|
||||
return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)
|
||||
raise InstallError("Unsupported method.")
|
||||
|
||||
|
||||
def _resolve_source(args: Args) -> Source:
|
||||
if args.url:
|
||||
owner, repo, ref, url_path = _parse_github_url(args.url, args.ref)
|
||||
if args.path is not None:
|
||||
paths = list(args.path)
|
||||
elif url_path:
|
||||
paths = [url_path]
|
||||
else:
|
||||
paths = []
|
||||
if not paths:
|
||||
raise InstallError("Missing --path for GitHub URL.")
|
||||
return Source(owner=owner, repo=repo, ref=ref, paths=paths)
|
||||
|
||||
if not args.repo:
|
||||
raise InstallError("Provide --repo or --url.")
|
||||
if "://" in args.repo:
|
||||
return _resolve_source(
|
||||
Args(url=args.repo, repo=None, path=args.path, ref=args.ref)
|
||||
)
|
||||
|
||||
repo_parts = [p for p in args.repo.split("/") if p]
|
||||
if len(repo_parts) != 2:
|
||||
raise InstallError("--repo must be in owner/repo format.")
|
||||
if not args.path:
|
||||
raise InstallError("Missing --path for --repo.")
|
||||
paths = list(args.path)
|
||||
return Source(
|
||||
owner=repo_parts[0],
|
||||
repo=repo_parts[1],
|
||||
ref=args.ref,
|
||||
paths=paths,
|
||||
)
|
||||
|
||||
|
||||
def _default_dest() -> str:
|
||||
return os.path.join(_codex_home(), "skills")
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> Args:
|
||||
parser = argparse.ArgumentParser(description="Install a skill from GitHub.")
|
||||
parser.add_argument("--repo", help="owner/repo")
|
||||
parser.add_argument("--url", help="https://github.com/owner/repo[/tree/ref/path]")
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
nargs="+",
|
||||
help="Path(s) to skill(s) inside repo",
|
||||
)
|
||||
parser.add_argument("--ref", default=DEFAULT_REF)
|
||||
parser.add_argument("--dest", help="Destination skills directory")
|
||||
parser.add_argument(
|
||||
"--name", help="Destination skill name (defaults to basename of path)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
choices=["auto", "download", "git"],
|
||||
default="auto",
|
||||
)
|
||||
return parser.parse_args(argv, namespace=Args())
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = _parse_args(argv)
|
||||
try:
|
||||
source = _resolve_source(args)
|
||||
source.ref = source.ref or args.ref
|
||||
if not source.paths:
|
||||
raise InstallError("No skill paths provided.")
|
||||
for path in source.paths:
|
||||
_validate_relative_path(path)
|
||||
dest_root = args.dest or _default_dest()
|
||||
tmp_dir = tempfile.mkdtemp(prefix="skill-install-", dir=_tmp_root())
|
||||
try:
|
||||
repo_root = _prepare_repo(source, args.method, tmp_dir)
|
||||
installed = []
|
||||
for path in source.paths:
|
||||
skill_name = args.name if len(source.paths) == 1 else None
|
||||
skill_name = skill_name or os.path.basename(path.rstrip("/"))
|
||||
_validate_skill_name(skill_name)
|
||||
if not skill_name:
|
||||
raise InstallError("Unable to derive skill name.")
|
||||
dest_dir = os.path.join(dest_root, skill_name)
|
||||
if os.path.exists(dest_dir):
|
||||
raise InstallError(f"Destination already exists: {dest_dir}")
|
||||
skill_src = os.path.join(repo_root, path)
|
||||
_validate_skill(skill_src)
|
||||
_copy_skill(skill_src, dest_dir)
|
||||
installed.append((skill_name, dest_dir))
|
||||
finally:
|
||||
if os.path.isdir(tmp_dir):
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
for skill_name, dest_dir in installed:
|
||||
print(f"Installed {skill_name} to {dest_dir}")
|
||||
return 0
|
||||
except InstallError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List skills from a GitHub repo path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
|
||||
from github_utils import github_api_contents_url, github_request
|
||||
|
||||
DEFAULT_REPO = "openai/skills"
|
||||
DEFAULT_PATH = "skills/.curated"
|
||||
DEFAULT_REF = "main"
|
||||
|
||||
|
||||
class ListError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Args(argparse.Namespace):
|
||||
repo: str
|
||||
path: str
|
||||
ref: str
|
||||
format: str
|
||||
|
||||
|
||||
def _request(url: str) -> bytes:
|
||||
return github_request(url, "codex-skill-list")
|
||||
|
||||
|
||||
def _codex_home() -> str:
|
||||
return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
|
||||
|
||||
|
||||
def _installed_skills() -> set[str]:
|
||||
root = os.path.join(_codex_home(), "skills")
|
||||
if not os.path.isdir(root):
|
||||
return set()
|
||||
entries = set()
|
||||
for name in os.listdir(root):
|
||||
path = os.path.join(root, name)
|
||||
if os.path.isdir(path):
|
||||
entries.add(name)
|
||||
return entries
|
||||
|
||||
|
||||
def _list_skills(repo: str, path: str, ref: str) -> list[str]:
|
||||
api_url = github_api_contents_url(repo, path, ref)
|
||||
try:
|
||||
payload = _request(api_url)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
raise ListError(
|
||||
"Skills path not found: "
|
||||
f"https://github.com/{repo}/tree/{ref}/{path}"
|
||||
) from exc
|
||||
raise ListError(f"Failed to fetch skills: HTTP {exc.code}") from exc
|
||||
data = json.loads(payload.decode("utf-8"))
|
||||
if not isinstance(data, list):
|
||||
raise ListError("Unexpected skills listing response.")
|
||||
skills = [item["name"] for item in data if item.get("type") == "dir"]
|
||||
return sorted(skills)
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> Args:
|
||||
parser = argparse.ArgumentParser(description="List skills.")
|
||||
parser.add_argument("--repo", default=DEFAULT_REPO)
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
default=DEFAULT_PATH,
|
||||
help="Repo path to list (default: skills/.curated)",
|
||||
)
|
||||
parser.add_argument("--ref", default=DEFAULT_REF)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["text", "json"],
|
||||
default="text",
|
||||
help="Output format",
|
||||
)
|
||||
return parser.parse_args(argv, namespace=Args())
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = _parse_args(argv)
|
||||
try:
|
||||
skills = _list_skills(args.repo, args.path, args.ref)
|
||||
installed = _installed_skills()
|
||||
if args.format == "json":
|
||||
payload = [
|
||||
{"name": name, "installed": name in installed} for name in skills
|
||||
]
|
||||
print(json.dumps(payload))
|
||||
else:
|
||||
for idx, name in enumerate(skills, start=1):
|
||||
suffix = " (already installed)" if name in installed else ""
|
||||
print(f"{idx}. {name}{suffix}")
|
||||
return 0
|
||||
except ListError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -112,6 +112,8 @@ Recommended sequence:
|
||||
Machine-specific note:
|
||||
|
||||
- Project-local `.worktrees/*/target` directories are common cleanup wins on this machine and are easy to miss with the old hard-coded workflow.
|
||||
- `cargo-sweep` is installed through the NixOS `code.nix` package set, but stale manually-installed binaries under `~/.cargo/bin` can shadow `/run/current-system/sw/bin/cargo-sweep`. If `cargo sweep` fails with a missing loader or `No such file or directory`, run `type -a cargo-sweep` and remove the stale `~/.cargo/bin/cargo-sweep` entry.
|
||||
- `nixos/imalison.nix` defines a daily user timer, `cargo-sweep-rust-targets.timer`, that runs `cargo-sweep sweep -r --hidden --maxsize 15GB` across `/home/imalison/Projects`, `/home/imalison/org`, and `/home/imalison/dotfiles`.
|
||||
|
||||
## Step 4: Investigation with `ncdu` and `du`
|
||||
|
||||
@@ -137,6 +139,18 @@ If `ncdu` is missing, use:
|
||||
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
|
||||
@@ -147,9 +161,16 @@ 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
|
||||
|
||||
@@ -183,6 +204,25 @@ 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
|
||||
|
||||
@@ -33,6 +33,9 @@ digraph unsubscribe_check {
|
||||
- Do not ask a kickoff question like "should I start now?".
|
||||
- Default scan window is `newer_than:7d` unless the user already specified a different range.
|
||||
- Only ask a follow-up question before starting if required information is missing and execution would otherwise be blocked.
|
||||
- Default user preference: they generally do not want subscription-style email in their inbox.
|
||||
- For obvious marketing/newsletter/digest mail with a working unsubscribe path, unsubscribe by default without asking for confirmation first.
|
||||
- Still ask first for borderline cases such as creator subscriptions, professional communities, event platforms, or anything that appears transactional/security-sensitive.
|
||||
|
||||
## How to Scan
|
||||
|
||||
@@ -42,6 +45,8 @@ digraph unsubscribe_check {
|
||||
- **Clearly unsubscribeable**: marketing, promos, digests user never engages with
|
||||
- **Ask user**: newsletters, community content, event platforms (might be wanted)
|
||||
|
||||
When the user's standing preference is to keep subscriptions out of the inbox, treat the **Clearly unsubscribeable** bucket as auto-actionable.
|
||||
|
||||
## Unsubscribe Execution
|
||||
|
||||
For each confirmed sender, do ALL of these:
|
||||
@@ -95,6 +100,7 @@ gws gmail users messages batchModify \
|
||||
- Community digests the user doesn't engage with
|
||||
- Financial marketing (not transactional alerts)
|
||||
- "Your weekly/daily/monthly" summaries
|
||||
- Messages with explicit unsubscribe/manage-preferences links whose primary purpose is promotional or newsletter delivery
|
||||
|
||||
## Signals to NOT Auto-Unsubscribe (Ask First)
|
||||
|
||||
|
||||
@@ -9,25 +9,28 @@ How the taffybar ecosystem packages are consumed by the NixOS configuration thro
|
||||
|
||||
See also: `taffybar-ecosystem-release` for the package dependency graph, release workflow, and Hackage publishing.
|
||||
|
||||
## The Three-Layer Flake Chain
|
||||
## The Flake Chain
|
||||
|
||||
The NixOS system build pulls in taffybar through three nested flake.nix files:
|
||||
The NixOS system build pulls in taffybar through the personal
|
||||
`imalison-taffybar` config flake. The top-level NixOS flake should not declare
|
||||
or override a direct `taffybar` input; the config flake owns its taffybar
|
||||
version.
|
||||
|
||||
```
|
||||
nixos/flake.nix (top — `just switch` reads this)
|
||||
│ ├── taffybar path:.../taffybar/taffybar
|
||||
│ ├── imalison-taffybar path:../dotfiles/config/taffybar
|
||||
│ └── gtk-sni-tray, gtk-strut, etc. (GitHub inputs)
|
||||
nixos/flake.nix (top - `just switch` reads this)
|
||||
│ └── imalison-taffybar path:../dotfiles/config/taffybar
|
||||
│
|
||||
dotfiles/config/taffybar/flake.nix (middle — imalison-taffybar config)
|
||||
dotfiles/config/taffybar/flake.nix (middle - imalison-taffybar config)
|
||||
│ ├── taffybar path:.../taffybar/taffybar
|
||||
│ └── gtk-sni-tray, gtk-strut, etc. (GitHub inputs)
|
||||
│
|
||||
dotfiles/config/taffybar/taffybar/flake.nix (bottom — taffybar library)
|
||||
dotfiles/config/taffybar/taffybar/flake.nix (bottom - taffybar library)
|
||||
│ └── gtk-sni-tray, gtk-strut, etc. (flake = false GitHub inputs)
|
||||
```
|
||||
|
||||
All three flakes declare their own top-level inputs for the ecosystem packages and use `follows` to keep versions consistent within each layer.
|
||||
The NixOS layer may make `imalison-taffybar` follow shared inputs such as
|
||||
`nixpkgs`, `flake-utils`, and `xmonad`, but it should not set
|
||||
`imalison-taffybar.inputs.taffybar.follows`.
|
||||
|
||||
## Why Bottom-Up Updates Matter
|
||||
|
||||
@@ -43,14 +46,14 @@ cd ~/.config/taffybar/taffybar && nix flake update <pkg>
|
||||
cd ~/.config/taffybar && nix flake update <pkg> taffybar
|
||||
|
||||
# Top:
|
||||
cd ~/dotfiles/nixos && nix flake update <pkg> imalison-taffybar taffybar
|
||||
cd ~/dotfiles/nixos && nix flake update imalison-taffybar
|
||||
```
|
||||
|
||||
Not every change requires touching all three layers. Think about which flake.lock files actually contain stale references:
|
||||
|
||||
- Changed **taffybar itself** — it's the bottom layer, so start at the middle (`nix flake update taffybar`) then the top.
|
||||
- Changed **taffybar itself** — it's owned by the config flake, so start at the middle (`nix flake update taffybar`) then update `imalison-taffybar` at the top.
|
||||
- Changed a **leaf ecosystem package** (e.g. gtk-strut) — start at the bottom since taffybar's flake.lock references it, then cascade up.
|
||||
- The nixos flake also has **direct GitHub inputs** for ecosystem packages with `follows` overrides. Updating those at the top level may be sufficient if nothing changed in the middle/bottom flake.lock files themselves.
|
||||
- The nixos flake can still have unrelated direct inputs such as `kanshi-sni`. Do not add a top-level `taffybar` input just to control the config flake's taffybar source.
|
||||
|
||||
## Rebuilding
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"iterm2",
|
||||
"java",
|
||||
"jumpcut",
|
||||
"karabiner",
|
||||
"libreoffice",
|
||||
"macpass",
|
||||
"mirrordisplays",
|
||||
@@ -170,6 +169,7 @@
|
||||
"tig",
|
||||
"tmate",
|
||||
"tmux",
|
||||
"zellij",
|
||||
"unoconv",
|
||||
"vim",
|
||||
"w3m",
|
||||
|
||||
6
dotfiles/claude/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*
|
||||
!.gitignore
|
||||
!CLAUDE.md
|
||||
!settings.json
|
||||
!settings.local.json
|
||||
!settings.local.json.example
|
||||
@@ -16,5 +16,6 @@
|
||||
"agent-browser@agent-browser": true
|
||||
},
|
||||
"effortLevel": "high",
|
||||
"skipDangerousModePermissionPrompt": true
|
||||
"skipDangerousModePermissionPrompt": true,
|
||||
"remoteControlAtStartup": true
|
||||
}
|
||||
|
||||
8
dotfiles/codex/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
*
|
||||
!.gitignore
|
||||
!AGENTS.md
|
||||
!config.toml
|
||||
!skills
|
||||
|
||||
# Legacy generated/local Codex state under this repo stays ignored. Active
|
||||
# host-local Codex fragments now live under ~/.codex.
|
||||
@@ -1,122 +1,12 @@
|
||||
model = "gpt-5.4"
|
||||
model = "gpt-5.5"
|
||||
model_reasoning_effort = "high"
|
||||
service_tier = "fast"
|
||||
personality = "pragmatic"
|
||||
notify = ["/Users/kat/.codex/plugins/cache/openai-bundled/computer-use/1.0.750/Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient", "turn-ended"]
|
||||
suppress_unstable_features_warning = true
|
||||
|
||||
[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."/Users/kat/dotfiles"]
|
||||
trust_level = "trusted"
|
||||
|
||||
[projects."/Users/kat"]
|
||||
trust_level = "trusted"
|
||||
|
||||
[notice]
|
||||
hide_gpt5_1_migration_prompt = true
|
||||
"hide_gpt-5.1-codex-max_migration_prompt" = true
|
||||
|
||||
[notice.model_migrations]
|
||||
"gpt-5.2" = "gpt-5.2-codex"
|
||||
# Portable Codex defaults. Home Manager regenerates ~/.codex/config.toml from
|
||||
# this file, ~/.codex/config.local.toml, and Codex-owned sections preserved in
|
||||
# ~/.codex/config.local-state.toml.
|
||||
|
||||
[mcp_servers.chrome-devtools]
|
||||
command = "npx"
|
||||
@@ -126,10 +16,6 @@ args = ["-y", "chrome-devtools-mcp@latest", "--auto-connect"]
|
||||
command = "npx"
|
||||
args = ["-y", "@google-cloud/observability-mcp"]
|
||||
|
||||
[mcp_servers.gmail]
|
||||
command = "nix"
|
||||
args = ["run", "/home/imalison/Projects/gmail-mcp#gmail-mcp-server"]
|
||||
|
||||
[mcp_servers.openaiDeveloperDocs]
|
||||
url = "https://developers.openai.com/mcp"
|
||||
|
||||
@@ -137,11 +23,9 @@ url = "https://developers.openai.com/mcp"
|
||||
unified_exec = true
|
||||
apps = true
|
||||
steer = true
|
||||
|
||||
[marketplaces.openai-bundled]
|
||||
last_updated = "2026-04-19T01:07:40Z"
|
||||
source_type = "local"
|
||||
source = "/Users/kat/.codex/.tmp/bundled-marketplaces/openai-bundled"
|
||||
goals = true
|
||||
fast_mode = true
|
||||
remote_control = true
|
||||
|
||||
[plugins."google-calendar@openai-curated"]
|
||||
enabled = true
|
||||
@@ -152,8 +36,20 @@ enabled = true
|
||||
[plugins."google-drive@openai-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."github@openai-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."computer-use@openai-bundled"]
|
||||
enabled = true
|
||||
|
||||
[plugins."github@openai-curated"]
|
||||
[plugins."documents@openai-primary-runtime"]
|
||||
enabled = true
|
||||
|
||||
[plugins."spreadsheets@openai-primary-runtime"]
|
||||
enabled = true
|
||||
|
||||
[plugins."presentations@openai-primary-runtime"]
|
||||
enabled = true
|
||||
|
||||
[plugins."browser-use@openai-bundled"]
|
||||
enabled = true
|
||||
|
||||
18
dotfiles/config/autorandr/mpg341cx-oled-240hz-solo/config
Normal file
@@ -0,0 +1,18 @@
|
||||
output HDMI-0
|
||||
off
|
||||
output DP-1
|
||||
off
|
||||
output DP-2
|
||||
off
|
||||
output DP-3
|
||||
off
|
||||
output DP-4
|
||||
off
|
||||
output DP-5
|
||||
off
|
||||
output DP-0
|
||||
crtc 0
|
||||
mode 3440x1440
|
||||
pos 0x0
|
||||
rate 240.00
|
||||
x-prop-non_desktop 0
|
||||
1
dotfiles/config/autorandr/mpg341cx-oled-240hz-solo/setup
Normal file
@@ -0,0 +1 @@
|
||||
DP-0 00ffffffffffff003669d04d0000000033210104b55022783bac05b04d3db7250f5054bfcf00714f81c0814081809500b300d1c00101e77c70a0d0a0295030203a0020513100001a023a801871382d40582c450020513100001e000000fd0c30f0919196010a202020202020000000fc004d50473334314358204f4c45440257020339f14901030204901211133f2309070783010000e2002a741a0000030330f000a066024f03f0000000000000e305e201e6060701664b00565e00a0a0a029503020350020513100001a6fc200a0a0a055503020350020513100001a00000000000000000000000000000000000000000000000000000000000000000000fc7012790300030150a2e300086f0d9f002f801f009f05b20031000900520101086f0d9f002f801f009f05540002000900b76901086f0d9f002f801f009f057600020009006f0502086f0d8f002f801f009f0563001d00090000000000000000000000000000000000000000000000000000000000000000000000000000001590
|
||||
@@ -1,90 +0,0 @@
|
||||
@binding-set gtk-emacs-text-entry
|
||||
{
|
||||
bind "<ctrl>b" { "move-cursor" (logical-positions, -1, 0) };
|
||||
bind "<shift><ctrl>b" { "move-cursor" (logical-positions, -1, 1) };
|
||||
bind "<ctrl>f" { "move-cursor" (logical-positions, 1, 0) };
|
||||
bind "<shift><ctrl>f" { "move-cursor" (logical-positions, 1, 1) };
|
||||
|
||||
bind "<alt>b" { "move-cursor" (words, -1, 0) };
|
||||
bind "<shift><alt>b" { "move-cursor" (words, -1, 1) };
|
||||
bind "<alt>f" { "move-cursor" (words, 1, 0) };
|
||||
bind "<shift><alt>f" { "move-cursor" (words, 1, 1) };
|
||||
|
||||
bind "<ctrl>a" { "move-cursor" (paragraph-ends, -1, 0) };
|
||||
bind "<shift><ctrl>a" { "move-cursor" (paragraph-ends, -1, 1) };
|
||||
bind "<ctrl>e" { "move-cursor" (paragraph-ends, 1, 0) };
|
||||
bind "<shift><ctrl>e" { "move-cursor" (paragraph-ends, 1, 1) };
|
||||
|
||||
bind "<ctrl>w" { "cut-clipboard" () };
|
||||
bind "<ctrl>y" { "paste-clipboard" () };
|
||||
|
||||
bind "<ctrl>d" { "delete-from-cursor" (chars, 1) };
|
||||
bind "<alt>d" { "delete-from-cursor" (word-ends, 1) };
|
||||
bind "<alt>BackSpace" { "delete-from-cursor" (word-ends, -1) };
|
||||
bind "<ctrl>k" { "delete-from-cursor" (paragraph-ends, 1) };
|
||||
|
||||
bind "<alt>space" { "delete-from-cursor" (whitespace, 1)
|
||||
"insert-at-cursor" (" ") };
|
||||
bind "<alt>KP_Space" { "delete-from-cursor" (whitespace, 1)
|
||||
"insert-at-cursor" (" ") };
|
||||
/*
|
||||
* Some non-Emacs keybindings people are attached to
|
||||
*/
|
||||
bind "<ctrl>u" { "move-cursor" (paragraph-ends, -1, 0)
|
||||
"delete-from-cursor" (paragraph-ends, 1) };
|
||||
|
||||
bind "<ctrl>h" { "delete-from-cursor" (chars, -1) };
|
||||
bind "<ctrl>w" { "delete-from-cursor" (word-ends, -1) };
|
||||
}
|
||||
|
||||
/*
|
||||
* Bindings for GtkTextView
|
||||
*/
|
||||
@binding-set gtk-emacs-text-view
|
||||
{
|
||||
bind "<ctrl>p" { "move-cursor" (display-lines, -1, 0) };
|
||||
bind "<shift><ctrl>p" { "move-cursor" (display-lines, -1, 1) };
|
||||
bind "<ctrl>n" { "move-cursor" (display-lines, 1, 0) };
|
||||
bind "<shift><ctrl>n" { "move-cursor" (display-lines, 1, 1) };
|
||||
|
||||
bind "<ctrl>space" { "set-anchor" () };
|
||||
bind "<ctrl>KP_Space" { "set-anchor" () };
|
||||
}
|
||||
|
||||
/*
|
||||
* Bindings for GtkTreeView
|
||||
*/
|
||||
@binding-set gtk-emacs-tree-view
|
||||
{
|
||||
bind "<ctrl>s" { "start-interactive-search" () };
|
||||
bind "<ctrl>f" { "move-cursor" (logical-positions, 1) };
|
||||
bind "<ctrl>b" { "move-cursor" (logical-positions, -1) };
|
||||
}
|
||||
|
||||
/*
|
||||
* Bindings for menus
|
||||
*/
|
||||
@binding-set gtk-emacs-menu
|
||||
{
|
||||
bind "<ctrl>n" { "move-current" (next) };
|
||||
bind "<ctrl>p" { "move-current" (prev) };
|
||||
bind "<ctrl>f" { "move-current" (child) };
|
||||
bind "<ctrl>b" { "move-current" (parent) };
|
||||
}
|
||||
|
||||
entry {
|
||||
-gtk-key-bindings: gtk-emacs-text-entry;
|
||||
}
|
||||
|
||||
textview {
|
||||
-gtk-key-bindings: gtk-emacs-text-entry, gtk-emacs-text-view;
|
||||
}
|
||||
|
||||
treeview {
|
||||
-gtk-key-bindings: gtk-emacs-tree-view;
|
||||
}
|
||||
|
||||
GtkMenuShell {
|
||||
-gtk-key-bindings: gtk-emacs-menu;
|
||||
}
|
||||
@import 'colors.css';
|
||||
@@ -1,17 +1,10 @@
|
||||
general {
|
||||
lock_cmd = pidof hyprlock || hyprlock
|
||||
before_sleep_cmd = loginctl lock-session
|
||||
after_sleep_cmd = hyprctl dispatch dpms on
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 600
|
||||
on-timeout = hypr-screensaver start
|
||||
on-resume = hypr-screensaver stop
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 900
|
||||
on-timeout = hypr-screensaver stop && hyprctl dispatch dpms off
|
||||
on-resume = hyprctl dispatch dpms on
|
||||
timeout = 300
|
||||
on-timeout = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver start
|
||||
on-resume = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop
|
||||
}
|
||||
|
||||
@@ -1,561 +0,0 @@
|
||||
# Hyprland Configuration
|
||||
# XMonad-like dynamic tiling using hy3 plugin
|
||||
# Based on XMonad configuration from xmonad.hs
|
||||
|
||||
# =============================================================================
|
||||
# PLUGINS (Hyprland pinned to 0.53.0 to match hy3)
|
||||
# =============================================================================
|
||||
# Load the plugin before parsing keybinds/layouts that depend on it
|
||||
plugin = /run/current-system/sw/lib/libhy3.so
|
||||
plugin = /run/current-system/sw/lib/libhyprexpo.so
|
||||
|
||||
# =============================================================================
|
||||
# MONITORS
|
||||
# =============================================================================
|
||||
monitor=,preferred,auto,1
|
||||
|
||||
# =============================================================================
|
||||
# PROGRAMS
|
||||
# =============================================================================
|
||||
$terminal = ghostty --gtk-single-instance=false
|
||||
$fileManager = dolphin
|
||||
$menu = rofi -show drun -show-icons
|
||||
$runMenu = rofi -show run
|
||||
|
||||
# =============================================================================
|
||||
# ENVIRONMENT VARIABLES
|
||||
# =============================================================================
|
||||
env = XCURSOR_SIZE,24
|
||||
env = QT_QPA_PLATFORMTHEME,qt5ct
|
||||
# Used by ~/.config/hypr/scripts/* to keep workspace IDs bounded.
|
||||
env = HYPR_MAX_WORKSPACE,9
|
||||
|
||||
# =============================================================================
|
||||
# INPUT CONFIGURATION
|
||||
# =============================================================================
|
||||
input {
|
||||
kb_layout = us
|
||||
kb_variant =
|
||||
kb_model =
|
||||
kb_options =
|
||||
kb_rules =
|
||||
|
||||
follow_mouse = 1
|
||||
|
||||
touchpad {
|
||||
natural_scroll = no
|
||||
}
|
||||
|
||||
sensitivity = 0
|
||||
}
|
||||
|
||||
# Cursor warping behavior
|
||||
cursor {
|
||||
persistent_warps = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# GENERAL SETTINGS
|
||||
# =============================================================================
|
||||
general {
|
||||
gaps_in = 5
|
||||
gaps_out = 10
|
||||
border_size = 0
|
||||
col.active_border = rgba(edb443ee) rgba(33ccffee) 45deg
|
||||
col.inactive_border = rgba(595959aa)
|
||||
|
||||
# Use hy3 layout for XMonad-like dynamic tiling
|
||||
layout = hy3
|
||||
|
||||
allow_tearing = false
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DECORATION
|
||||
# =============================================================================
|
||||
decoration {
|
||||
rounding = 5
|
||||
|
||||
blur {
|
||||
enabled = true
|
||||
size = 3
|
||||
passes = 1
|
||||
}
|
||||
|
||||
# Fade inactive windows (like XMonad's fadeInactive)
|
||||
active_opacity = 1.0
|
||||
inactive_opacity = 0.9
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# ANIMATIONS
|
||||
# =============================================================================
|
||||
animations {
|
||||
enabled = yes
|
||||
|
||||
# Hyprland supports bezier curves, not true spring physics.
|
||||
# Use a mild overshoot plus GNOME-like window animation style.
|
||||
bezier = overshoot, 0.05, 0.9, 0.1, 1.1
|
||||
bezier = smoothOut, 0.36, 1, 0.3, 1
|
||||
bezier = smoothInOut, 0.42, 0, 0.58, 1
|
||||
bezier = linear, 0, 0, 1, 1
|
||||
|
||||
# SPEED is in deciseconds (e.g. 6 == 600ms).
|
||||
animation = windows, 1, 6, overshoot, gnomed
|
||||
animation = windowsIn, 1, 6, overshoot, gnomed
|
||||
animation = windowsOut, 1, 5, smoothInOut, gnomed
|
||||
animation = windowsMove, 1, 6, smoothOut
|
||||
animation = border, 0
|
||||
animation = borderangle, 0
|
||||
animation = fade, 1, 5, smoothOut
|
||||
animation = workspaces, 1, 6, smoothOut, slidefade 15%
|
||||
animation = specialWorkspace, 1, 6, smoothOut, slidevert
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MASTER LAYOUT CONFIGURATION
|
||||
# =============================================================================
|
||||
master {
|
||||
new_status = slave
|
||||
mfact = 0.5
|
||||
orientation = left
|
||||
}
|
||||
|
||||
# Dwindle layout (alternative - binary tree like i3)
|
||||
dwindle {
|
||||
pseudotile = yes
|
||||
preserve_split = yes
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# WORKSPACE RULES (SMART GAPS)
|
||||
# =============================================================================
|
||||
# Replace no_gaps_when_only (removed in newer Hyprland)
|
||||
# Remove gaps when there's only one visible tiled window (ignore special workspaces)
|
||||
workspace = w[tv1]s[false], gapsout:0, gapsin:0
|
||||
workspace = f[1]s[false], gapsout:0, gapsin:0
|
||||
|
||||
# Group/tabbed window configuration (built-in alternative to hy3 tabs)
|
||||
group {
|
||||
col.border_active = rgba(edb443ff)
|
||||
col.border_inactive = rgba(091f2eff)
|
||||
|
||||
groupbar {
|
||||
enabled = true
|
||||
font_size = 12
|
||||
height = 22
|
||||
col.active = rgba(edb443ff)
|
||||
col.inactive = rgba(091f2eff)
|
||||
text_color = rgba(091f2eff)
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HY3/HYPREXPO PLUGIN CONFIG
|
||||
# =============================================================================
|
||||
plugin {
|
||||
hy3 {
|
||||
# Disable autotile to get XMonad-like manual control
|
||||
autotile {
|
||||
enable = false
|
||||
}
|
||||
|
||||
# Tab configuration
|
||||
tabs {
|
||||
height = 22
|
||||
padding = 6
|
||||
render_text = true
|
||||
text_font = "Sans"
|
||||
text_height = 10
|
||||
text_padding = 3
|
||||
col.active = rgba(edb443ff)
|
||||
col.inactive = rgba(091f2eff)
|
||||
col.urgent = rgba(ff0000ff)
|
||||
col.text.active = rgba(091f2eff)
|
||||
col.text.inactive = rgba(ffffffff)
|
||||
col.text.urgent = rgba(ffffffff)
|
||||
}
|
||||
}
|
||||
|
||||
hyprexpo {
|
||||
# Always include workspace 1 in the overview grid
|
||||
workspace_method = first 1
|
||||
# Only show workspaces with windows
|
||||
skip_empty = true
|
||||
# Show numeric workspace labels in the expo grid
|
||||
show_workspace_numbers = true
|
||||
# 3 columns -> 3x3 grid when 9 workspaces are visible
|
||||
columns = 3
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MISC
|
||||
# =============================================================================
|
||||
misc {
|
||||
force_default_wallpaper = 0
|
||||
disable_hyprland_logo = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# BINDS OPTIONS
|
||||
# =============================================================================
|
||||
binds {
|
||||
# Keep workspace history so "previous" can toggle back reliably.
|
||||
allow_workspace_cycles = true
|
||||
workspace_back_and_forth = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# WINDOW RULES
|
||||
# =============================================================================
|
||||
# Float dialogs
|
||||
windowrule = match:class ^()$, match:title ^()$, float on
|
||||
windowrule = match:title ^(Picture-in-Picture)$, float on
|
||||
windowrule = match:title ^(Open File)$, float on
|
||||
windowrule = match:title ^(Save File)$, float on
|
||||
windowrule = match:title ^(Confirm)$, float on
|
||||
|
||||
# Rumno OSD/notifications: treat as an overlay, not a "real" managed window.
|
||||
# (Matches both class and title because rumno may set either depending on backend.)
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, float on
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, pin on
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, center on
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, decorate off
|
||||
windowrule = match:class ^(.*[Rr]umno.*)$, no_shadow on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, float on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, pin on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, center on
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, decorate off
|
||||
windowrule = match:title ^(.*[Rr]umno.*)$, no_shadow on
|
||||
|
||||
# Scratchpad sizing handled by hyprscratch exec rules (see hyprland.nix)
|
||||
# Using hyprscratch rules instead of windowrule to avoid affecting child windows (e.g. Slack meets)
|
||||
|
||||
# =============================================================================
|
||||
# KEY BINDINGS
|
||||
# =============================================================================
|
||||
|
||||
# Modifier keys
|
||||
$mainMod = SUPER
|
||||
$modAlt = SUPER ALT
|
||||
$hyper = SUPER CTRL ALT
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Program Launching
|
||||
# -----------------------------------------------------------------------------
|
||||
bind = $mainMod, P, exec, $menu
|
||||
bind = $mainMod SHIFT, P, exec, $runMenu
|
||||
bind = $mainMod SHIFT, Return, exec, $terminal
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Overview (Hyprexpo)
|
||||
# -----------------------------------------------------------------------------
|
||||
bind = $mainMod, TAB, hyprexpo:expo, toggle
|
||||
bind = $mainMod SHIFT, TAB, hyprexpo:expo, bring
|
||||
bind = $mainMod, Q, killactive,
|
||||
bind = $mainMod SHIFT, C, killactive,
|
||||
bind = $mainMod SHIFT, Q, exit,
|
||||
# Emacs-everywhere (like XMonad's emacs-everywhere)
|
||||
bind = $mainMod, E, exec, emacsclient --eval '(emacs-everywhere)'
|
||||
bind = $mainMod, V, exec, wl-paste | xdotool type --file -
|
||||
|
||||
# Chrome/Browser (raise or spawn like XMonad's bindBringAndRaise)
|
||||
bind = $modAlt, C, exec, ~/.config/hypr/scripts/raise-or-run.sh google-chrome google-chrome-stable
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SCRATCHPADS (managed by hyprscratch daemon with auto-dismiss)
|
||||
# -----------------------------------------------------------------------------
|
||||
bind = $modAlt, E, exec, hyprscratch toggle element
|
||||
bind = $modAlt, G, exec, hyprscratch toggle gmail
|
||||
bind = $modAlt, H, exec, hyprscratch toggle htop
|
||||
bind = $modAlt, M, exec, hyprscratch toggle messages
|
||||
bind = $modAlt, K, exec, hyprscratch toggle slack
|
||||
bind = $modAlt, S, exec, hyprscratch toggle spotify
|
||||
bind = $modAlt, T, exec, hyprscratch toggle transmission
|
||||
bind = $modAlt, V, exec, hyprscratch toggle volume
|
||||
bind = $modAlt, grave, exec, hyprscratch toggle dropdown
|
||||
|
||||
# Hidden workspace (like XMonad's NSP)
|
||||
bind = $mainMod, X, movetoworkspace, special:NSP
|
||||
bind = $mainMod SHIFT, X, togglespecialworkspace, NSP
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DIRECTIONAL NAVIGATION (WASD - like XMonad Navigation2D)
|
||||
# Using hy3 dispatchers for proper tree-based navigation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Focus movement (Mod + WASD) - hy3:movefocus navigates the tree
|
||||
bind = $mainMod, W, hy3:movefocus, u
|
||||
bind = $mainMod, S, hy3:movefocus, d
|
||||
bind = $mainMod, A, hy3:movefocus, l
|
||||
bind = $mainMod, D, hy3:movefocus, r
|
||||
|
||||
# Move windows (Mod + Shift + WASD) - hy3:movewindow with once=true for swapping
|
||||
bind = $mainMod SHIFT, W, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh u once
|
||||
bind = $mainMod SHIFT, S, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh d once
|
||||
bind = $mainMod SHIFT, A, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh l once
|
||||
bind = $mainMod SHIFT, D, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh r once
|
||||
|
||||
# Resize windows (Mod + Ctrl + WASD)
|
||||
binde = $mainMod CTRL, W, resizeactive, 0 -50
|
||||
binde = $mainMod CTRL, S, resizeactive, 0 50
|
||||
binde = $mainMod CTRL, A, resizeactive, -50 0
|
||||
binde = $mainMod CTRL, D, resizeactive, 50 0
|
||||
|
||||
# Screen/Monitor focus (Hyper + WASD)
|
||||
bind = $hyper, W, focusmonitor, u
|
||||
bind = $hyper, S, focusmonitor, d
|
||||
bind = $hyper, A, focusmonitor, l
|
||||
bind = $hyper, D, focusmonitor, r
|
||||
|
||||
# Move window to monitor and follow (Hyper + Shift + WASD)
|
||||
bind = $hyper SHIFT, W, movewindow, mon:u
|
||||
bind = $hyper SHIFT, S, movewindow, mon:d
|
||||
bind = $hyper SHIFT, A, movewindow, mon:l
|
||||
bind = $hyper SHIFT, D, movewindow, mon:r
|
||||
|
||||
# Shift to empty workspace on screen direction (Hyper + Ctrl + WASD)
|
||||
# Like XMonad's shiftToEmptyOnScreen
|
||||
bind = $hyper CTRL, W, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh u
|
||||
bind = $hyper CTRL, S, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh d
|
||||
bind = $hyper CTRL, A, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh l
|
||||
bind = $hyper CTRL, D, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh r
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# LAYOUT CONTROL (XMonad-like with hy3)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Create groups with different orientations (like XMonad layouts)
|
||||
# hy3:makegroup creates a split/tab group from focused window
|
||||
bind = $mainMod, Space, hy3:changegroup, toggletab
|
||||
bind = $mainMod SHIFT, Space, hy3:changegroup, opposite
|
||||
|
||||
# Create specific group types
|
||||
bind = $mainMod, H, hy3:makegroup, h
|
||||
bind = $mainMod SHIFT, V, hy3:makegroup, v
|
||||
# Mod+Ctrl+Space mirrors Mod+Space (tabs instead of fullscreen)
|
||||
bind = $mainMod CTRL, Space, hy3:changegroup, toggletab
|
||||
|
||||
# Change group type (cycle h -> v -> tab)
|
||||
bind = $mainMod, slash, hy3:changegroup, h
|
||||
bind = $mainMod SHIFT, slash, hy3:changegroup, v
|
||||
|
||||
# Tab navigation (like XMonad's focus next/prev in tabbed)
|
||||
bind = $mainMod, bracketright, hy3:focustab, r, wrap
|
||||
bind = $mainMod, bracketleft, hy3:focustab, l, wrap
|
||||
|
||||
# Move window within tab group (hy3 has no movetab dispatcher)
|
||||
bind = $mainMod SHIFT, bracketright, hy3:movewindow, r, visible
|
||||
bind = $mainMod SHIFT, bracketleft, hy3:movewindow, l, visible
|
||||
|
||||
# Expand focus to parent group (like XMonad's focus parent)
|
||||
bind = $mainMod, grave, hy3:expand, expand
|
||||
bind = $mainMod SHIFT, grave, hy3:expand, base
|
||||
|
||||
# Fullscreen (like XMonad's NBFULL toggle)
|
||||
bind = $mainMod, F, fullscreen, 0
|
||||
bind = $mainMod SHIFT, F, fullscreen, 1
|
||||
|
||||
# Toggle floating
|
||||
bind = $mainMod, T, togglefloating,
|
||||
|
||||
# Resize split ratio (hy3 uses resizeactive for splits)
|
||||
binde = $mainMod, comma, resizeactive, -50 0
|
||||
binde = $mainMod, period, resizeactive, 50 0
|
||||
|
||||
# Equalize window sizes on workspace (hy3)
|
||||
bind = $mainMod SHIFT, equal, hy3:equalize, workspace
|
||||
|
||||
# Kill group - removes the focused window from its group
|
||||
bind = $mainMod, N, hy3:killactive
|
||||
|
||||
# hy3:setswallow - set a window to swallow newly spawned windows
|
||||
bind = $mainMod CTRL, M, hy3:setswallow, toggle
|
||||
|
||||
# Minimize/unminimize (via special workspace)
|
||||
bind = $mainMod, M, exec, ~/.config/hypr/scripts/minimize-active.sh minimized
|
||||
bind = $mainMod SHIFT, M, exec, ~/.config/hypr/scripts/unminimize-last.sh minimized
|
||||
|
||||
# Minimized "picker" mode:
|
||||
# Open the minimized special workspace, focus a window, press Enter to restore it.
|
||||
bind = $modAlt, Return, exec, ~/.config/hypr/scripts/minimized-mode.sh minimized
|
||||
|
||||
submap = minimized
|
||||
bind = , Return, exec, ~/.config/hypr/scripts/unminimize-last.sh minimized; hyprctl dispatch submap reset
|
||||
bind = , Escape, exec, ~/.config/hypr/scripts/minimized-cancel.sh minimized
|
||||
bind = $modAlt, Return, exec, ~/.config/hypr/scripts/minimized-cancel.sh minimized
|
||||
|
||||
# Optional: basic focus navigation inside the picker.
|
||||
bind = , H, movefocus, l
|
||||
bind = , J, movefocus, d
|
||||
bind = , K, movefocus, u
|
||||
bind = , L, movefocus, r
|
||||
bind = , left, movefocus, l
|
||||
bind = , down, movefocus, d
|
||||
bind = , up, movefocus, u
|
||||
bind = , right, movefocus, r
|
||||
|
||||
submap = reset
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# WORKSPACE CONTROL
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Switch workspaces (1-9 only) on the currently focused monitor.
|
||||
bind = $mainMod, 1, focusworkspaceoncurrentmonitor, 1
|
||||
bind = $mainMod, 2, focusworkspaceoncurrentmonitor, 2
|
||||
bind = $mainMod, 3, focusworkspaceoncurrentmonitor, 3
|
||||
bind = $mainMod, 4, focusworkspaceoncurrentmonitor, 4
|
||||
bind = $mainMod, 5, focusworkspaceoncurrentmonitor, 5
|
||||
bind = $mainMod, 6, focusworkspaceoncurrentmonitor, 6
|
||||
bind = $mainMod, 7, focusworkspaceoncurrentmonitor, 7
|
||||
bind = $mainMod, 8, focusworkspaceoncurrentmonitor, 8
|
||||
bind = $mainMod, 9, focusworkspaceoncurrentmonitor, 9
|
||||
|
||||
# Move window to workspace
|
||||
bind = $mainMod SHIFT, 1, movetoworkspace, 1
|
||||
bind = $mainMod SHIFT, 2, movetoworkspace, 2
|
||||
bind = $mainMod SHIFT, 3, movetoworkspace, 3
|
||||
bind = $mainMod SHIFT, 4, movetoworkspace, 4
|
||||
bind = $mainMod SHIFT, 5, movetoworkspace, 5
|
||||
bind = $mainMod SHIFT, 6, movetoworkspace, 6
|
||||
bind = $mainMod SHIFT, 7, movetoworkspace, 7
|
||||
bind = $mainMod SHIFT, 8, movetoworkspace, 8
|
||||
bind = $mainMod SHIFT, 9, movetoworkspace, 9
|
||||
|
||||
# Move and follow to workspace (like XMonad's shiftThenView)
|
||||
bind = $mainMod CTRL, 1, movetoworkspacesilent, 1
|
||||
bind = $mainMod CTRL, 1, focusworkspaceoncurrentmonitor, 1
|
||||
bind = $mainMod CTRL, 2, movetoworkspacesilent, 2
|
||||
bind = $mainMod CTRL, 2, focusworkspaceoncurrentmonitor, 2
|
||||
bind = $mainMod CTRL, 3, movetoworkspacesilent, 3
|
||||
bind = $mainMod CTRL, 3, focusworkspaceoncurrentmonitor, 3
|
||||
bind = $mainMod CTRL, 4, movetoworkspacesilent, 4
|
||||
bind = $mainMod CTRL, 4, focusworkspaceoncurrentmonitor, 4
|
||||
bind = $mainMod CTRL, 5, movetoworkspacesilent, 5
|
||||
bind = $mainMod CTRL, 5, focusworkspaceoncurrentmonitor, 5
|
||||
bind = $mainMod CTRL, 6, movetoworkspacesilent, 6
|
||||
bind = $mainMod CTRL, 6, focusworkspaceoncurrentmonitor, 6
|
||||
bind = $mainMod CTRL, 7, movetoworkspacesilent, 7
|
||||
bind = $mainMod CTRL, 7, focusworkspaceoncurrentmonitor, 7
|
||||
bind = $mainMod CTRL, 8, movetoworkspacesilent, 8
|
||||
bind = $mainMod CTRL, 8, focusworkspaceoncurrentmonitor, 8
|
||||
bind = $mainMod CTRL, 9, movetoworkspacesilent, 9
|
||||
bind = $mainMod CTRL, 9, focusworkspaceoncurrentmonitor, 9
|
||||
|
||||
# Toggle to the previous workspace on the current monitor using Hyprland's
|
||||
# built-in per-monitor workspace history.
|
||||
bind = $mainMod, backslash, workspace, previous_per_monitor
|
||||
|
||||
# Swap current workspace with another (like XMonad's swapWithCurrent)
|
||||
bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh
|
||||
|
||||
# Go to next empty workspace (like XMonad's moveTo Next emptyWS)
|
||||
bind = $hyper, E, exec, ~/.config/hypr/scripts/workspace-goto-empty.sh
|
||||
|
||||
# Move to next screen (like XMonad's shiftToNextScreenX)
|
||||
bind = $mainMod, Z, focusmonitor, +1
|
||||
bind = $mainMod SHIFT, Z, movewindow, mon:+1
|
||||
|
||||
# Shift to empty workspace and view (like XMonad's shiftToEmptyAndView)
|
||||
bind = $mainMod SHIFT, H, exec, ~/.config/hypr/scripts/workspace-move-to-empty.sh
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# WINDOW MANAGEMENT
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Go to window (rofi window switcher with icons)
|
||||
bind = $mainMod, G, exec, ~/.config/hypr/scripts/go-to-window.sh
|
||||
|
||||
# Bring window (move to current workspace)
|
||||
bind = $mainMod, B, exec, ~/.config/hypr/scripts/bring-window.sh
|
||||
|
||||
# Replace window (swap focused with selected - like XMonad's myReplaceWindow)
|
||||
bind = $mainMod SHIFT, B, exec, ~/.config/hypr/scripts/replace-window.sh
|
||||
|
||||
# Gather windows of same class (like XMonad's gatherThisClass)
|
||||
bind = $hyper, G, exec, ~/.config/hypr/scripts/gather-class.sh
|
||||
|
||||
# Focus next window of different class (like XMonad's focusNextClass)
|
||||
bind = $mainMod, apostrophe, exec, ~/.config/hypr/scripts/focus-next-class.sh
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MEDIA KEYS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Volume control (matching XMonad: Mod+I=up, Mod+K=down, Mod+U=mute)
|
||||
binde = , XF86AudioRaiseVolume, exec, set_volume --unmute --change-volume +5
|
||||
binde = , XF86AudioLowerVolume, exec, set_volume --unmute --change-volume -5
|
||||
bind = , XF86AudioMute, exec, set_volume --toggle-mute
|
||||
binde = $mainMod, I, exec, set_volume --unmute --change-volume +5
|
||||
binde = $mainMod, K, exec, set_volume --unmute --change-volume -5
|
||||
bind = $mainMod, U, exec, set_volume --toggle-mute
|
||||
|
||||
# Media player controls (matching XMonad: Mod+;=play, Mod+L=next, Mod+J=prev)
|
||||
bind = $mainMod, semicolon, exec, playerctl play-pause
|
||||
bind = , XF86AudioPlay, exec, playerctl play-pause
|
||||
bind = , XF86AudioPause, exec, playerctl play-pause
|
||||
bind = $mainMod, L, exec, playerctl next
|
||||
bind = , XF86AudioNext, exec, playerctl next
|
||||
bind = $mainMod, J, exec, playerctl previous
|
||||
bind = , XF86AudioPrev, exec, playerctl previous
|
||||
|
||||
# Mute current window (like XMonad's toggle_mute_current_window)
|
||||
bind = $hyper SHIFT, Q, exec, toggle_mute_current_window.sh
|
||||
bind = $hyper CTRL, Q, exec, toggle_mute_current_window.sh only
|
||||
|
||||
# Brightness control
|
||||
binde = , XF86MonBrightnessUp, exec, brightness.sh up
|
||||
binde = , XF86MonBrightnessDown, exec, brightness.sh down
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# UTILITY BINDINGS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
bind = $hyper, V, exec, cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy
|
||||
bind = $hyper, P, exec, rofi-pass
|
||||
bind = $hyper, H, exec, grim -g "$(slurp)" - | swappy -f -
|
||||
bind = $hyper, C, exec, shell_command.sh
|
||||
bind = $hyper, X, exec, rofi_command.sh
|
||||
bind = $hyper SHIFT, L, exec, hyprlock
|
||||
bind = $hyper, K, exec, rofi_kill_process.sh
|
||||
bind = $hyper SHIFT, K, exec, rofi_kill_all.sh
|
||||
bind = $hyper, R, exec, rofi-systemd
|
||||
bind = $hyper, slash, exec, toggle_taffybar
|
||||
bind = $hyper, 9, exec, start_synergy.sh
|
||||
bind = $hyper, I, exec, rofi_select_input.hs
|
||||
bind = $hyper, O, exec, rofi_paswitch
|
||||
bind = $hyper, W, exec, rofi_wallpaper.sh
|
||||
bind = $hyper, Y, exec, rofi_agentic_skill
|
||||
|
||||
# Reload config
|
||||
bind = $mainMod, R, exec, hyprctl reload
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MOUSE BINDINGS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
bindm = $mainMod, mouse:272, movewindow
|
||||
bindm = $mainMod, mouse:273, resizewindow
|
||||
|
||||
# Scroll through workspaces
|
||||
bind = $mainMod, mouse_down, exec, ~/.config/hypr/scripts/workspace-scroll.sh +1
|
||||
bind = $mainMod, mouse_up, exec, ~/.config/hypr/scripts/workspace-scroll.sh -1
|
||||
|
||||
# =============================================================================
|
||||
# AUTOSTART
|
||||
# =============================================================================
|
||||
|
||||
# Wire Hyprland into Home Manager's standard user-session targets.
|
||||
# `graphical-session.target` pulls in most tray/SNI applets (which in turn pull in `tray.target`).
|
||||
# Keep the systemd user manager in sync with the current Hyprland session before
|
||||
# starting any session-bound units. Separate `exec-once` commands race.
|
||||
exec-once = sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'
|
||||
# Force a fresh daemon after compositor restarts so hyprscratch doesn't keep a stale socket.
|
||||
exec-once = systemctl --user restart hyprscratch.service
|
||||
exec-once = hypridle
|
||||
|
||||
# Clipboard history daemon
|
||||
exec-once = wl-paste --type text --watch cliphist store
|
||||
exec-once = wl-paste --type image --watch cliphist store
|
||||
42
dotfiles/config/hypr/hyprland.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
local function config_dir()
|
||||
local source = debug.getinfo(1, "S").source
|
||||
if source:sub(1, 1) == "@" then
|
||||
source = source:sub(2)
|
||||
end
|
||||
|
||||
local dir = source:match("^(.*)/[^/]*$")
|
||||
if dir and dir ~= "" then
|
||||
return dir
|
||||
end
|
||||
|
||||
return "."
|
||||
end
|
||||
|
||||
local base_dir = config_dir()
|
||||
package.path = table.concat({
|
||||
base_dir .. "/?.lua",
|
||||
base_dir .. "/?/init.lua",
|
||||
package.path,
|
||||
}, ";")
|
||||
|
||||
local modules = {
|
||||
"hyprland.state",
|
||||
"hyprland.scratchpads",
|
||||
"hyprland.core",
|
||||
"hyprland.layouts",
|
||||
"hyprland.windows",
|
||||
"hyprland.settings",
|
||||
"hyprland.binds",
|
||||
"hyprland.events",
|
||||
}
|
||||
|
||||
for _, module in ipairs(modules) do
|
||||
package.loaded[module] = nil
|
||||
end
|
||||
|
||||
local ctx = require(modules[1])
|
||||
setmetatable(ctx, { __index = _G })
|
||||
|
||||
for i = 2, #modules do
|
||||
require(modules[i]).setup(ctx)
|
||||
end
|
||||
332
dotfiles/config/hypr/hyprland/binds.lua
Normal file
@@ -0,0 +1,332 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
|
||||
local function desc(description, opts)
|
||||
local bind_opts = {}
|
||||
for key, value in pairs(opts or {}) do
|
||||
bind_opts[key] = value
|
||||
end
|
||||
bind_opts.description = description
|
||||
return bind_opts
|
||||
end
|
||||
|
||||
local function setup_launcher_and_app_bindings()
|
||||
bind(main_mod .. " + P", exec(launcher_command), desc("Open application launcher"))
|
||||
bind(main_mod .. " + SHIFT + P", exec(run_menu), desc("Open command runner"))
|
||||
bind(main_mod .. " + SHIFT + Return", exec(terminal), desc("Open terminal"))
|
||||
bind(main_mod .. " + E", exec("emacsclient --eval '(emacs-everywhere)'"), desc("Open Emacs Everywhere"))
|
||||
bind(main_mod .. " + V", exec("wl-paste --no-newline | ydotool type --file -"), desc("Type clipboard contents"))
|
||||
end
|
||||
|
||||
local function setup_shell_and_session_bindings()
|
||||
bind(hyper .. " + SHIFT + N", exec(shell_ui_command .. " control-center"), desc("Open control center"))
|
||||
bind(hyper .. " + CTRL + N", exec(shell_ui_command .. " settings"), desc("Open system settings"))
|
||||
bind(main_mod .. " + Q", exec("hyprctl reload"), desc("Reload Hyprland"))
|
||||
bind(main_mod .. " + R", exec("hyprctl reload"), desc("Reload Hyprland"))
|
||||
bind(hyper .. " + SHIFT + L", exec("hyprlock"), desc("Lock screen"))
|
||||
bind(hyper .. " + SHIFT + V", toggle_visual_performance_mode, desc("Toggle Hyprland performance mode"))
|
||||
bind(hyper .. " + slash", function()
|
||||
hl.exec_cmd("toggle_taffybar")
|
||||
refresh_monitor_reserved_cache(0.25)
|
||||
refresh_active_scratchpad_geometries_later(600)
|
||||
end, desc("Toggle taffybar"))
|
||||
end
|
||||
|
||||
local function setup_audio_media_bindings()
|
||||
bind(main_mod .. " + I", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
|
||||
bind(main_mod .. " + K", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
|
||||
bind(main_mod .. " + U", exec("set_volume --toggle-mute"), desc("Toggle mute"))
|
||||
bind(main_mod .. " + semicolon", exec("playerctl play-pause"), desc("Play or pause media"))
|
||||
bind(main_mod .. " + L", exec("playerctl next"), desc("Skip to next media track"))
|
||||
bind(main_mod .. " + J", exec("playerctl previous"), desc("Skip to previous media track"))
|
||||
|
||||
bind("XF86AudioPlay", exec("playerctl play-pause"), desc("Play or pause media"))
|
||||
bind("XF86AudioPause", exec("playerctl play-pause"), desc("Play or pause media"))
|
||||
bind("XF86AudioNext", exec("playerctl next"), desc("Skip to next media track"))
|
||||
bind("XF86AudioPrev", exec("playerctl previous"), desc("Skip to previous media track"))
|
||||
bind("XF86AudioRaiseVolume", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
|
||||
bind("XF86AudioLowerVolume", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
|
||||
bind("XF86AudioMute", exec("set_volume --toggle-mute"), desc("Toggle mute"))
|
||||
bind(hyper .. " + O", exec("/home/imalison/dotfiles/dotfiles/lib/functions/rofi_paswitch"), desc("Open PulseAudio output switcher"))
|
||||
bind(hyper .. " + SHIFT + O", exec("/home/imalison/dotfiles/dotfiles/lib/bin/kef-optical"), desc("Switch KEF speakers to optical input"))
|
||||
end
|
||||
|
||||
local function setup_display_wallpaper_and_capture_bindings()
|
||||
bind("XF86MonBrightnessUp", exec("brightness.sh up"), desc("Raise display brightness", { repeating = true }))
|
||||
bind("XF86MonBrightnessDown", exec("brightness.sh down"), desc("Lower display brightness", { repeating = true }))
|
||||
bind("Print", exec("flameshot gui"), desc("Take screenshot"))
|
||||
bind(hyper .. " + H", exec("flameshot gui"), desc("Take screenshot"))
|
||||
bind(hyper .. " + backslash", exec("/home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle"), desc("Toggle monitor input"))
|
||||
bind(hyper .. " + comma", exec("rofi_wallpaper.sh"), desc("Open wallpaper menu"))
|
||||
bind(hyper .. " + SHIFT + comma", exec("/home/imalison/dotfiles/dotfiles/lib/bin/neowall-wallpaper toggle"), desc("Toggle neowall wallpaper"))
|
||||
end
|
||||
|
||||
local function setup_rofi_and_tool_bindings()
|
||||
bind(main_mod .. " + X", exec("rofi_command.sh"), desc("Open command menu"))
|
||||
bind(hyper .. " + V", exec([[cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy]]), desc("Open clipboard history"))
|
||||
bind(hyper .. " + P", exec("rofi-pass"), desc("Open password menu"))
|
||||
bind(hyper .. " + C", exec("rofi_tmcodex.sh"), desc("Open Codex session menu"))
|
||||
bind(hyper .. " + SHIFT + C", exec("rofi_tmcodex.sh resume"), desc("Resume Codex session"))
|
||||
bind(hyper .. " + L", exec("hypr_rofi_layout"), desc("Open Hyprland layout menu"))
|
||||
bind(hyper .. " + K", exec("rofi_kill_process.sh"), desc("Open process kill menu"))
|
||||
bind(hyper .. " + SHIFT + K", exec("rofi_kill_all.sh"), desc("Open kill-all menu"))
|
||||
bind(hyper .. " + R", exec("rofi_systemd_mono"), desc("Open systemd unit menu"))
|
||||
bind(hyper .. " + X", exec("hypr_rofi_action"), desc("Open Hyprland action menu"))
|
||||
bind(hyper .. " + I", exec("rofi_select_input.hs"), desc("Open input selection menu"))
|
||||
bind(hyper .. " + Y", exec("rofi_agentic_skill"), desc("Open agentic skill menu"))
|
||||
end
|
||||
|
||||
local function setup_external_command_bindings()
|
||||
setup_launcher_and_app_bindings()
|
||||
setup_shell_and_session_bindings()
|
||||
setup_audio_media_bindings()
|
||||
setup_display_wallpaper_and_capture_bindings()
|
||||
setup_rofi_and_tool_bindings()
|
||||
end
|
||||
|
||||
local function setup_window_overview_bindings()
|
||||
bind(main_mod .. " + SHIFT + C", hl.dsp.window.close(), desc("Close active window"))
|
||||
bind(main_mod .. " + SHIFT + Q", hl.dsp.exit(), desc("Exit Hyprland"))
|
||||
bind(main_mod .. " + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
|
||||
bind(main_mod .. " + SHIFT + Tab", hyprwinview({
|
||||
action = "show",
|
||||
include_current_workspace = false,
|
||||
start_in_filter_mode = true,
|
||||
default_action = "bring",
|
||||
}), desc("Show all-workspace window overview", overview_bind_opts))
|
||||
bind(main_mod .. " + SHIFT + slash", hyprwinview({ action = "toggle-filter" }), desc("Toggle window overview filter", overview_bind_opts))
|
||||
bind("ALT + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
|
||||
bind("ALT + SHIFT + Tab", hyprexpo("on"), desc("Open hyprexpo workspace overview", overview_bind_opts))
|
||||
bind(main_mod .. " + G", hyprwinview({
|
||||
action = "show",
|
||||
start_in_filter_mode = true,
|
||||
default_action = "select",
|
||||
}), desc("Show window overview", overview_bind_opts))
|
||||
bind(main_mod .. " + B", hyprwinview({
|
||||
action = "show",
|
||||
start_in_filter_mode = true,
|
||||
default_action = "bring",
|
||||
}), desc("Bring window from overview", overview_bind_opts))
|
||||
bind(main_mod .. " + SHIFT + B", hyprwinview({
|
||||
action = "show",
|
||||
start_in_filter_mode = true,
|
||||
default_action = "bring-replace",
|
||||
}), desc("Replace active window from overview", overview_bind_opts))
|
||||
end
|
||||
|
||||
local function setup_window_focus_and_move_bindings()
|
||||
bind(main_mod .. " + W", function()
|
||||
focus_direction("up")
|
||||
end, desc("Focus window above"))
|
||||
bind(main_mod .. " + S", function()
|
||||
focus_direction("down")
|
||||
end, desc("Focus window below"))
|
||||
bind(main_mod .. " + A", function()
|
||||
focus_direction("left")
|
||||
end, desc("Focus window to the left"))
|
||||
bind(main_mod .. " + D", function()
|
||||
focus_direction("right")
|
||||
end, desc("Focus window to the right"))
|
||||
|
||||
bind(main_mod .. " + SHIFT + W", function()
|
||||
swap_direction("up")
|
||||
end, desc("Swap active window upward"))
|
||||
bind(main_mod .. " + SHIFT + S", function()
|
||||
swap_direction("down")
|
||||
end, desc("Swap active window downward"))
|
||||
bind(main_mod .. " + SHIFT + A", function()
|
||||
swap_direction("left")
|
||||
end, desc("Swap active window left"))
|
||||
bind(main_mod .. " + SHIFT + D", function()
|
||||
swap_direction("right")
|
||||
end, desc("Swap active window right"))
|
||||
|
||||
bind(main_mod .. " + CTRL + W", function()
|
||||
move_window_to_monitor("u", false)
|
||||
end, desc("Move window to monitor above"))
|
||||
bind(main_mod .. " + CTRL + S", function()
|
||||
move_window_to_monitor("d", false)
|
||||
end, desc("Move window to monitor below"))
|
||||
bind(main_mod .. " + CTRL + A", function()
|
||||
move_window_to_monitor("l", false)
|
||||
end, desc("Move window to monitor on the left"))
|
||||
bind(main_mod .. " + CTRL + D", function()
|
||||
move_window_to_monitor("r", false)
|
||||
end, desc("Move window to monitor on the right"))
|
||||
bind(main_mod .. " + CTRL + SHIFT + W", function()
|
||||
move_window_to_empty_workspace_on_monitor("u")
|
||||
end, desc("Move window to empty workspace on monitor above"))
|
||||
bind(main_mod .. " + CTRL + SHIFT + S", function()
|
||||
move_window_to_empty_workspace_on_monitor("d")
|
||||
end, desc("Move window to empty workspace on monitor below"))
|
||||
bind(main_mod .. " + CTRL + SHIFT + A", function()
|
||||
move_window_to_empty_workspace_on_monitor("l")
|
||||
end, desc("Move window to empty workspace on left monitor"))
|
||||
bind(main_mod .. " + CTRL + SHIFT + D", function()
|
||||
move_window_to_empty_workspace_on_monitor("r")
|
||||
end, desc("Move window to empty workspace on right monitor"))
|
||||
end
|
||||
|
||||
local function setup_submap_bindings()
|
||||
hl.define_submap("swap-workspace", function()
|
||||
for i = 1, 9 do
|
||||
local workspace_id = i
|
||||
bind(tostring(i), function()
|
||||
swap_current_workspace_with(workspace_id)
|
||||
dispatch(hl.dsp.submap("reset"))
|
||||
end, desc("Swap current workspace with workspace " .. workspace_id))
|
||||
end
|
||||
|
||||
bind("Escape", hl.dsp.submap("reset"), desc("Exit workspace swap mode"))
|
||||
bind("catchall", hl.dsp.submap("reset"), desc("Exit workspace swap mode"))
|
||||
end)
|
||||
|
||||
hl.define_submap("window-picker", function()
|
||||
for i = 1, 9 do
|
||||
local index = i
|
||||
bind(tostring(i), function()
|
||||
activate_window_picker_candidate(index)
|
||||
end, desc("Activate window picker candidate " .. index))
|
||||
end
|
||||
|
||||
bind("Escape", hl.dsp.submap("reset"), desc("Exit window picker"))
|
||||
bind("catchall", hl.dsp.submap("reset"), desc("Exit window picker"))
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
local function setup_window_resize_and_monitor_bindings()
|
||||
bind(mod_alt .. " + SHIFT + W", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), desc("Shrink window height upward", { repeating = true }))
|
||||
bind(mod_alt .. " + SHIFT + S", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), desc("Grow window height downward", { repeating = true }))
|
||||
bind(mod_alt .. " + SHIFT + A", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), desc("Shrink window width leftward", { repeating = true }))
|
||||
bind(mod_alt .. " + SHIFT + D", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), desc("Grow window width rightward", { repeating = true }))
|
||||
|
||||
bind(hyper .. " + W", hl.dsp.focus({ monitor = "u" }), desc("Focus monitor above"))
|
||||
bind(hyper .. " + S", hl.dsp.focus({ monitor = "d" }), desc("Focus monitor below"))
|
||||
bind(hyper .. " + A", hl.dsp.focus({ monitor = "l" }), desc("Focus monitor on the left"))
|
||||
bind(hyper .. " + D", hl.dsp.focus({ monitor = "r" }), desc("Focus monitor on the right"))
|
||||
bind(hyper .. " + SHIFT + W", function()
|
||||
move_window_to_monitor("u", true)
|
||||
end, desc("Move window to monitor above and follow"))
|
||||
bind(hyper .. " + SHIFT + S", function()
|
||||
move_window_to_monitor("d", true)
|
||||
end, desc("Move window to monitor below and follow"))
|
||||
bind(hyper .. " + SHIFT + A", function()
|
||||
move_window_to_monitor("l", true)
|
||||
end, desc("Move window to left monitor and follow"))
|
||||
bind(hyper .. " + SHIFT + D", function()
|
||||
move_window_to_monitor("r", true)
|
||||
end, desc("Move window to right monitor and follow"))
|
||||
end
|
||||
|
||||
local function setup_layout_and_window_state_bindings()
|
||||
bind(main_mod .. " + Space", cycle_layout_or_restore_tabbed_group, desc("Cycle workspace layout"))
|
||||
bind(main_mod .. " + SHIFT + Space", force_columns_layout, desc("Force columns layout"))
|
||||
bind(main_mod .. " + CTRL + Space", gather_workspace_into_tabbed_group, desc("Gather workspace into tabbed group"))
|
||||
bind(main_mod .. " + bracketright", monocle_next, desc("Focus next monocle window"))
|
||||
bind(main_mod .. " + bracketleft", monocle_prev, desc("Focus previous monocle window"))
|
||||
bind(main_mod .. " + T", hl.dsp.window.float({ action = "disable" }), desc("Tile active window"))
|
||||
bind(main_mod .. " + O", toggle_pinned_active_window, desc("Toggle pinned active window"))
|
||||
bind(main_mod .. " + M", minimize_active_window, desc("Minimize active window"))
|
||||
bind(main_mod .. " + SHIFT + M", restore_last_minimized, desc("Restore last minimized window"))
|
||||
bind(main_mod .. " + CTRL + SHIFT + M", function()
|
||||
enter_window_picker("minimized")
|
||||
end, desc("Pick minimized window to restore"))
|
||||
bind(main_mod .. " + SHIFT + equal", schedule_nstack_count_update, desc("Update nstack window count"))
|
||||
bind(main_mod .. " + CTRL + M", hl.dsp.window.toggle_swallow(), desc("Toggle window swallowing"))
|
||||
bind(main_mod .. " + SHIFT + E", function()
|
||||
move_to_next_empty_workspace(true)
|
||||
end, desc("Move to next empty workspace"))
|
||||
bind(main_mod .. " + CTRL + E", function()
|
||||
move_to_next_empty_workspace(false)
|
||||
end, desc("Move window to next empty workspace"))
|
||||
bind(main_mod .. " + apostrophe", focus_next_class, desc("Focus next window class"))
|
||||
bind(hyper .. " + 1", toggle_inactive_opacity_for_active_window, desc("Toggle inactive opacity reduction for active window"))
|
||||
bind(mod_alt .. " + W", show_active_window_info, desc("Show active window info"))
|
||||
end
|
||||
|
||||
local function setup_scratchpad_bindings()
|
||||
bind(main_mod .. " + SHIFT + X", hl.dsp.workspace.toggle_special("NSP"), desc("Toggle NSP special workspace"))
|
||||
bind(mod_alt .. " + C", function()
|
||||
toggle_scratchpad("codex")
|
||||
end, desc("Toggle Codex scratchpad"))
|
||||
bind(mod_alt .. " + E", function()
|
||||
toggle_scratchpad("element")
|
||||
end, desc("Toggle Element scratchpad"))
|
||||
bind(mod_alt .. " + H", function()
|
||||
toggle_scratchpad("htop")
|
||||
end, desc("Toggle htop scratchpad"))
|
||||
bind(mod_alt .. " + K", function()
|
||||
toggle_scratchpad("slack")
|
||||
end, desc("Toggle Slack scratchpad"))
|
||||
bind(mod_alt .. " + M", function()
|
||||
toggle_scratchpad("messages")
|
||||
end, desc("Toggle Messages scratchpad"))
|
||||
bind(mod_alt .. " + S", function()
|
||||
toggle_scratchpad("spotify")
|
||||
end, desc("Toggle Spotify scratchpad"))
|
||||
bind(mod_alt .. " + T", function()
|
||||
toggle_scratchpad("transmission")
|
||||
end, desc("Toggle Transmission scratchpad"))
|
||||
bind(mod_alt .. " + V", function()
|
||||
toggle_scratchpad("volume")
|
||||
end, desc("Toggle volume scratchpad"))
|
||||
bind(mod_alt .. " + grave", function()
|
||||
toggle_scratchpad("dropdown")
|
||||
end, desc("Toggle dropdown scratchpad"))
|
||||
bind(mod_alt .. " + Space", minimize_other_classes, desc("Minimize other window classes"))
|
||||
bind(mod_alt .. " + SHIFT + Space", restore_focused_class, desc("Restore focused window class"))
|
||||
bind(mod_alt .. " + Return", restore_all_minimized, desc("Restore all minimized windows"))
|
||||
end
|
||||
|
||||
local function setup_workspace_bindings()
|
||||
for i = 1, 9 do
|
||||
local workspace = tostring(i)
|
||||
bind(main_mod .. " + " .. workspace, hl.dsp.focus({ workspace = workspace, on_current_monitor = true }), desc("Focus workspace " .. workspace))
|
||||
bind(main_mod .. " + SHIFT + " .. workspace, hl.dsp.window.move({ workspace = workspace, follow = false }), desc("Move window to workspace " .. workspace))
|
||||
bind(main_mod .. " + CTRL + " .. workspace, function()
|
||||
dispatch(hl.dsp.window.move({ workspace = workspace, follow = false }))
|
||||
dispatch(hl.dsp.focus({ workspace = workspace, on_current_monitor = true }))
|
||||
end, desc("Move window to workspace " .. workspace .. " and follow"))
|
||||
end
|
||||
|
||||
bind(main_mod .. " + backslash", workspacehistory("cycle", 1), desc("Cycle to next workspace in history"))
|
||||
bind(main_mod .. " + slash", workspacehistory("cycle", -1), desc("Cycle to previous workspace in history"))
|
||||
bind(main_mod .. " + Escape", workspacehistory("cancel"), desc("Cancel workspace history cycle"))
|
||||
bind(main_mod .. " + Z", hl.dsp.focus({ monitor = "+1" }), desc("Focus next monitor"))
|
||||
bind(main_mod .. " + SHIFT + Z", hl.dsp.window.move({ monitor = "+1" }), desc("Move window to next monitor"))
|
||||
bind(main_mod .. " + mouse_down", function()
|
||||
cycle_workspace(1)
|
||||
end, desc("Cycle to next workspace"))
|
||||
bind(main_mod .. " + mouse_up", function()
|
||||
cycle_workspace(-1)
|
||||
end, desc("Cycle to previous workspace"))
|
||||
bind(hyper .. " + E", focus_next_empty_workspace, desc("Focus next empty workspace"))
|
||||
bind(hyper .. " + 5", enter_workspace_swap_mode, desc("Enter workspace swap mode"))
|
||||
bind(hyper .. " + G", gather_focused_class, desc("Gather focused window class"))
|
||||
bind(hyper .. " + SHIFT + backslash", workspacehistory("debug"), desc("Show workspace history debug info"))
|
||||
end
|
||||
|
||||
local function setup_mouse_bindings()
|
||||
bind(main_mod .. " + mouse:272", float_and_drag_active_window, desc("Float and drag active window"))
|
||||
bind(main_mod .. " + mouse:273", float_and_resize_active_window, desc("Float and resize active window"))
|
||||
end
|
||||
|
||||
local function setup_internal_window_manager_bindings()
|
||||
setup_window_overview_bindings()
|
||||
setup_window_focus_and_move_bindings()
|
||||
setup_submap_bindings()
|
||||
setup_window_resize_and_monitor_bindings()
|
||||
setup_layout_and_window_state_bindings()
|
||||
setup_scratchpad_bindings()
|
||||
setup_workspace_bindings()
|
||||
setup_mouse_bindings()
|
||||
end
|
||||
|
||||
setup_external_command_bindings()
|
||||
setup_internal_window_manager_bindings()
|
||||
end
|
||||
|
||||
return M
|
||||
615
dotfiles/config/hypr/hyprland/core.lua
Normal file
@@ -0,0 +1,615 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local function command_line_contains(needle)
|
||||
local command_line = io.open("/proc/self/cmdline", "rb")
|
||||
if not command_line then
|
||||
return false
|
||||
end
|
||||
|
||||
local contents = command_line:read("*a") or ""
|
||||
command_line:close()
|
||||
return contents:find(needle, 1, true) ~= nil
|
||||
end
|
||||
|
||||
verify_config = command_line_contains("--verify-config")
|
||||
dev_session = os.getenv("IMALISON_HYPRLAND_DEV_SESSION") == "1"
|
||||
|
||||
local function exec(command)
|
||||
return hl.dsp.exec_cmd(command)
|
||||
end
|
||||
|
||||
local function dispatch(dispatcher)
|
||||
return hl.dispatch(dispatcher)
|
||||
end
|
||||
|
||||
local action_registry = {}
|
||||
|
||||
local function action_text(value)
|
||||
return tostring(value or ""):gsub("[\t\r\n]", " "):gsub(" +", " "):match("^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
local function action_registry_path()
|
||||
local runtime_dir = os.getenv("XDG_RUNTIME_DIR") or "/tmp"
|
||||
return runtime_dir .. "/hyprland-actions.tsv"
|
||||
end
|
||||
|
||||
local function register_action(keys, dispatcher, opts)
|
||||
local description = opts and opts.description
|
||||
if not description or description == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local id = tostring(#action_registry + 1)
|
||||
action_registry[#action_registry + 1] = {
|
||||
id = id,
|
||||
keys = action_text(keys),
|
||||
description = action_text(description),
|
||||
dispatcher = dispatcher,
|
||||
}
|
||||
end
|
||||
|
||||
local function bind(keys, dispatcher, opts)
|
||||
hl.bind(keys, dispatcher, opts)
|
||||
register_action(keys, dispatcher, opts)
|
||||
end
|
||||
|
||||
_G.im_hyprland_write_actions = function()
|
||||
local actions_file = io.open(action_registry_path(), "w")
|
||||
if not actions_file then
|
||||
return
|
||||
end
|
||||
|
||||
for _, action in ipairs(action_registry) do
|
||||
actions_file:write(action.id, "\t", action.description, "\t", action.keys, "\n")
|
||||
end
|
||||
|
||||
actions_file:close()
|
||||
end
|
||||
|
||||
_G.im_hyprland_run_action = function(id)
|
||||
local action = action_registry[tonumber(id)]
|
||||
if not action then
|
||||
return
|
||||
end
|
||||
|
||||
if type(action.dispatcher) == "function" then
|
||||
action.dispatcher()
|
||||
else
|
||||
dispatch(action.dispatcher)
|
||||
end
|
||||
end
|
||||
|
||||
local function shell_quote(value)
|
||||
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function overview_trace(label)
|
||||
local enabled = io.open(overview_trace_enabled_path, "r")
|
||||
if not enabled then
|
||||
return
|
||||
end
|
||||
enabled:close()
|
||||
|
||||
local trace = io.open(overview_trace_path, "a")
|
||||
if trace then
|
||||
trace:write(os.date("%Y-%m-%d %H:%M:%S "), label, "\n")
|
||||
trace:close()
|
||||
end
|
||||
end
|
||||
|
||||
local function window_selector(window)
|
||||
if not window or not window.address then
|
||||
return nil
|
||||
end
|
||||
return "address:" .. tostring(window.address)
|
||||
end
|
||||
|
||||
local function hyprexpo_call(method, arg)
|
||||
return function()
|
||||
overview_trace("hyprexpo:" .. method .. (arg and (" " .. tostring(arg)) or ""))
|
||||
if hl.plugin and hl.plugin.hyprexpo and hl.plugin.hyprexpo[method] then
|
||||
hl.plugin.hyprexpo[method](arg)
|
||||
else
|
||||
hl.notification.create({
|
||||
text = "hyprexpo is not loaded",
|
||||
duration = 1800,
|
||||
icon = notification_icons.warning,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function hyprexpo(action)
|
||||
return hyprexpo_call("expo", action or "toggle")
|
||||
end
|
||||
|
||||
local function hyprwinview(action)
|
||||
return function()
|
||||
local label = "hyprwinview"
|
||||
if type(action) == "table" and action.action then
|
||||
label = label .. " " .. tostring(action.action)
|
||||
elseif type(action) ~= "table" and action ~= nil then
|
||||
label = label .. " " .. tostring(action)
|
||||
end
|
||||
|
||||
local function invoke()
|
||||
overview_trace(label)
|
||||
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.overview then
|
||||
hl.plugin.hyprwinview.overview(action)
|
||||
else
|
||||
hl.notification.create({
|
||||
text = "hyprwinview is not loaded",
|
||||
duration = 1800,
|
||||
icon = notification_icons.warning,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
invoke()
|
||||
end
|
||||
end
|
||||
|
||||
local function workspacehistory(action, arg)
|
||||
return function()
|
||||
if hl.plugin and hl.plugin.workspacehistory and hl.plugin.workspacehistory[action] then
|
||||
hl.plugin.workspacehistory[action](arg)
|
||||
else
|
||||
hl.notification.create({
|
||||
text = "workspacehistory is not loaded",
|
||||
duration = 1800,
|
||||
icon = notification_icons.warning,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_nstack_config()
|
||||
if verify_config or not enable_nstack or not configure_nstack_plugin_from_lua then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
nstack = {
|
||||
layout = {
|
||||
orientation = "left",
|
||||
new_on_top = false,
|
||||
new_near_focused = true,
|
||||
new_is_master = false,
|
||||
no_gaps_when_only = true,
|
||||
special_scale_factor = 0.8,
|
||||
inherit_fullscreen = true,
|
||||
stacks = 1,
|
||||
center_single_master = false,
|
||||
mfact = 0.0,
|
||||
single_mfact = 1.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_hyprexpo_config()
|
||||
if verify_config or not enable_hyprexpo then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
hyprexpo = {
|
||||
columns = 3,
|
||||
gap_size = 5,
|
||||
gap_size_outer = 0,
|
||||
bg_col = 0xff111111,
|
||||
workspace_method = "first 1",
|
||||
skip_empty = false,
|
||||
max_workspace = max_workspace,
|
||||
gesture_distance = 200,
|
||||
keynav_wrap_h = 1,
|
||||
keynav_wrap_v = 1,
|
||||
keynav_reading_order = 0,
|
||||
border_width = 2,
|
||||
border_color_current = "rgb(66ccff)",
|
||||
border_color_focus = "rgb(edb443)",
|
||||
border_color_hover = "rgb(aabbcc)",
|
||||
tile_rounding = 5,
|
||||
tile_rounding_power = 2.0,
|
||||
label_enable = 1,
|
||||
label_font_size = 28,
|
||||
label_text_mode = "id",
|
||||
label_position = "center",
|
||||
label_offset_x = 6,
|
||||
label_offset_y = 6,
|
||||
selection_label_enable = 0,
|
||||
label_show = "always",
|
||||
label_color_default = 0xffffffff,
|
||||
label_color_hover = 0xffeeeeee,
|
||||
label_color_focus = 0xffedb443,
|
||||
label_color_current = 0xff66ccff,
|
||||
label_bg_enable = 1,
|
||||
label_bg_color = 0xcc000000,
|
||||
label_bg_rounding = 10,
|
||||
label_padding = 12,
|
||||
label_font_bold = 1,
|
||||
label_pixel_snap = 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_hyprwinview_config()
|
||||
if verify_config or not enable_hyprwinview then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
hyprwinview = {
|
||||
gap_size = 24,
|
||||
margin = 48,
|
||||
background = "rgba(10101400)",
|
||||
background_blur = 1,
|
||||
border_col = "rgba(ffffff33)",
|
||||
hover_border_col = "rgba(66ccffee)",
|
||||
border_size = 3,
|
||||
window_order = "application",
|
||||
keys_default_action = "return,enter,space,g,f",
|
||||
keys_filter_toggle = "/",
|
||||
show_app_icon = 1,
|
||||
app_icon_size = 48,
|
||||
app_icon_theme_source = "auto",
|
||||
app_icon_position = "bottom right",
|
||||
app_icon_margin_x = 12,
|
||||
app_icon_margin_y = 12,
|
||||
app_icon_margin_relative_x = 0.0,
|
||||
app_icon_margin_relative_y = 0.0,
|
||||
app_icon_offset_x = 0,
|
||||
app_icon_offset_y = 0,
|
||||
app_icon_backplate_col = "rgba(00000066)",
|
||||
app_icon_backplate_padding = 6,
|
||||
show_window_text = 1,
|
||||
window_text_font = "Sans",
|
||||
window_text_size = 14,
|
||||
window_text_color = "rgba(ffffffff)",
|
||||
window_text_backplate_col = "rgba(00000099)",
|
||||
window_text_padding = 6,
|
||||
filter_animation_ms = 140,
|
||||
animation = "workspace_zoom",
|
||||
animation_in_ms = 280,
|
||||
animation_out_ms = 220,
|
||||
animation_speed = 1.0,
|
||||
animation_scale = 0.94,
|
||||
animation_stagger_ms = 16,
|
||||
animation_stagger_max_ms = 120,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.configure then
|
||||
hl.plugin.hyprwinview.configure({
|
||||
keys = {
|
||||
left = { "a", "h", "left" },
|
||||
right = { "d", "l", "right" },
|
||||
up = { "w", "k", "up" },
|
||||
down = { "s", "j", "down" },
|
||||
default_action = { "return", "enter", "space", "g", "f" },
|
||||
bring = { "b", "shift+return", "shift+space" },
|
||||
bring_replace = { "shift + b" },
|
||||
close = { "escape", "q" },
|
||||
filter_toggle = { "/" },
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function active_workspace()
|
||||
return hl.get_active_workspace()
|
||||
end
|
||||
|
||||
local function active_workspace_id()
|
||||
local workspace = active_workspace()
|
||||
if workspace and type(workspace.id) == "number" and workspace.id >= 1 then
|
||||
return math.min(max_workspace, math.max(1, workspace.id))
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
local function workspace_key(workspace)
|
||||
workspace = workspace or active_workspace()
|
||||
if workspace and workspace.id then
|
||||
return tostring(workspace.id)
|
||||
end
|
||||
return tostring(active_workspace_id())
|
||||
end
|
||||
|
||||
local function current_workspace_layout()
|
||||
return workspace_layouts[workspace_key()] or columns_layout
|
||||
end
|
||||
|
||||
local function write_layout_state()
|
||||
local runtime_dir = os.getenv("XDG_RUNTIME_DIR")
|
||||
if not runtime_dir then
|
||||
return
|
||||
end
|
||||
|
||||
local file = io.open(runtime_dir .. "/hyprland-layout-state", "w")
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
|
||||
local workspace = active_workspace()
|
||||
file:write("workspace=", workspace_key(workspace), "\n")
|
||||
file:write("layout=", current_layout, "\n")
|
||||
for key, layout in pairs(workspace_layouts) do
|
||||
file:write("workspace.", tostring(key), "=", tostring(layout), "\n")
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function is_normal_workspace(workspace)
|
||||
return workspace and not workspace.special and workspace.id and workspace.id >= 1
|
||||
end
|
||||
|
||||
local function same_workspace(left, right)
|
||||
if not left or not right then
|
||||
return false
|
||||
end
|
||||
|
||||
if left.name and right.name and tostring(left.name) == tostring(right.name) then
|
||||
return true
|
||||
end
|
||||
|
||||
return left.id and right.id and left.id == right.id
|
||||
end
|
||||
|
||||
local function is_minimized_workspace(workspace)
|
||||
if not workspace then
|
||||
return false
|
||||
end
|
||||
|
||||
local name = tostring(workspace.name or "")
|
||||
return name == minimized_workspace or name == "minimized" or (workspace.special and name:find("minimized", 1, true) ~= nil)
|
||||
end
|
||||
|
||||
local function is_minimized_window(window)
|
||||
return window and is_minimized_workspace(window.workspace)
|
||||
end
|
||||
|
||||
local function is_normal_window(window)
|
||||
return window
|
||||
and window.mapped ~= false
|
||||
and not window.hidden
|
||||
and window.workspace
|
||||
and is_normal_workspace(window.workspace)
|
||||
and not is_scratchpad_window(window)
|
||||
and not is_minimized_window(window)
|
||||
end
|
||||
|
||||
local function tiled_windows(workspace)
|
||||
local windows = {}
|
||||
if not workspace then
|
||||
return windows
|
||||
end
|
||||
|
||||
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
|
||||
if not window.floating and not window.hidden then
|
||||
windows[#windows + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
return windows
|
||||
end
|
||||
|
||||
local function tiled_window_count(workspace)
|
||||
return #tiled_windows(workspace)
|
||||
end
|
||||
|
||||
local function sort_windows_by_focus_history(windows)
|
||||
table.sort(windows, function(left, right)
|
||||
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
|
||||
end)
|
||||
end
|
||||
|
||||
local function window_address_set(windows)
|
||||
local addresses = {}
|
||||
for _, window in ipairs(windows) do
|
||||
if window and window.address then
|
||||
addresses[window.address] = true
|
||||
end
|
||||
end
|
||||
return addresses
|
||||
end
|
||||
|
||||
local function window_address_list(windows)
|
||||
local addresses = {}
|
||||
for _, window in ipairs(windows) do
|
||||
if window and window.address then
|
||||
addresses[#addresses + 1] = window.address
|
||||
end
|
||||
end
|
||||
return addresses
|
||||
end
|
||||
|
||||
local function window_address_in_set(window, addresses)
|
||||
return window and window.address and addresses[window.address] or false
|
||||
end
|
||||
|
||||
local function windows_by_address()
|
||||
local windows = {}
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if window and window.address then
|
||||
windows[window.address] = window
|
||||
end
|
||||
end
|
||||
return windows
|
||||
end
|
||||
|
||||
local function numeric_component(value, key, index)
|
||||
if type(value) ~= "table" then
|
||||
return 0
|
||||
end
|
||||
|
||||
return tonumber(value[key] or value[index]) or 0
|
||||
end
|
||||
|
||||
local function window_center(window)
|
||||
local at = window and window.at or {}
|
||||
local size = window and window.size or {}
|
||||
return numeric_component(at, "x", 1) + numeric_component(size, "x", 1) / 2,
|
||||
numeric_component(at, "y", 2) + numeric_component(size, "y", 2) / 2
|
||||
end
|
||||
|
||||
local function tiled_window_geometry(window)
|
||||
if not window or window.floating then
|
||||
return nil
|
||||
end
|
||||
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return nil
|
||||
end
|
||||
|
||||
local at = window.at or {}
|
||||
local size = window.size or {}
|
||||
local width = math.floor(numeric_component(size, "x", 1))
|
||||
local height = math.floor(numeric_component(size, "y", 2))
|
||||
if width <= 0 or height <= 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
selector = selector,
|
||||
x = math.floor(numeric_component(at, "x", 1)),
|
||||
y = math.floor(numeric_component(at, "y", 2)),
|
||||
width = width,
|
||||
height = height,
|
||||
}
|
||||
end
|
||||
|
||||
local function window_distance_squared(window, x, y)
|
||||
local wx, wy = window_center(window)
|
||||
local dx = wx - x
|
||||
local dy = wy - y
|
||||
return dx * dx + dy * dy
|
||||
end
|
||||
|
||||
local function sort_windows_by_visual_position(windows)
|
||||
table.sort(windows, function(left, right)
|
||||
local left_x, left_y = window_center(left)
|
||||
local right_x, right_y = window_center(right)
|
||||
|
||||
if math.abs(left_x - right_x) > 10 then
|
||||
return left_x < right_x
|
||||
end
|
||||
if math.abs(left_y - right_y) > 10 then
|
||||
return left_y < right_y
|
||||
end
|
||||
return tostring(left.address or "") < tostring(right.address or "")
|
||||
end)
|
||||
end
|
||||
|
||||
local function grouping_direction(window, anchor)
|
||||
local wx, wy = window_center(window)
|
||||
local ax, ay = window_center(anchor)
|
||||
local dx = wx - ax
|
||||
local dy = wy - ay
|
||||
|
||||
if math.abs(dx) >= math.abs(dy) then
|
||||
return dx >= 0 and "left" or "right"
|
||||
end
|
||||
return dy >= 0 and "up" or "down"
|
||||
end
|
||||
|
||||
local function grouping_directions(window, anchor)
|
||||
local primary = grouping_direction(window, anchor)
|
||||
local directions = { primary }
|
||||
for _, direction in ipairs({ "left", "right", "up", "down" }) do
|
||||
if direction ~= primary then
|
||||
directions[#directions + 1] = direction
|
||||
end
|
||||
end
|
||||
return directions
|
||||
end
|
||||
|
||||
local function workspace_window_count(workspace_id)
|
||||
local workspace = hl.get_workspace(tostring(workspace_id))
|
||||
if not workspace then
|
||||
return 0
|
||||
end
|
||||
return workspace.windows or tiled_window_count(workspace)
|
||||
end
|
||||
|
||||
local function find_empty_workspace(target_monitor, exclude_id)
|
||||
local unused_candidate = nil
|
||||
local elsewhere_empty_candidate = nil
|
||||
local target_monitor_name = target_monitor and target_monitor.name or nil
|
||||
|
||||
for i = 1, max_workspace do
|
||||
if i ~= exclude_id then
|
||||
local workspace = hl.get_workspace(tostring(i))
|
||||
|
||||
if not workspace then
|
||||
unused_candidate = unused_candidate or i
|
||||
elseif is_normal_workspace(workspace) and workspace_window_count(i) == 0 then
|
||||
local monitor = workspace.monitor
|
||||
if target_monitor_name and monitor and monitor.name == target_monitor_name then
|
||||
return i
|
||||
end
|
||||
elsewhere_empty_candidate = elsewhere_empty_candidate or i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return unused_candidate or elsewhere_empty_candidate
|
||||
end
|
||||
|
||||
ctx.command_line_contains = command_line_contains
|
||||
ctx.bind = bind
|
||||
ctx.exec = exec
|
||||
ctx.dispatch = dispatch
|
||||
ctx.shell_quote = shell_quote
|
||||
ctx.overview_trace = overview_trace
|
||||
ctx.window_selector = window_selector
|
||||
ctx.hyprexpo = hyprexpo
|
||||
ctx.hyprwinview = hyprwinview
|
||||
ctx.workspacehistory = workspacehistory
|
||||
ctx.apply_nstack_config = apply_nstack_config
|
||||
ctx.apply_hyprexpo_config = apply_hyprexpo_config
|
||||
ctx.apply_hyprwinview_config = apply_hyprwinview_config
|
||||
ctx.active_workspace = active_workspace
|
||||
ctx.active_workspace_id = active_workspace_id
|
||||
ctx.workspace_key = workspace_key
|
||||
ctx.current_workspace_layout = current_workspace_layout
|
||||
ctx.write_layout_state = write_layout_state
|
||||
ctx.is_normal_workspace = is_normal_workspace
|
||||
ctx.same_workspace = same_workspace
|
||||
ctx.is_minimized_workspace = is_minimized_workspace
|
||||
ctx.is_minimized_window = is_minimized_window
|
||||
ctx.is_normal_window = is_normal_window
|
||||
ctx.tiled_windows = tiled_windows
|
||||
ctx.tiled_window_count = tiled_window_count
|
||||
ctx.sort_windows_by_focus_history = sort_windows_by_focus_history
|
||||
ctx.window_address_set = window_address_set
|
||||
ctx.window_address_list = window_address_list
|
||||
ctx.window_address_in_set = window_address_in_set
|
||||
ctx.windows_by_address = windows_by_address
|
||||
ctx.numeric_component = numeric_component
|
||||
ctx.window_center = window_center
|
||||
ctx.tiled_window_geometry = tiled_window_geometry
|
||||
ctx.window_distance_squared = window_distance_squared
|
||||
ctx.sort_windows_by_visual_position = sort_windows_by_visual_position
|
||||
ctx.grouping_direction = grouping_direction
|
||||
ctx.grouping_directions = grouping_directions
|
||||
ctx.workspace_window_count = workspace_window_count
|
||||
ctx.find_empty_workspace = find_empty_workspace
|
||||
end
|
||||
|
||||
return M
|
||||
96
dotfiles/config/hypr/hyprland/events.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local fullscreen_states = {}
|
||||
|
||||
local function unset_fullscreen_state(window, state)
|
||||
dispatch(hl.dsp.window.fullscreen_state({
|
||||
internal = state.internal,
|
||||
client = state.client,
|
||||
action = "unset",
|
||||
window = window_selector(window),
|
||||
}))
|
||||
end
|
||||
|
||||
local function reconcile_fullscreen_state(window)
|
||||
if not window or not window.address then
|
||||
return
|
||||
end
|
||||
|
||||
local address = tostring(window.address)
|
||||
local previous = fullscreen_states[address]
|
||||
local current = {
|
||||
internal = tonumber(window.fullscreen) or 0,
|
||||
client = tonumber(window.fullscreen_client) or 0,
|
||||
}
|
||||
fullscreen_states[address] = current
|
||||
|
||||
if window.floating or current_layout == monocle_layout then
|
||||
return
|
||||
end
|
||||
|
||||
if current.internal == 1 or (previous and previous.internal >= 2 and current.internal > 0 and current.client == 0) then
|
||||
unset_fullscreen_state(window, current)
|
||||
fullscreen_states[address] = { internal = 0, client = 0 }
|
||||
end
|
||||
end
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
apply_nstack_config()
|
||||
apply_hyprexpo_config()
|
||||
apply_hyprwinview_config()
|
||||
apply_hyprwobbly_config()
|
||||
apply_hyprglass_config()
|
||||
apply_visual_performance_mode()
|
||||
apply_rules()
|
||||
if not dev_session then
|
||||
hl.exec_cmd("sh -lc '/run/current-system/sw/bin/uwsm finalize HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE XAUTHORITY IMALISON_SESSION_TYPE=wayland IMALISON_WINDOW_MANAGER=hyprland || dbus-update-activation-environment --systemd XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE IMALISON_WINDOW_MANAGER; systemctl --user start hyprland-session.target'")
|
||||
hl.exec_cmd("hypridle")
|
||||
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
||||
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
||||
end
|
||||
write_layout_state()
|
||||
schedule_nstack_count_update()
|
||||
refresh_monitor_reserved_cache(0.25)
|
||||
refresh_monitor_reserved_cache(1.25)
|
||||
end)
|
||||
|
||||
hl.on("config.reloaded", apply_nstack_config)
|
||||
hl.on("config.reloaded", apply_hyprexpo_config)
|
||||
hl.on("config.reloaded", apply_hyprwinview_config)
|
||||
hl.on("config.reloaded", apply_hyprwobbly_config)
|
||||
hl.on("config.reloaded", apply_hyprglass_config)
|
||||
hl.on("config.reloaded", apply_visual_performance_mode)
|
||||
hl.on("config.reloaded", apply_rules)
|
||||
hl.on("config.reloaded", refresh_shell_workarea_and_scratchpads)
|
||||
hl.on("layer.opened", refresh_shell_workarea_and_scratchpads)
|
||||
hl.on("layer.closed", refresh_shell_workarea_and_scratchpads)
|
||||
hl.on("monitor.added", refresh_shell_workarea_and_scratchpads)
|
||||
hl.on("monitor.removed", refresh_shell_workarea_and_scratchpads)
|
||||
hl.on("monitor.layout_changed", refresh_shell_workarea_and_scratchpads)
|
||||
|
||||
hl.on("window.open", schedule_nstack_count_update)
|
||||
hl.on("window.destroy", schedule_nstack_count_update)
|
||||
hl.on("window.kill", schedule_nstack_count_update)
|
||||
hl.on("window.move_to_workspace", schedule_nstack_count_update)
|
||||
hl.on("workspace.active", sync_layout_for_active_workspace)
|
||||
hl.on("monitor.focused", sync_layout_for_active_workspace)
|
||||
|
||||
hl.on("window.open", update_monocle_notice)
|
||||
hl.on("window.destroy", update_monocle_notice)
|
||||
hl.on("window.kill", update_monocle_notice)
|
||||
hl.on("window.move_to_workspace", update_monocle_notice)
|
||||
hl.on("window.fullscreen", reconcile_fullscreen_state)
|
||||
hl.on("window.update_rules", reconcile_fullscreen_state)
|
||||
|
||||
hl.on("window.open", adopt_matching_scratchpad_window)
|
||||
hl.on("window.class", adopt_matching_scratchpad_window)
|
||||
hl.on("window.title", adopt_matching_scratchpad_window)
|
||||
|
||||
hl.on("window.open", raise_file_chooser_window_later)
|
||||
hl.on("window.class", raise_file_chooser_window_later)
|
||||
hl.on("window.title", raise_file_chooser_window_later)
|
||||
end
|
||||
|
||||
return M
|
||||
596
dotfiles/config/hypr/hyprland/layouts.lua
Normal file
@@ -0,0 +1,596 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local function is_nstack_layout(layout)
|
||||
return layout == columns_layout or layout == grid_layout
|
||||
end
|
||||
|
||||
local function hyprland_layout(layout)
|
||||
if layout == grid_layout then
|
||||
return columns_layout
|
||||
end
|
||||
return layout
|
||||
end
|
||||
|
||||
local function update_nstack_count()
|
||||
if not enable_nstack or not is_nstack_layout(current_layout) then
|
||||
return
|
||||
end
|
||||
|
||||
local workspace = hl.get_active_workspace()
|
||||
local count = tiled_window_count(workspace)
|
||||
if count == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local stack_count = count
|
||||
if current_layout == grid_layout then
|
||||
stack_count = math.ceil(math.sqrt(count))
|
||||
end
|
||||
|
||||
stack_count = math.max(stack_count, 2)
|
||||
dispatch(hl.dsp.layout("setstackcount " .. tostring(stack_count)))
|
||||
end
|
||||
|
||||
local function schedule_nstack_count_update()
|
||||
if stack_update_timer then
|
||||
stack_update_timer:set_enabled(false)
|
||||
end
|
||||
|
||||
stack_update_timer = hl.timer(update_nstack_count, { timeout = 25, type = "oneshot" })
|
||||
end
|
||||
|
||||
local function dismiss_monocle_notice()
|
||||
if monocle_notice and monocle_notice:is_alive() then
|
||||
monocle_notice:dismiss()
|
||||
end
|
||||
monocle_notice = nil
|
||||
end
|
||||
|
||||
local function update_monocle_notice()
|
||||
if current_layout ~= monocle_layout then
|
||||
dismiss_monocle_notice()
|
||||
return
|
||||
end
|
||||
|
||||
local workspace = hl.get_active_workspace()
|
||||
local count = tiled_window_count(workspace)
|
||||
if count <= 1 then
|
||||
dismiss_monocle_notice()
|
||||
return
|
||||
end
|
||||
|
||||
local text = "Monocle: " .. tostring(count) .. " windows"
|
||||
if monocle_notice and monocle_notice:is_alive() then
|
||||
monocle_notice:set_text(text)
|
||||
monocle_notice:set_timeout(60000)
|
||||
monocle_notice:pause()
|
||||
else
|
||||
monocle_notice = hl.notification.create({
|
||||
text = text,
|
||||
duration = 60000,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
monocle_notice:pause()
|
||||
end
|
||||
end
|
||||
|
||||
local function layout_name(layout)
|
||||
return layout_names[layout] or tostring(layout)
|
||||
end
|
||||
|
||||
local function notify_layout(layout)
|
||||
hl.notification.create({
|
||||
text = "Layout: " .. layout_name(layout),
|
||||
duration = 1200,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
|
||||
local function set_layout(layout)
|
||||
workspace_layouts[workspace_key()] = layout
|
||||
current_layout = layout
|
||||
hl.config({ general = { layout = hyprland_layout(layout) } })
|
||||
write_layout_state()
|
||||
|
||||
if is_nstack_layout(layout) then
|
||||
dismiss_monocle_notice()
|
||||
schedule_nstack_count_update()
|
||||
else
|
||||
update_monocle_notice()
|
||||
end
|
||||
end
|
||||
|
||||
_G.im_hyprland_set_layout = function(layout)
|
||||
if not layout_names[layout] then
|
||||
hl.notification.create({
|
||||
text = "Unknown layout: " .. tostring(layout),
|
||||
duration = 1800,
|
||||
icon = notification_icons.warning,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
set_layout(layout)
|
||||
notify_layout(layout)
|
||||
end
|
||||
|
||||
local function sync_layout_for_active_workspace()
|
||||
current_layout = current_workspace_layout()
|
||||
hl.config({ general = { layout = hyprland_layout(current_layout) } })
|
||||
write_layout_state()
|
||||
|
||||
if is_nstack_layout(current_layout) then
|
||||
dismiss_monocle_notice()
|
||||
schedule_nstack_count_update()
|
||||
else
|
||||
update_monocle_notice()
|
||||
end
|
||||
end
|
||||
|
||||
local function cycle_layout(delta)
|
||||
local current_index = 1
|
||||
for index, layout in ipairs(layout_cycle) do
|
||||
if layout == current_layout then
|
||||
current_index = index
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local next_index = ((current_index - 1 + delta) % #layout_cycle) + 1
|
||||
local next_layout = layout_cycle[next_index]
|
||||
set_layout(next_layout)
|
||||
notify_layout(next_layout)
|
||||
end
|
||||
|
||||
local function toggle_columns_monocle()
|
||||
if current_layout == columns_layout then
|
||||
set_layout(monocle_layout)
|
||||
else
|
||||
set_layout(columns_layout)
|
||||
end
|
||||
end
|
||||
|
||||
local function active_group_size()
|
||||
local window = hl.get_active_window()
|
||||
return window and window.group and window.group.size or 0
|
||||
end
|
||||
|
||||
local function monocle_next()
|
||||
local window = hl.get_active_window()
|
||||
if window and window.group and window.group.size and window.group.size > 1 then
|
||||
dispatch(hl.dsp.group.next({ window = window_selector(window) }))
|
||||
elseif current_layout == monocle_layout then
|
||||
dispatch(hl.dsp.layout("cyclenext"))
|
||||
update_monocle_notice()
|
||||
else
|
||||
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
|
||||
end
|
||||
end
|
||||
|
||||
local function monocle_prev()
|
||||
local window = hl.get_active_window()
|
||||
if window and window.group and window.group.size and window.group.size > 1 then
|
||||
dispatch(hl.dsp.group.prev({ window = window_selector(window) }))
|
||||
elseif current_layout == monocle_layout then
|
||||
dispatch(hl.dsp.layout("cycleprev"))
|
||||
update_monocle_notice()
|
||||
else
|
||||
dispatch(hl.dsp.window.cycle_next({ next = false, tiled = true, floating = false }))
|
||||
end
|
||||
end
|
||||
|
||||
local function focus_direction(direction)
|
||||
overview_trace("focus_direction " .. direction)
|
||||
if active_group_size() > 1 or current_layout == monocle_layout then
|
||||
if direction == "up" or direction == "left" then
|
||||
monocle_prev()
|
||||
else
|
||||
monocle_next()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.focus({ direction = direction }))
|
||||
end
|
||||
|
||||
local function swap_direction(direction)
|
||||
if enable_nstack and is_nstack_layout(current_layout) and active_group_size() <= 1 then
|
||||
dispatch(hl.dsp.layout("swapdirection " .. direction))
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.window.swap({ direction = direction }))
|
||||
end
|
||||
|
||||
local function focus_workspace(workspace_id)
|
||||
dispatch(hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true }))
|
||||
end
|
||||
|
||||
local function move_window_to_workspace(workspace_id, follow, window)
|
||||
local target_window = window or hl.get_active_window()
|
||||
local target_selector = window_selector(target_window)
|
||||
dispatch(hl.dsp.window.move({ workspace = tostring(workspace_id), follow = false, window = target_selector }))
|
||||
if follow then
|
||||
focus_workspace(workspace_id)
|
||||
if target_selector then
|
||||
dispatch(hl.dsp.focus({ window = target_selector }))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function notify_tabbed_group(text)
|
||||
hl.notification.create({
|
||||
text = text,
|
||||
duration = 1800,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
|
||||
local function active_workspace_tiled_group_candidates(workspace)
|
||||
local candidates = tiled_windows(workspace)
|
||||
sort_windows_by_focus_history(candidates)
|
||||
return candidates
|
||||
end
|
||||
|
||||
local function move_window_into_group(window, anchor)
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, direction in ipairs(grouping_directions(window, anchor)) do
|
||||
dispatch(hl.dsp.focus({ window = selector }))
|
||||
dispatch(hl.dsp.window.move({ into_group = direction, window = selector }))
|
||||
|
||||
local active = hl.get_active_window()
|
||||
if active and active.group and active.group.size and active.group.size > 1 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function find_tabbed_group_anchor(state)
|
||||
local active = hl.get_active_window()
|
||||
if active and active.group and active.group.size and active.group.size > 1 then
|
||||
return active
|
||||
end
|
||||
|
||||
if not state then
|
||||
return nil
|
||||
end
|
||||
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
|
||||
return window
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ordered_windows_for_tabbed_group_restore(state, workspace_id)
|
||||
local ordered = {}
|
||||
local seen = {}
|
||||
local live_windows = windows_by_address()
|
||||
local workspace = workspace_id and hl.get_workspace(tostring(workspace_id)) or active_workspace()
|
||||
|
||||
if state and state.order then
|
||||
for _, address in ipairs(state.order) do
|
||||
local window = live_windows[address]
|
||||
if window and not window.floating and not window.hidden and (not workspace or same_workspace(window.workspace, workspace)) then
|
||||
ordered[#ordered + 1] = window
|
||||
seen[address] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if workspace then
|
||||
for _, window in ipairs(tiled_windows(workspace)) do
|
||||
if window and window.address and not seen[window.address] then
|
||||
ordered[#ordered + 1] = window
|
||||
seen[window.address] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ordered
|
||||
end
|
||||
|
||||
local function restore_tabbed_group_window_order(state, workspace_id)
|
||||
local ordered = ordered_windows_for_tabbed_group_restore(state, workspace_id)
|
||||
if #ordered <= 1 or not workspace_id then
|
||||
return
|
||||
end
|
||||
|
||||
local restore_workspace = tabbed_group_restore_workspace_prefix .. tostring(workspace_id)
|
||||
for _, window in ipairs(ordered) do
|
||||
move_window_to_workspace(restore_workspace, false, window)
|
||||
end
|
||||
|
||||
for _, window in ipairs(ordered) do
|
||||
move_window_to_workspace(workspace_id, false, window)
|
||||
end
|
||||
end
|
||||
|
||||
local function restore_workspace_tabbed_group()
|
||||
local key = workspace_key()
|
||||
local state = tabbed_workspace_groups[key]
|
||||
local anchor = find_tabbed_group_anchor(state)
|
||||
local anchor_selector = window_selector(anchor)
|
||||
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
|
||||
|
||||
if not anchor_selector then
|
||||
tabbed_workspace_groups[key] = nil
|
||||
set_layout(columns_layout)
|
||||
notify_tabbed_group("No tabbed group to restore")
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
||||
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
|
||||
tabbed_workspace_groups[key] = nil
|
||||
set_layout(columns_layout)
|
||||
restore_tabbed_group_window_order(state, target_workspace_id)
|
||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
||||
schedule_nstack_count_update()
|
||||
end
|
||||
|
||||
local function gather_workspace_into_tabbed_group()
|
||||
local workspace = active_workspace()
|
||||
if not is_normal_workspace(workspace) then
|
||||
return
|
||||
end
|
||||
|
||||
local key = workspace_key(workspace)
|
||||
if tabbed_workspace_groups[key] or active_group_size() > 1 then
|
||||
restore_workspace_tabbed_group()
|
||||
return
|
||||
end
|
||||
|
||||
local original_windows = tiled_windows(workspace)
|
||||
sort_windows_by_visual_position(original_windows)
|
||||
local original_order = window_address_list(original_windows)
|
||||
local candidates = active_workspace_tiled_group_candidates(workspace)
|
||||
if #candidates <= 1 then
|
||||
set_layout(columns_layout)
|
||||
return
|
||||
end
|
||||
|
||||
local candidate_addresses = window_address_set(candidates)
|
||||
local focused = hl.get_active_window()
|
||||
local anchor = nil
|
||||
if focused and not focused.floating and not focused.group and window_address_in_set(focused, candidate_addresses) then
|
||||
anchor = focused
|
||||
end
|
||||
|
||||
if not anchor then
|
||||
for _, window in ipairs(candidates) do
|
||||
if not window.group then
|
||||
anchor = window
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local anchor_selector = window_selector(anchor)
|
||||
if not anchor_selector then
|
||||
notify_tabbed_group("Current tiled windows are already grouped")
|
||||
return
|
||||
end
|
||||
|
||||
set_layout(columns_layout)
|
||||
|
||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
||||
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
|
||||
|
||||
local group_windows = {}
|
||||
for _, window in ipairs(candidates) do
|
||||
if window ~= anchor and not window.group then
|
||||
group_windows[#group_windows + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
local anchor_x, anchor_y = window_center(anchor)
|
||||
table.sort(group_windows, function(left, right)
|
||||
return window_distance_squared(left, anchor_x, anchor_y) < window_distance_squared(right, anchor_x, anchor_y)
|
||||
end)
|
||||
|
||||
local grouped_count = 1
|
||||
for _, window in ipairs(group_windows) do
|
||||
if move_window_into_group(window, anchor) then
|
||||
grouped_count = grouped_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
if grouped_count <= 1 then
|
||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
||||
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
|
||||
notify_tabbed_group("Unable to group tiled windows")
|
||||
return
|
||||
elseif grouped_count < #candidates then
|
||||
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
|
||||
end
|
||||
|
||||
tabbed_workspace_groups[key] = {
|
||||
anchor = anchor.address,
|
||||
order = original_order,
|
||||
windows = candidate_addresses,
|
||||
}
|
||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
||||
end
|
||||
|
||||
local function force_columns_layout()
|
||||
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
|
||||
restore_workspace_tabbed_group()
|
||||
else
|
||||
set_layout(columns_layout)
|
||||
end
|
||||
end
|
||||
|
||||
local function cycle_layout_or_restore_tabbed_group()
|
||||
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
|
||||
restore_workspace_tabbed_group()
|
||||
return
|
||||
end
|
||||
|
||||
cycle_layout(1)
|
||||
end
|
||||
|
||||
local function copy_windows(workspace)
|
||||
local windows = {}
|
||||
if not workspace then
|
||||
return windows
|
||||
end
|
||||
|
||||
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
|
||||
if window and not window.hidden then
|
||||
windows[#windows + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
return windows
|
||||
end
|
||||
|
||||
local function swap_current_workspace_with(target_id)
|
||||
local current = active_workspace()
|
||||
if not current or not current.id or current.id == target_id then
|
||||
return
|
||||
end
|
||||
|
||||
local target = hl.get_workspace(tostring(target_id))
|
||||
local current_windows = copy_windows(current)
|
||||
local target_windows = copy_windows(target)
|
||||
|
||||
for _, window in ipairs(current_windows) do
|
||||
move_window_to_workspace(target_id, false, window)
|
||||
end
|
||||
|
||||
for _, window in ipairs(target_windows) do
|
||||
move_window_to_workspace(current.id, false, window)
|
||||
end
|
||||
|
||||
focus_workspace(current.id)
|
||||
end
|
||||
|
||||
local function enter_workspace_swap_mode()
|
||||
hl.notification.create({
|
||||
text = "Swap with workspace 1-9",
|
||||
duration = 2200,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
dispatch(hl.dsp.submap("swap-workspace"))
|
||||
end
|
||||
|
||||
local function focus_next_empty_workspace()
|
||||
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
|
||||
if workspace_id then
|
||||
focus_workspace(workspace_id)
|
||||
end
|
||||
end
|
||||
|
||||
local function move_to_next_empty_workspace(follow)
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
|
||||
if workspace_id then
|
||||
move_window_to_workspace(workspace_id, follow, window)
|
||||
end
|
||||
end
|
||||
|
||||
local function cycle_workspace(delta)
|
||||
local current = active_workspace_id()
|
||||
local next_workspace = ((current - 1 + delta) % max_workspace) + 1
|
||||
focus_workspace(next_workspace)
|
||||
end
|
||||
|
||||
local function move_window_to_monitor(direction, follow)
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
local original_monitor = hl.get_active_monitor()
|
||||
dispatch(hl.dsp.window.move({ monitor = direction, follow = follow, window = window_selector(window) }))
|
||||
|
||||
if not follow and original_monitor then
|
||||
dispatch(hl.dsp.focus({ monitor = original_monitor }))
|
||||
end
|
||||
end
|
||||
|
||||
local function move_window_to_empty_workspace_on_monitor(direction)
|
||||
local window = hl.get_active_window()
|
||||
local original_monitor = hl.get_active_monitor()
|
||||
local target_monitor = hl.get_monitor(direction)
|
||||
|
||||
if not window or not original_monitor or not target_monitor or target_monitor == original_monitor then
|
||||
return
|
||||
end
|
||||
|
||||
local workspace_id = find_empty_workspace(target_monitor, active_workspace_id())
|
||||
if not workspace_id then
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.focus({ monitor = target_monitor }))
|
||||
focus_workspace(workspace_id)
|
||||
dispatch(hl.dsp.focus({ monitor = original_monitor }))
|
||||
move_window_to_workspace(workspace_id, false, window)
|
||||
end
|
||||
|
||||
ctx.is_nstack_layout = is_nstack_layout
|
||||
ctx.hyprland_layout = hyprland_layout
|
||||
ctx.update_nstack_count = update_nstack_count
|
||||
ctx.schedule_nstack_count_update = schedule_nstack_count_update
|
||||
ctx.dismiss_monocle_notice = dismiss_monocle_notice
|
||||
ctx.update_monocle_notice = update_monocle_notice
|
||||
ctx.layout_name = layout_name
|
||||
ctx.notify_layout = notify_layout
|
||||
ctx.set_layout = set_layout
|
||||
ctx.sync_layout_for_active_workspace = sync_layout_for_active_workspace
|
||||
ctx.cycle_layout = cycle_layout
|
||||
ctx.toggle_columns_monocle = toggle_columns_monocle
|
||||
ctx.active_group_size = active_group_size
|
||||
ctx.monocle_next = monocle_next
|
||||
ctx.monocle_prev = monocle_prev
|
||||
ctx.focus_direction = focus_direction
|
||||
ctx.swap_direction = swap_direction
|
||||
ctx.focus_workspace = focus_workspace
|
||||
ctx.move_window_to_workspace = move_window_to_workspace
|
||||
ctx.notify_tabbed_group = notify_tabbed_group
|
||||
ctx.active_workspace_tiled_group_candidates = active_workspace_tiled_group_candidates
|
||||
ctx.move_window_into_group = move_window_into_group
|
||||
ctx.find_tabbed_group_anchor = find_tabbed_group_anchor
|
||||
ctx.ordered_windows_for_tabbed_group_restore = ordered_windows_for_tabbed_group_restore
|
||||
ctx.restore_tabbed_group_window_order = restore_tabbed_group_window_order
|
||||
ctx.restore_workspace_tabbed_group = restore_workspace_tabbed_group
|
||||
ctx.gather_workspace_into_tabbed_group = gather_workspace_into_tabbed_group
|
||||
ctx.force_columns_layout = force_columns_layout
|
||||
ctx.cycle_layout_or_restore_tabbed_group = cycle_layout_or_restore_tabbed_group
|
||||
ctx.copy_windows = copy_windows
|
||||
ctx.swap_current_workspace_with = swap_current_workspace_with
|
||||
ctx.enter_workspace_swap_mode = enter_workspace_swap_mode
|
||||
ctx.focus_next_empty_workspace = focus_next_empty_workspace
|
||||
ctx.move_to_next_empty_workspace = move_to_next_empty_workspace
|
||||
ctx.cycle_workspace = cycle_workspace
|
||||
ctx.move_window_to_monitor = move_window_to_monitor
|
||||
ctx.move_window_to_empty_workspace_on_monitor = move_window_to_empty_workspace_on_monitor
|
||||
end
|
||||
|
||||
return M
|
||||
490
dotfiles/config/hypr/hyprland/scratchpads.lua
Normal file
@@ -0,0 +1,490 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
|
||||
scratchpad_size_ratio = 0.95
|
||||
dropdown_height_ratio = 0.5
|
||||
dropdown_animation_frames = 18
|
||||
dropdown_animation_frame_ms = 16
|
||||
scratchpad_pending = {}
|
||||
monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv"
|
||||
scratchpad_fallback_reserved_top = 60
|
||||
|
||||
scratchpads = {
|
||||
codex = {
|
||||
command = "codex_desktop_scratchpad",
|
||||
class = "codex-desktop",
|
||||
},
|
||||
htop = {
|
||||
command = "alacritty --class htop-scratch --title htop -e htop",
|
||||
class = "htop-scratch",
|
||||
},
|
||||
volume = {
|
||||
command = "pavucontrol",
|
||||
class = "org.pulseaudio.pavucontrol",
|
||||
},
|
||||
spotify = {
|
||||
command = "spotify",
|
||||
class = "spotify",
|
||||
},
|
||||
element = {
|
||||
command = "element-desktop",
|
||||
classes = { "Element", "electron" },
|
||||
title = "Element",
|
||||
},
|
||||
slack = {
|
||||
command = "slack",
|
||||
class = "Slack",
|
||||
},
|
||||
messages = {
|
||||
command = "google-chrome-stable --profile-directory=Default --app=https://messages.google.com/web/conversations",
|
||||
class = "chrome-messages.google.com",
|
||||
},
|
||||
transmission = {
|
||||
command = "transmission-gtk",
|
||||
class = "transmission-gtk",
|
||||
},
|
||||
dropdown = {
|
||||
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown",
|
||||
class = "com.mitchellh.ghostty.dropdown",
|
||||
dropdown = true,
|
||||
},
|
||||
}
|
||||
|
||||
local function lower_contains(value, needle)
|
||||
if not needle or needle == "" then
|
||||
return true
|
||||
end
|
||||
|
||||
value = string.lower(tostring(value or ""))
|
||||
needle = string.lower(tostring(needle))
|
||||
return value:find(needle, 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function lower_contains_any(value, needles)
|
||||
if type(needles) ~= "table" then
|
||||
return lower_contains(value, needles)
|
||||
end
|
||||
|
||||
for _, needle in ipairs(needles) do
|
||||
if lower_contains(value, needle) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function scratchpad_window_matches(window, def)
|
||||
return window
|
||||
and not (type(is_file_chooser_window) == "function" and is_file_chooser_window(window))
|
||||
and lower_contains_any(window.class, def.classes or def.class)
|
||||
and lower_contains(window.title, def.title)
|
||||
end
|
||||
|
||||
local function is_scratchpad_window(window)
|
||||
for _, def in pairs(scratchpads) do
|
||||
if scratchpad_window_matches(window, def) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function matching_scratchpad_name(window)
|
||||
for name, def in pairs(scratchpads) do
|
||||
if scratchpad_window_matches(window, def) then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function scratchpad_workspace(name)
|
||||
return "special:scratch-" .. name
|
||||
end
|
||||
|
||||
local function as_number(value, default)
|
||||
local number = tonumber(value)
|
||||
if number == nil then
|
||||
return default
|
||||
end
|
||||
return number
|
||||
end
|
||||
|
||||
local function logical_monitor_dimension(value, scale)
|
||||
value = as_number(value, 0)
|
||||
scale = as_number(scale, 1)
|
||||
if scale <= 0 then
|
||||
scale = 1
|
||||
end
|
||||
return math.floor((value / scale) + 0.5)
|
||||
end
|
||||
|
||||
local function split_tsv(line)
|
||||
local fields = {}
|
||||
for field in (line .. "\t"):gmatch("([^\t]*)\t") do
|
||||
fields[#fields + 1] = field
|
||||
end
|
||||
return fields
|
||||
end
|
||||
|
||||
local function monitor_from_reserved_fields(monitor, fields)
|
||||
if not monitor or not monitor.name or fields[1] ~= monitor.name or #fields < 10 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
name = monitor.name,
|
||||
x = tonumber(fields[2]),
|
||||
y = tonumber(fields[3]),
|
||||
width = tonumber(fields[4]),
|
||||
height = tonumber(fields[5]),
|
||||
scale = tonumber(fields[6]),
|
||||
reserved = {
|
||||
tonumber(fields[7]),
|
||||
tonumber(fields[8]),
|
||||
tonumber(fields[9]),
|
||||
tonumber(fields[10]),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local function monitor_from_reserved_lines(monitor, lines)
|
||||
if not monitor or not monitor.name then
|
||||
return nil
|
||||
end
|
||||
|
||||
for line in lines do
|
||||
local cached = monitor_from_reserved_fields(monitor, split_tsv(line))
|
||||
if cached then
|
||||
return cached
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function monitor_from_reserved_cache(monitor)
|
||||
if verify_config or not monitor or not monitor.name then
|
||||
return nil
|
||||
end
|
||||
|
||||
local file = io.open(monitor_reserved_cache_path, "r")
|
||||
if not file then
|
||||
return nil
|
||||
end
|
||||
|
||||
local cached = monitor_from_reserved_lines(monitor, file:lines())
|
||||
file:close()
|
||||
return cached
|
||||
end
|
||||
|
||||
local function refresh_monitor_reserved_cache(delay)
|
||||
if verify_config then
|
||||
return
|
||||
end
|
||||
|
||||
local command = string.format(
|
||||
[=[sleep %.2f; cache="${XDG_RUNTIME_DIR:-/tmp}/hyprland-monitor-reserved.tsv"; tmp="$cache.tmp"; /run/current-system/sw/bin/hyprctl -j monitors 2>/dev/null | /run/current-system/sw/bin/jq -r '.[] | [.name, .x, .y, .width, .height, .scale, .reserved[0], .reserved[1], .reserved[2], .reserved[3]] | @tsv' > "$tmp" && mv "$tmp" "$cache"]=],
|
||||
as_number(delay, 0)
|
||||
)
|
||||
hl.exec_cmd("sh -lc " .. shell_quote(command))
|
||||
end
|
||||
|
||||
local function monitor_workarea(monitor)
|
||||
monitor = monitor_from_reserved_cache(monitor) or monitor
|
||||
local width = logical_monitor_dimension(monitor.width, monitor.scale)
|
||||
local height = logical_monitor_dimension(monitor.height, monitor.scale)
|
||||
local reserved = monitor.reserved or { 0, scratchpad_fallback_reserved_top, 0, 0 }
|
||||
local left = math.floor(as_number(reserved[1], 0))
|
||||
local top = math.floor(as_number(reserved[2], 0))
|
||||
local right = math.floor(as_number(reserved[3], 0))
|
||||
local bottom = math.floor(as_number(reserved[4], 0))
|
||||
local work_width = width - left - right
|
||||
local work_height = height - top - bottom
|
||||
|
||||
if work_width <= 0 then
|
||||
left = 0
|
||||
right = 0
|
||||
work_width = width
|
||||
end
|
||||
if work_height <= 0 then
|
||||
top = 0
|
||||
bottom = 0
|
||||
work_height = height
|
||||
end
|
||||
|
||||
return {
|
||||
x = math.floor(as_number(monitor.x, 0)) + left,
|
||||
y = math.floor(as_number(monitor.y, 0)) + top,
|
||||
width = work_width,
|
||||
height = work_height,
|
||||
}
|
||||
end
|
||||
|
||||
local function matching_scratchpad_windows(name)
|
||||
local def = scratchpads[name]
|
||||
local windows = {}
|
||||
if not def then
|
||||
return windows
|
||||
end
|
||||
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if scratchpad_window_matches(window, def) then
|
||||
windows[#windows + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
return windows
|
||||
end
|
||||
|
||||
local function scratchpad_geometry(name, target_monitor, position)
|
||||
local def = scratchpads[name]
|
||||
local monitor = target_monitor or hl.get_active_monitor()
|
||||
if not def or not monitor then
|
||||
return
|
||||
end
|
||||
|
||||
local workarea = monitor_workarea(monitor)
|
||||
local width
|
||||
local height
|
||||
local x
|
||||
local y
|
||||
if def.dropdown then
|
||||
width = workarea.width
|
||||
height = math.floor(workarea.height * dropdown_height_ratio)
|
||||
x = workarea.x
|
||||
y = workarea.y
|
||||
if position == "above" then
|
||||
y = workarea.y - height
|
||||
elseif type(position) == "number" then
|
||||
y = position
|
||||
end
|
||||
else
|
||||
width = math.floor(workarea.width * scratchpad_size_ratio)
|
||||
height = math.floor(workarea.height * scratchpad_size_ratio)
|
||||
x = workarea.x + math.floor((workarea.width - width) / 2)
|
||||
y = workarea.y + math.floor((workarea.height - height) / 2)
|
||||
end
|
||||
|
||||
return {
|
||||
width = width,
|
||||
height = height,
|
||||
x = x,
|
||||
y = y,
|
||||
}
|
||||
end
|
||||
|
||||
local function apply_scratchpad_geometry(name, window, target_monitor, position)
|
||||
local def = scratchpads[name]
|
||||
if not def or not window then
|
||||
return
|
||||
end
|
||||
|
||||
local geometry = scratchpad_geometry(name, target_monitor, position)
|
||||
if not geometry then
|
||||
return
|
||||
end
|
||||
local selector = window_selector(window)
|
||||
|
||||
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
|
||||
dispatch(hl.dsp.window.tag({ tag = "+scratchpad", window = selector }))
|
||||
dispatch(hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector }))
|
||||
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = selector }))
|
||||
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = selector }))
|
||||
if def.dropdown then
|
||||
dispatch(hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = selector }))
|
||||
dispatch(hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = selector }))
|
||||
end
|
||||
end
|
||||
|
||||
local function schedule_scratchpad_geometry(name, window, target_monitor, position, timeout)
|
||||
hl.timer(function()
|
||||
apply_scratchpad_geometry(name, window, target_monitor, position)
|
||||
end, { timeout = timeout or 50, type = "oneshot" })
|
||||
end
|
||||
|
||||
local function dropdown_spring_progress(progress)
|
||||
if progress >= 1 then
|
||||
return 1
|
||||
end
|
||||
return 1 - (math.exp(-5.0 * progress) * math.cos(7.0 * progress))
|
||||
end
|
||||
|
||||
local function animate_dropdown_scratchpad_down(name, window, target_monitor)
|
||||
local from = scratchpad_geometry(name, target_monitor, "above")
|
||||
local to = scratchpad_geometry(name, target_monitor)
|
||||
if not from or not to then
|
||||
schedule_scratchpad_geometry(name, window, target_monitor, nil, 35)
|
||||
return
|
||||
end
|
||||
|
||||
for frame = 1, dropdown_animation_frames do
|
||||
local progress = frame / dropdown_animation_frames
|
||||
local eased = dropdown_spring_progress(progress)
|
||||
local y = math.floor(from.y + ((to.y - from.y) * eased) + 0.5)
|
||||
schedule_scratchpad_geometry(name, window, target_monitor, y, frame * dropdown_animation_frame_ms)
|
||||
end
|
||||
end
|
||||
|
||||
local function hide_scratchpad_window(name, window)
|
||||
remove_minimized_window(window)
|
||||
move_window_to_workspace(scratchpad_workspace(name), false, window)
|
||||
end
|
||||
|
||||
local function show_scratchpad_window(name, window, workspace, target_monitor)
|
||||
workspace = workspace or active_workspace()
|
||||
if not workspace then
|
||||
return
|
||||
end
|
||||
|
||||
remove_minimized_window(window)
|
||||
if scratchpads[name] and scratchpads[name].dropdown then
|
||||
apply_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), "above")
|
||||
end
|
||||
move_window_to_workspace(workspace.id, false, window)
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
if scratchpads[name] and scratchpads[name].dropdown then
|
||||
animate_dropdown_scratchpad_down(name, window, target_monitor or hl.get_active_monitor())
|
||||
else
|
||||
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
|
||||
end
|
||||
end
|
||||
|
||||
local function scratchpad_is_visible(window)
|
||||
local workspace = active_workspace()
|
||||
return workspace and window and same_workspace(window.workspace, workspace)
|
||||
end
|
||||
|
||||
-- Active scratchpads are scratchpad windows visible on the active workspace.
|
||||
-- Invoking a different scratchpad replaces that active set.
|
||||
local function active_scratchpad_windows(except_name)
|
||||
local windows = {}
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
local name = matching_scratchpad_name(window)
|
||||
if name and name ~= except_name and scratchpad_is_visible(window) then
|
||||
windows[#windows + 1] = {
|
||||
name = name,
|
||||
window = window,
|
||||
}
|
||||
end
|
||||
end
|
||||
return windows
|
||||
end
|
||||
|
||||
local function hide_active_scratchpads(except_name)
|
||||
for _, active in ipairs(active_scratchpad_windows(except_name)) do
|
||||
hide_scratchpad_window(active.name, active.window)
|
||||
end
|
||||
end
|
||||
|
||||
local function refresh_active_scratchpad_geometries()
|
||||
local monitor = hl.get_active_monitor()
|
||||
for _, active in ipairs(active_scratchpad_windows()) do
|
||||
schedule_scratchpad_geometry(active.name, active.window, monitor)
|
||||
end
|
||||
end
|
||||
|
||||
local function refresh_active_scratchpad_geometries_later(timeout)
|
||||
hl.timer(refresh_active_scratchpad_geometries, { timeout = timeout or 300, type = "oneshot" })
|
||||
end
|
||||
|
||||
local function refresh_shell_workarea_and_scratchpads()
|
||||
refresh_monitor_reserved_cache(0.15)
|
||||
refresh_active_scratchpad_geometries_later(400)
|
||||
end
|
||||
|
||||
local function adopt_matching_scratchpad_window(window)
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
for name, def in pairs(scratchpads) do
|
||||
if scratchpad_window_matches(window, def) then
|
||||
if scratchpad_pending[name] then
|
||||
local pending = scratchpad_pending[name]
|
||||
scratchpad_pending[name] = nil
|
||||
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor())
|
||||
elseif scratchpad_is_visible(window) then
|
||||
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle_scratchpad(name)
|
||||
local def = scratchpads[name]
|
||||
if not def then
|
||||
return
|
||||
end
|
||||
|
||||
if current_layout == monocle_layout then
|
||||
set_layout(columns_layout)
|
||||
end
|
||||
|
||||
local windows = matching_scratchpad_windows(name)
|
||||
if #windows == 0 then
|
||||
hide_active_scratchpads(name)
|
||||
scratchpad_pending[name] = {
|
||||
monitor = hl.get_active_monitor(),
|
||||
workspace = active_workspace(),
|
||||
}
|
||||
hl.exec_cmd(def.command)
|
||||
return
|
||||
end
|
||||
|
||||
local any_visible = false
|
||||
for _, window in ipairs(windows) do
|
||||
if scratchpad_is_visible(window) then
|
||||
any_visible = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if any_visible then
|
||||
for _, window in ipairs(windows) do
|
||||
hide_scratchpad_window(name, window)
|
||||
end
|
||||
else
|
||||
hide_active_scratchpads(name)
|
||||
local workspace = active_workspace()
|
||||
local target_monitor = hl.get_active_monitor()
|
||||
for _, window in ipairs(windows) do
|
||||
show_scratchpad_window(name, window, workspace, target_monitor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ctx.lower_contains = lower_contains
|
||||
ctx.lower_contains_any = lower_contains_any
|
||||
ctx.scratchpad_window_matches = scratchpad_window_matches
|
||||
ctx.is_scratchpad_window = is_scratchpad_window
|
||||
ctx.matching_scratchpad_name = matching_scratchpad_name
|
||||
ctx.scratchpad_workspace = scratchpad_workspace
|
||||
ctx.as_number = as_number
|
||||
ctx.logical_monitor_dimension = logical_monitor_dimension
|
||||
ctx.split_tsv = split_tsv
|
||||
ctx.monitor_from_reserved_fields = monitor_from_reserved_fields
|
||||
ctx.monitor_from_reserved_lines = monitor_from_reserved_lines
|
||||
ctx.monitor_from_reserved_cache = monitor_from_reserved_cache
|
||||
ctx.refresh_monitor_reserved_cache = refresh_monitor_reserved_cache
|
||||
ctx.monitor_workarea = monitor_workarea
|
||||
ctx.scratchpad_geometry = scratchpad_geometry
|
||||
ctx.matching_scratchpad_windows = matching_scratchpad_windows
|
||||
ctx.apply_scratchpad_geometry = apply_scratchpad_geometry
|
||||
ctx.schedule_scratchpad_geometry = schedule_scratchpad_geometry
|
||||
ctx.dropdown_spring_progress = dropdown_spring_progress
|
||||
ctx.animate_dropdown_scratchpad_down = animate_dropdown_scratchpad_down
|
||||
ctx.hide_scratchpad_window = hide_scratchpad_window
|
||||
ctx.show_scratchpad_window = show_scratchpad_window
|
||||
ctx.scratchpad_is_visible = scratchpad_is_visible
|
||||
ctx.active_scratchpad_windows = active_scratchpad_windows
|
||||
ctx.hide_active_scratchpads = hide_active_scratchpads
|
||||
ctx.refresh_active_scratchpad_geometries = refresh_active_scratchpad_geometries
|
||||
ctx.refresh_active_scratchpad_geometries_later = refresh_active_scratchpad_geometries_later
|
||||
ctx.refresh_shell_workarea_and_scratchpads = refresh_shell_workarea_and_scratchpads
|
||||
ctx.adopt_matching_scratchpad_window = adopt_matching_scratchpad_window
|
||||
ctx.toggle_scratchpad = toggle_scratchpad
|
||||
end
|
||||
|
||||
return M
|
||||
422
dotfiles/config/hypr/hyprland/settings.lua
Normal file
@@ -0,0 +1,422 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local file_chooser_title_rule = "^(Open File|Open Files|Save File|Save Files|Save As|Select File|Select Files|Choose File|Choose Files|File Upload|Upload File|Upload Files|Select Folder|Choose Folder|Open Folder|Save Folder)$"
|
||||
|
||||
local function lower_string(value)
|
||||
return string.lower(tostring(value or ""))
|
||||
end
|
||||
|
||||
local function title_indicates_file_chooser(title)
|
||||
title = lower_string(title)
|
||||
if title == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, exact in ipairs({
|
||||
"open file",
|
||||
"open files",
|
||||
"save file",
|
||||
"save files",
|
||||
"save as",
|
||||
"select file",
|
||||
"select files",
|
||||
"choose file",
|
||||
"choose files",
|
||||
"file upload",
|
||||
"upload file",
|
||||
"upload files",
|
||||
"select folder",
|
||||
"choose folder",
|
||||
"open folder",
|
||||
"save folder",
|
||||
}) do
|
||||
if title == exact then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return title:find("file chooser", 1, true) ~= nil
|
||||
or title:find("file picker", 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function is_file_chooser_window(window)
|
||||
return window
|
||||
and (title_indicates_file_chooser(window.title) or title_indicates_file_chooser(window.initial_title))
|
||||
end
|
||||
|
||||
local function raise_file_chooser_window(window)
|
||||
if verify_config or not is_file_chooser_window(window) then
|
||||
return
|
||||
end
|
||||
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
|
||||
dispatch(hl.dsp.window.center({ window = selector }))
|
||||
dispatch(hl.dsp.focus({ window = selector }))
|
||||
dispatch(hl.dsp.window.bring_to_top({ window = selector }))
|
||||
end
|
||||
|
||||
local function raise_file_chooser_window_later(window, timeout)
|
||||
hl.timer(function()
|
||||
local refreshed = window and window.address and hl.get_window(window_selector(window)) or window
|
||||
raise_file_chooser_window(refreshed)
|
||||
end, { timeout = timeout or 50, type = "oneshot" })
|
||||
end
|
||||
|
||||
if enable_nstack and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so")
|
||||
end
|
||||
if enable_hyprexpo and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
|
||||
end
|
||||
if enable_hyprwinview and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprwinview.so")
|
||||
end
|
||||
if enable_workspace_history and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhypr-workspace-history.so")
|
||||
end
|
||||
if enable_hyprwobbly and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/libhyprwobbly.so")
|
||||
end
|
||||
if enable_hyprglass and not verify_config then
|
||||
hl.plugin.load("/run/current-system/sw/lib/hyprglass.so")
|
||||
end
|
||||
|
||||
hl.env("XCURSOR_SIZE", "24")
|
||||
hl.env("HYPRCURSOR_SIZE", "24")
|
||||
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
|
||||
hl.env("HYPR_MAX_WORKSPACE", "9")
|
||||
|
||||
hl.config({
|
||||
input = {
|
||||
kb_layout = "us",
|
||||
kb_variant = "",
|
||||
kb_model = "",
|
||||
kb_options = "",
|
||||
kb_rules = "",
|
||||
follow_mouse = 1,
|
||||
sensitivity = 0,
|
||||
touchpad = {
|
||||
natural_scroll = false,
|
||||
},
|
||||
},
|
||||
cursor = {
|
||||
persistent_warps = true,
|
||||
},
|
||||
general = {
|
||||
gaps_in = 5,
|
||||
gaps_out = 10,
|
||||
border_size = 2,
|
||||
col = {
|
||||
active_border = { colors = { "rgba(3b82f6ee)", "rgba(33ccffee)" }, angle = 45 },
|
||||
inactive_border = "rgba(00000000)",
|
||||
},
|
||||
layout = columns_layout,
|
||||
allow_tearing = false,
|
||||
},
|
||||
decoration = {
|
||||
rounding = 5,
|
||||
blur = {
|
||||
enabled = true,
|
||||
size = 7,
|
||||
passes = 3,
|
||||
},
|
||||
active_opacity = 1.0,
|
||||
inactive_opacity = 0.65,
|
||||
},
|
||||
animations = {
|
||||
enabled = true,
|
||||
},
|
||||
binds = {
|
||||
allow_workspace_cycles = true,
|
||||
workspace_back_and_forth = true,
|
||||
},
|
||||
group = {
|
||||
group_on_movetoworkspace = false,
|
||||
col = {
|
||||
border_active = "rgba(edb443ff)",
|
||||
border_inactive = "rgba(091f2eff)",
|
||||
},
|
||||
groupbar = {
|
||||
enabled = true,
|
||||
blur = true,
|
||||
font_size = 13,
|
||||
gradients = true,
|
||||
height = 26,
|
||||
indicator_gap = 0,
|
||||
indicator_height = 1,
|
||||
rounding = 5,
|
||||
gradient_rounding = 5,
|
||||
text_padding = 8,
|
||||
col = {
|
||||
active = "rgba(edb443ff)",
|
||||
inactive = "rgba(101820f2)",
|
||||
},
|
||||
text_color = "rgba(091018ff)",
|
||||
text_color_inactive = "rgba(f2f5f7ff)",
|
||||
},
|
||||
},
|
||||
misc = {
|
||||
force_default_wallpaper = 0,
|
||||
disable_hyprland_logo = true,
|
||||
exit_window_retains_fullscreen = true,
|
||||
},
|
||||
})
|
||||
|
||||
hl.curve("overshoot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
|
||||
hl.curve("smoothOut", { type = "bezier", points = { { 0.36, 1 }, { 0.3, 1 } } })
|
||||
hl.curve("smoothInOut", { type = "bezier", points = { { 0.42, 0 }, { 0.58, 1 } } })
|
||||
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
|
||||
local spring_time_scale = 5
|
||||
local function spring_curve(mass, stiffness, dampening)
|
||||
return {
|
||||
type = "spring",
|
||||
mass = mass,
|
||||
stiffness = stiffness * spring_time_scale * spring_time_scale,
|
||||
dampening = dampening * spring_time_scale,
|
||||
}
|
||||
end
|
||||
|
||||
hl.curve("workspaceSpring", spring_curve(2.4, 38, 8))
|
||||
hl.curve("windowSpring", spring_curve(2.5, 40, 10))
|
||||
|
||||
local animations = {
|
||||
{ leaf = "global", enabled = true, speed = 8, bezier = "default" },
|
||||
|
||||
{ leaf = "windows", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
|
||||
{ leaf = "windowsIn", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
|
||||
{ leaf = "windowsOut", enabled = true, speed = 8, spring = "windowSpring", style = "slide bottom" },
|
||||
{ leaf = "windowsMove", enabled = true, speed = 8, spring = "windowSpring" },
|
||||
|
||||
{ leaf = "border", enabled = false },
|
||||
{ leaf = "borderangle", enabled = false },
|
||||
|
||||
{ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeIn", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeOut", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeSwitch", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeShadow", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeGlow", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeLayers", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeLayersIn", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeLayersOut", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadePopups", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadePopupsIn", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadePopupsOut", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "fadeDpms", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
|
||||
{ leaf = "layers", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
|
||||
{ leaf = "layersIn", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
|
||||
{ leaf = "layersOut", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
|
||||
|
||||
{ leaf = "workspaces", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
|
||||
{ leaf = "workspacesIn", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
|
||||
{ leaf = "workspacesOut", enabled = true, speed = 10, spring = "workspaceSpring", style = "slide" },
|
||||
{ leaf = "specialWorkspace", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
|
||||
{ leaf = "specialWorkspaceIn", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
|
||||
{ leaf = "specialWorkspaceOut", enabled = true, speed = 8, spring = "workspaceSpring", style = "slidevert" },
|
||||
|
||||
{ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "smoothOut" },
|
||||
-- Disabled for now: Hyprland 0.54.0 can crash while damaging a monitor
|
||||
-- from this startup animation's update callback during output discovery.
|
||||
-- { leaf = "monitorAdded", enabled = true, speed = 5, bezier = "smoothOut" },
|
||||
{ leaf = "monitorAdded", enabled = false, speed = 5, bezier = "smoothOut" },
|
||||
}
|
||||
|
||||
for _, animation in ipairs(animations) do
|
||||
hl.animation(animation)
|
||||
end
|
||||
|
||||
local function apply_hyprglass_config()
|
||||
if verify_config or not enable_hyprglass then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
hyprglass = {
|
||||
enabled = 0,
|
||||
default_theme = "dark",
|
||||
default_preset = "default",
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_hyprwobbly_config()
|
||||
if verify_config or not enable_hyprwobbly then
|
||||
return
|
||||
end
|
||||
|
||||
hl.config({
|
||||
plugin = {
|
||||
hyprwobbly = {
|
||||
enabled = hypr_visual_performance_mode and 0 or 1,
|
||||
mode = "always",
|
||||
grid_width = 4,
|
||||
grid_height = 4,
|
||||
tiles_x = 12,
|
||||
tiles_y = 12,
|
||||
spring_k = 18.0,
|
||||
friction = 8.0,
|
||||
mass = 12.0,
|
||||
move_factor = 0.65,
|
||||
resize_factor = 0.45,
|
||||
max_warp = 140.0,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_visual_performance_mode()
|
||||
if verify_config then
|
||||
return
|
||||
end
|
||||
|
||||
local visual_effects_enabled = not hypr_visual_performance_mode
|
||||
hl.config({
|
||||
decoration = {
|
||||
blur = {
|
||||
enabled = visual_effects_enabled,
|
||||
},
|
||||
},
|
||||
animations = {
|
||||
enabled = visual_effects_enabled,
|
||||
},
|
||||
})
|
||||
|
||||
if enable_hyprwobbly then
|
||||
hl.config({
|
||||
plugin = {
|
||||
hyprwobbly = {
|
||||
enabled = visual_effects_enabled and 1 or 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle_visual_performance_mode()
|
||||
hypr_visual_performance_mode = not hypr_visual_performance_mode
|
||||
apply_visual_performance_mode()
|
||||
hl.notification.create({
|
||||
text = "Hyprland performance mode: " .. (hypr_visual_performance_mode and "on" or "off"),
|
||||
duration = 1800,
|
||||
icon = hypr_visual_performance_mode and notification_icons.warning or notification_icons.ok,
|
||||
color = hypr_visual_performance_mode and "rgba(edb443ff)" or "rgba(33ccffee)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
|
||||
local function apply_rules()
|
||||
if verify_config then
|
||||
return
|
||||
end
|
||||
|
||||
hl.workspace_rule({ workspace = "w[tv1]s[false]", gaps_out = 0, gaps_in = 0 })
|
||||
hl.workspace_rule({ workspace = "f[1]s[false]", gaps_out = 0, gaps_in = 0 })
|
||||
|
||||
hl.window_rule({ match = { class = "^()$", title = "^()$" }, float = true })
|
||||
hl.window_rule({ match = { title = "^(Picture-in-Picture)$" }, float = true })
|
||||
hl.window_rule({
|
||||
name = "rofi-glass-window",
|
||||
match = { class = "^(rofi)$" },
|
||||
float = true,
|
||||
center = true,
|
||||
decorate = false,
|
||||
no_shadow = true,
|
||||
xray = false,
|
||||
})
|
||||
hl.layer_rule({
|
||||
name = "rofi-glass-layer",
|
||||
match = { namespace = "^(rofi)$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.05,
|
||||
xray = false,
|
||||
})
|
||||
hl.window_rule({
|
||||
name = "file-chooser-dialogs",
|
||||
match = { title = file_chooser_title_rule },
|
||||
float = true,
|
||||
center = true,
|
||||
focus_on_activate = true,
|
||||
stay_focused = true,
|
||||
})
|
||||
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
|
||||
|
||||
for index, match in ipairs({
|
||||
{ class = "^(flameshot)$" },
|
||||
{ title = "^(flameshot)$" },
|
||||
}) do
|
||||
hl.window_rule({
|
||||
name = "flameshot-overlay-" .. tostring(index),
|
||||
match = match,
|
||||
float = true,
|
||||
no_anim = true,
|
||||
suppress_event = "fullscreen",
|
||||
})
|
||||
end
|
||||
hl.layer_rule({
|
||||
name = "flameshot-layer-overlay",
|
||||
match = { namespace = "^(flameshot)$" },
|
||||
no_anim = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
|
||||
no_anim = true,
|
||||
})
|
||||
hl.window_rule({
|
||||
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
|
||||
tag = "+hyprglass_enabled",
|
||||
})
|
||||
hl.window_rule({
|
||||
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
|
||||
tag = "+hyprglass_theme_light",
|
||||
})
|
||||
hl.window_rule({
|
||||
match = { class = "^(.*[Rr]umno.*)$" },
|
||||
float = true,
|
||||
pin = true,
|
||||
center = true,
|
||||
decorate = false,
|
||||
no_shadow = true,
|
||||
})
|
||||
hl.window_rule({
|
||||
match = { title = "^(.*[Rr]umno.*)$" },
|
||||
float = true,
|
||||
pin = true,
|
||||
center = true,
|
||||
decorate = false,
|
||||
no_shadow = true,
|
||||
})
|
||||
hl.window_rule({
|
||||
name = "subtle-pinned-window-border",
|
||||
match = { pin = true },
|
||||
border_size = 2,
|
||||
border_color = "rgba(edb443ff) rgba(ff4d5dcc)",
|
||||
})
|
||||
hl.window_rule({
|
||||
match = { tag = inactive_opacity_override_tag },
|
||||
opacity = "1.0 override 1.0 override 1.0 override",
|
||||
})
|
||||
end
|
||||
|
||||
ctx.apply_rules = apply_rules
|
||||
ctx.apply_hyprglass_config = apply_hyprglass_config
|
||||
ctx.apply_hyprwobbly_config = apply_hyprwobbly_config
|
||||
ctx.apply_visual_performance_mode = apply_visual_performance_mode
|
||||
ctx.is_file_chooser_window = is_file_chooser_window
|
||||
ctx.raise_file_chooser_window = raise_file_chooser_window
|
||||
ctx.raise_file_chooser_window_later = raise_file_chooser_window_later
|
||||
ctx.toggle_visual_performance_mode = toggle_visual_performance_mode
|
||||
end
|
||||
|
||||
return M
|
||||
63
dotfiles/config/hypr/hyprland/state.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
local shell_ui_command = "hypr_shell_ui"
|
||||
local columns_layout = "nStack"
|
||||
local large_main_layout = "master"
|
||||
local grid_layout = "grid"
|
||||
local monocle_layout = "monocle"
|
||||
|
||||
return {
|
||||
main_mod = "SUPER",
|
||||
mod_alt = "SUPER + ALT",
|
||||
hyper = "SUPER + CTRL + ALT",
|
||||
|
||||
terminal = "ghostty --gtk-single-instance=false",
|
||||
shell_ui_command = shell_ui_command,
|
||||
launcher_command = shell_ui_command .. " launcher",
|
||||
run_menu = shell_ui_command .. " run",
|
||||
|
||||
-- Hyprland shadows ordinary keybinds after one fires; without transparent,
|
||||
-- the first overview chord after a focus-moving bind can be skipped.
|
||||
overview_bind_opts = { dont_inhibit = true, transparent = true },
|
||||
overview_trace_enabled_path = "/tmp/hypr-overview-bind.enable",
|
||||
overview_trace_path = "/tmp/hypr-overview-bind.log",
|
||||
notification_icons = {
|
||||
warning = 0,
|
||||
info = 1,
|
||||
hint = 2,
|
||||
error = 3,
|
||||
confused = 4,
|
||||
ok = 5,
|
||||
none = 6,
|
||||
},
|
||||
|
||||
max_workspace = 9,
|
||||
columns_layout = columns_layout,
|
||||
large_main_layout = large_main_layout,
|
||||
grid_layout = grid_layout,
|
||||
monocle_layout = monocle_layout,
|
||||
layout_cycle = { columns_layout, large_main_layout, grid_layout },
|
||||
layout_names = {
|
||||
[columns_layout] = "Columns",
|
||||
[large_main_layout] = "Large main",
|
||||
[grid_layout] = "Grid",
|
||||
[monocle_layout] = "Monocle",
|
||||
},
|
||||
minimized_workspace = "special:minimized",
|
||||
inactive_opacity_override_tag = "no-inactive-opacity",
|
||||
tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-",
|
||||
current_layout = columns_layout,
|
||||
enable_nstack = true,
|
||||
enable_hyprexpo = true,
|
||||
enable_hyprwinview = true,
|
||||
enable_workspace_history = true,
|
||||
enable_hyprwobbly = true,
|
||||
enable_hyprglass = false,
|
||||
hypr_visual_performance_mode = false,
|
||||
configure_nstack_plugin_from_lua = false,
|
||||
workspace_layouts = {},
|
||||
minimized_windows = {},
|
||||
tabbed_workspace_groups = {},
|
||||
window_picker_mode = nil,
|
||||
window_picker_candidates = {},
|
||||
stack_update_timer = nil,
|
||||
monocle_notice = nil,
|
||||
}
|
||||
502
dotfiles/config/hypr/hyprland/windows.lua
Normal file
@@ -0,0 +1,502 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup(ctx)
|
||||
local _ENV = ctx
|
||||
local function same_class_windows(class_name)
|
||||
local windows = {}
|
||||
if not class_name or class_name == "" then
|
||||
return windows
|
||||
end
|
||||
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if is_normal_window(window) and window.class == class_name then
|
||||
windows[#windows + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
return windows
|
||||
end
|
||||
|
||||
local function short_text(value, limit)
|
||||
value = tostring(value or "")
|
||||
value = value:gsub("[%c\t\r\n]", " ")
|
||||
if #value <= limit then
|
||||
return value
|
||||
end
|
||||
return value:sub(1, limit - 3) .. "..."
|
||||
end
|
||||
|
||||
local function normal_windows()
|
||||
local windows = {}
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if is_normal_window(window) then
|
||||
windows[#windows + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(windows, function(left, right)
|
||||
local left_workspace = left.workspace and left.workspace.id or max_workspace + 1
|
||||
local right_workspace = right.workspace and right.workspace.id or max_workspace + 1
|
||||
if left_workspace ~= right_workspace then
|
||||
return left_workspace < right_workspace
|
||||
end
|
||||
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
|
||||
end)
|
||||
|
||||
return windows
|
||||
end
|
||||
|
||||
local function window_picker_entry(index, window)
|
||||
local workspace = window.workspace and window.workspace.id or "?"
|
||||
local class = short_text(window.class, 18)
|
||||
local title = short_text(window.title, 48)
|
||||
return tostring(index) .. " [" .. tostring(workspace) .. "] " .. class .. " " .. title
|
||||
end
|
||||
|
||||
local function remove_minimized_window(target)
|
||||
local remaining = {}
|
||||
local target_address = target and target.address
|
||||
for _, window in ipairs(minimized_windows) do
|
||||
if window and window.address ~= target_address then
|
||||
remaining[#remaining + 1] = window
|
||||
end
|
||||
end
|
||||
minimized_windows = remaining
|
||||
end
|
||||
|
||||
local function add_minimized_window(window)
|
||||
if not window or not window.address then
|
||||
return
|
||||
end
|
||||
|
||||
remove_minimized_window(window)
|
||||
minimized_windows[#minimized_windows + 1] = window
|
||||
end
|
||||
|
||||
local function hydrate_minimized_windows()
|
||||
local by_address = {}
|
||||
local current_by_address = {}
|
||||
local hydrated = {}
|
||||
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if window and window.address then
|
||||
current_by_address[window.address] = window
|
||||
end
|
||||
end
|
||||
|
||||
for _, window in ipairs(minimized_windows) do
|
||||
local current = window and window.address and current_by_address[window.address]
|
||||
if current and is_minimized_window(current) and not by_address[current.address] then
|
||||
by_address[current.address] = true
|
||||
hydrated[#hydrated + 1] = current
|
||||
end
|
||||
end
|
||||
|
||||
for _, window in pairs(current_by_address) do
|
||||
if window and window.address and is_minimized_window(window) and not by_address[window.address] then
|
||||
by_address[window.address] = true
|
||||
hydrated[#hydrated + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
minimized_windows = hydrated
|
||||
end
|
||||
|
||||
local function float_active_window_preserving_tiled_geometry()
|
||||
local geometry = tiled_window_geometry(hl.get_active_window())
|
||||
dispatch(hl.dsp.window.float({ action = "enable", window = geometry and geometry.selector or nil }))
|
||||
if geometry then
|
||||
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = geometry.selector }))
|
||||
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = geometry.selector }))
|
||||
end
|
||||
return geometry
|
||||
end
|
||||
|
||||
local function float_and_drag_active_window()
|
||||
float_active_window_preserving_tiled_geometry()
|
||||
dispatch(hl.dsp.window.drag())
|
||||
end
|
||||
|
||||
local function float_and_resize_active_window()
|
||||
float_active_window_preserving_tiled_geometry()
|
||||
dispatch(hl.dsp.window.resize())
|
||||
end
|
||||
|
||||
local function toggle_pinned_active_window()
|
||||
local window = hl.get_active_window()
|
||||
local selector = window_selector(window)
|
||||
if not window or not selector then
|
||||
return
|
||||
end
|
||||
|
||||
if window.pinned then
|
||||
dispatch(hl.dsp.window.pin({ action = "disable", window = selector }))
|
||||
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
|
||||
return
|
||||
end
|
||||
|
||||
if not window.floating then
|
||||
float_active_window_preserving_tiled_geometry()
|
||||
end
|
||||
dispatch(hl.dsp.window.pin({ action = "enable", window = selector }))
|
||||
end
|
||||
|
||||
local function current_minimized_windows()
|
||||
hydrate_minimized_windows()
|
||||
|
||||
local windows = {}
|
||||
for _, window in ipairs(minimized_windows) do
|
||||
if window and window.address and is_minimized_window(window) then
|
||||
windows[#windows + 1] = window
|
||||
end
|
||||
end
|
||||
minimized_windows = windows
|
||||
return windows
|
||||
end
|
||||
|
||||
local function restore_minimized_window(window, workspace)
|
||||
if not window or not workspace then
|
||||
return false
|
||||
end
|
||||
|
||||
move_window_to_workspace(workspace.id, false, window)
|
||||
return true
|
||||
end
|
||||
|
||||
local function window_picker_candidates_for(mode)
|
||||
if mode == "minimized" then
|
||||
return current_minimized_windows()
|
||||
end
|
||||
|
||||
local focused = hl.get_active_window()
|
||||
local workspace = active_workspace()
|
||||
local candidates = {}
|
||||
|
||||
for _, window in ipairs(normal_windows()) do
|
||||
local include = true
|
||||
if mode == "bring" and workspace and window.workspace == workspace then
|
||||
include = false
|
||||
elseif mode == "replace" and focused and window == focused then
|
||||
include = false
|
||||
end
|
||||
|
||||
if include then
|
||||
candidates[#candidates + 1] = window
|
||||
end
|
||||
end
|
||||
|
||||
return candidates
|
||||
end
|
||||
|
||||
local function activate_window_picker_candidate(index)
|
||||
local window = window_picker_candidates[index]
|
||||
local mode = window_picker_mode
|
||||
window_picker_mode = nil
|
||||
window_picker_candidates = {}
|
||||
dispatch(hl.dsp.submap("reset"))
|
||||
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
if mode == "go" then
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
return
|
||||
end
|
||||
|
||||
local workspace = active_workspace()
|
||||
if mode == "bring" and workspace then
|
||||
move_window_to_workspace(workspace.id, false, window)
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
return
|
||||
end
|
||||
|
||||
if mode == "minimized" and workspace then
|
||||
remove_minimized_window(window)
|
||||
restore_minimized_window(window, workspace)
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
return
|
||||
end
|
||||
|
||||
if mode == "replace" then
|
||||
local focused = hl.get_active_window()
|
||||
if focused and focused ~= window then
|
||||
dispatch(hl.dsp.window.swap({ target = window_selector(window), window = window_selector(focused) }))
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function enter_window_picker(mode)
|
||||
window_picker_mode = mode
|
||||
window_picker_candidates = window_picker_candidates_for(mode)
|
||||
|
||||
if #window_picker_candidates == 0 then
|
||||
local empty_text = "No windows available"
|
||||
if mode == "minimized" then
|
||||
empty_text = "No minimized windows"
|
||||
end
|
||||
|
||||
hl.notification.create({
|
||||
text = empty_text,
|
||||
duration = 1800,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
local count = math.min(#window_picker_candidates, 9)
|
||||
for i = 1, count do
|
||||
lines[#lines + 1] = window_picker_entry(i, window_picker_candidates[i])
|
||||
end
|
||||
|
||||
hl.notification.create({
|
||||
text = table.concat(lines, "\n"),
|
||||
duration = 5000,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 11,
|
||||
})
|
||||
dispatch(hl.dsp.submap("window-picker"))
|
||||
end
|
||||
|
||||
local function gather_focused_class()
|
||||
local focused = hl.get_active_window()
|
||||
local workspace = active_workspace()
|
||||
if not focused or not workspace or not focused.class or focused.class == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local count = 0
|
||||
for _, window in ipairs(same_class_windows(focused.class)) do
|
||||
if window ~= focused and window.workspace ~= workspace then
|
||||
move_window_to_workspace(workspace.id, false, window)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
hl.notification.create({
|
||||
text = "Gathered " .. tostring(count) .. " " .. focused.class .. " windows",
|
||||
duration = 1600,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
|
||||
local function focus_next_class()
|
||||
local focused = hl.get_active_window()
|
||||
if not focused or not focused.class or focused.class == "" then
|
||||
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
|
||||
return
|
||||
end
|
||||
|
||||
local classes = {}
|
||||
local first_by_class = {}
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if is_normal_window(window) and window.class and window.class ~= "" and not first_by_class[window.class] then
|
||||
first_by_class[window.class] = window
|
||||
classes[#classes + 1] = window.class
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(classes)
|
||||
if #classes <= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local current_index = 1
|
||||
for index, class_name in ipairs(classes) do
|
||||
if class_name == focused.class then
|
||||
current_index = index
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local next_class = classes[(current_index % #classes) + 1]
|
||||
local target = first_by_class[next_class]
|
||||
if target then
|
||||
dispatch(hl.dsp.focus({ window = window_selector(target) }))
|
||||
end
|
||||
end
|
||||
|
||||
local function show_active_window_info()
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
hl.notification.create({
|
||||
text = "No active window",
|
||||
duration = 1800,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local workspace = window.workspace and (window.workspace.name or window.workspace.id) or "?"
|
||||
local lines = {
|
||||
"Class: " .. tostring(window.class or ""),
|
||||
"Title: " .. tostring(window.title or ""),
|
||||
"Workspace: " .. tostring(workspace),
|
||||
"Pinned: " .. tostring(window.pinned or false),
|
||||
"Address: " .. tostring(window.address or ""),
|
||||
"PID: " .. tostring(window.pid or ""),
|
||||
}
|
||||
|
||||
hl.notification.create({
|
||||
text = table.concat(lines, "\n"),
|
||||
duration = 5000,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 11,
|
||||
})
|
||||
end
|
||||
|
||||
local function window_has_tag(window, tag)
|
||||
for _, value in ipairs((window and window.tags) or {}) do
|
||||
if tostring(value):gsub("%*$", "") == tag then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function toggle_inactive_opacity_for_active_window()
|
||||
local window = hl.get_active_window()
|
||||
local selector = window_selector(window)
|
||||
if not selector then
|
||||
return
|
||||
end
|
||||
|
||||
local disabling_reduction = not window_has_tag(window, inactive_opacity_override_tag)
|
||||
dispatch(hl.dsp.window.tag({ tag = inactive_opacity_override_tag, window = selector }))
|
||||
hl.notification.create({
|
||||
text = "Inactive opacity reduction: " .. (disabling_reduction and "off for window" or "on for window"),
|
||||
duration = 1600,
|
||||
icon = notification_icons.info,
|
||||
color = "rgba(edb443ff)",
|
||||
font_size = 13,
|
||||
})
|
||||
end
|
||||
|
||||
local function raise_or_spawn(class_fragment, command)
|
||||
local fragment = string.lower(class_fragment)
|
||||
for _, window in ipairs(hl.get_windows()) do
|
||||
if is_normal_window(window) and window.class and string.find(string.lower(window.class), fragment, 1, true) then
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
hl.exec_cmd(command)
|
||||
end
|
||||
|
||||
local function minimize_active_window()
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
add_minimized_window(window)
|
||||
move_window_to_workspace(minimized_workspace, false, window)
|
||||
end
|
||||
|
||||
local function restore_last_minimized()
|
||||
local workspace = active_workspace()
|
||||
if not workspace then
|
||||
return
|
||||
end
|
||||
|
||||
hydrate_minimized_windows()
|
||||
|
||||
while #minimized_windows > 0 do
|
||||
local window = table.remove(minimized_windows)
|
||||
if window and window.address and is_minimized_window(window) then
|
||||
restore_minimized_window(window, workspace)
|
||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function restore_all_minimized()
|
||||
local workspace = active_workspace()
|
||||
if not workspace then
|
||||
return
|
||||
end
|
||||
|
||||
hydrate_minimized_windows()
|
||||
|
||||
while #minimized_windows > 0 do
|
||||
restore_minimized_window(table.remove(minimized_windows), workspace)
|
||||
end
|
||||
end
|
||||
|
||||
local function minimize_other_classes()
|
||||
local focused = hl.get_active_window()
|
||||
local workspace = active_workspace()
|
||||
if not focused or not workspace then
|
||||
return
|
||||
end
|
||||
|
||||
for _, window in ipairs(tiled_windows(workspace)) do
|
||||
if window ~= focused and window.class ~= focused.class then
|
||||
add_minimized_window(window)
|
||||
move_window_to_workspace(minimized_workspace, false, window)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function restore_focused_class()
|
||||
local focused = hl.get_active_window()
|
||||
local workspace = active_workspace()
|
||||
if not focused or not workspace or not focused.class then
|
||||
return
|
||||
end
|
||||
|
||||
hydrate_minimized_windows()
|
||||
|
||||
local remaining = {}
|
||||
for _, window in ipairs(minimized_windows) do
|
||||
if window and window.class == focused.class and is_minimized_window(window) then
|
||||
restore_minimized_window(window, workspace)
|
||||
else
|
||||
remaining[#remaining + 1] = window
|
||||
end
|
||||
end
|
||||
minimized_windows = remaining
|
||||
end
|
||||
|
||||
ctx.same_class_windows = same_class_windows
|
||||
ctx.short_text = short_text
|
||||
ctx.normal_windows = normal_windows
|
||||
ctx.window_picker_entry = window_picker_entry
|
||||
ctx.remove_minimized_window = remove_minimized_window
|
||||
ctx.add_minimized_window = add_minimized_window
|
||||
ctx.hydrate_minimized_windows = hydrate_minimized_windows
|
||||
ctx.float_active_window_preserving_tiled_geometry = float_active_window_preserving_tiled_geometry
|
||||
ctx.float_and_drag_active_window = float_and_drag_active_window
|
||||
ctx.float_and_resize_active_window = float_and_resize_active_window
|
||||
ctx.toggle_pinned_active_window = toggle_pinned_active_window
|
||||
ctx.current_minimized_windows = current_minimized_windows
|
||||
ctx.restore_minimized_window = restore_minimized_window
|
||||
ctx.window_picker_candidates_for = window_picker_candidates_for
|
||||
ctx.activate_window_picker_candidate = activate_window_picker_candidate
|
||||
ctx.enter_window_picker = enter_window_picker
|
||||
ctx.gather_focused_class = gather_focused_class
|
||||
ctx.focus_next_class = focus_next_class
|
||||
ctx.show_active_window_info = show_active_window_info
|
||||
ctx.toggle_inactive_opacity_for_active_window = toggle_inactive_opacity_for_active_window
|
||||
ctx.raise_or_spawn = raise_or_spawn
|
||||
ctx.minimize_active_window = minimize_active_window
|
||||
ctx.restore_last_minimized = restore_last_minimized
|
||||
ctx.restore_all_minimized = restore_all_minimized
|
||||
ctx.minimize_other_classes = minimize_other_classes
|
||||
ctx.restore_focused_class = restore_focused_class
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -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
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"global": {
|
||||
"check_for_updates_on_startup": true,
|
||||
"show_in_menu_bar": true,
|
||||
"show_profile_name_in_menu_bar": false
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"complex_modifications": {
|
||||
"parameters": {
|
||||
"basic.to_if_alone_timeout_milliseconds": 1000
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"manipulators": [
|
||||
{
|
||||
"description": "Change right command to command+control+option+shift.",
|
||||
"from": {
|
||||
"key_code": "right_command",
|
||||
"modifiers": {
|
||||
"optional": [
|
||||
"any"
|
||||
]
|
||||
}
|
||||
},
|
||||
"to": [
|
||||
{
|
||||
"key_code": "left_shift",
|
||||
"modifiers": [
|
||||
"left_command",
|
||||
"left_control",
|
||||
"left_option"
|
||||
]
|
||||
}
|
||||
],
|
||||
"to_if_alone": [
|
||||
{
|
||||
"key_code": "escape",
|
||||
"modifiers": {
|
||||
"optional": [
|
||||
"any"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "basic"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"devices": [
|
||||
{
|
||||
"disable_built_in_keyboard_if_exists": false,
|
||||
"fn_function_keys": {},
|
||||
"identifiers": {
|
||||
"is_keyboard": true,
|
||||
"is_pointing_device": false,
|
||||
"product_id": 610,
|
||||
"vendor_id": 1452
|
||||
},
|
||||
"ignore": false,
|
||||
"simple_modifications": {}
|
||||
},
|
||||
{
|
||||
"disable_built_in_keyboard_if_exists": false,
|
||||
"fn_function_keys": {},
|
||||
"identifiers": {
|
||||
"is_keyboard": true,
|
||||
"is_pointing_device": false,
|
||||
"product_id": 597,
|
||||
"vendor_id": 1452
|
||||
},
|
||||
"ignore": false,
|
||||
"simple_modifications": {}
|
||||
}
|
||||
],
|
||||
"fn_function_keys": {
|
||||
"f1": "vk_consumer_brightness_down",
|
||||
"f10": "mute",
|
||||
"f11": "volume_down",
|
||||
"f12": "volume_up",
|
||||
"f2": "vk_consumer_brightness_up",
|
||||
"f3": "vk_mission_control",
|
||||
"f4": "vk_launchpad",
|
||||
"f5": "vk_consumer_illumination_down",
|
||||
"f6": "vk_consumer_illumination_up",
|
||||
"f7": "vk_consumer_previous",
|
||||
"f8": "vk_consumer_play",
|
||||
"f9": "vk_consumer_next"
|
||||
},
|
||||
"name": "Default profile",
|
||||
"one_to_many_mappings": {},
|
||||
"selected": true,
|
||||
"simple_modifications": {
|
||||
"caps_lock": "left_control"
|
||||
},
|
||||
"standalone_keys": {},
|
||||
"virtual_hid_keyboard": {
|
||||
"caps_lock_delay_milliseconds": 0,
|
||||
"keyboard_type": "ansi",
|
||||
"standalone_keys_delay_milliseconds": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
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
@@ -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
|
||||
}
|
||||
780
dotfiles/config/river-xmonad/Main.hs
Normal file
@@ -0,0 +1,780 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Concurrent (forkIO)
|
||||
import Data.Bits ((.&.), complement)
|
||||
import Data.Char (toLower)
|
||||
import Data.Function (on)
|
||||
import Data.List (find, foldl', isInfixOf, isPrefixOf, minimumBy)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Word (Word32)
|
||||
import Graphics.X11.ExtraTypes.XF86
|
||||
import System.Exit (ExitCode(..))
|
||||
import System.IO (hFlush, stdout)
|
||||
import System.Process (readCreateProcessWithExitCode, shell, spawnCommand, waitForProcess)
|
||||
import XMonad
|
||||
import qualified XMonad.Layout.Renamed as RN
|
||||
import XMonad.River.WindowManager
|
||||
import XMonad.River.WindowManager.Wayland
|
||||
import qualified XMonad.StackSet as W
|
||||
|
||||
data Direction = DirectionUp | DirectionDown | DirectionLeft | DirectionRight
|
||||
deriving (Eq, Show)
|
||||
|
||||
data EqualColumns a = EqualColumns
|
||||
deriving (Read, Show, Typeable)
|
||||
|
||||
instance LayoutClass EqualColumns a where
|
||||
description _ = "Columns"
|
||||
pureLayout _ rect stack =
|
||||
zip windows (equalColumnRects rect (length windows))
|
||||
where
|
||||
windows = W.integrate stack
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
let bindings = keyBindings
|
||||
configLog $ "starting imalison-river-xmonad with keybindings=" ++ show (length bindings)
|
||||
initialState <- initialRiverWMState riverConfig
|
||||
runRiverWMWaylandConfig
|
||||
RiverWMWaylandConfig
|
||||
{ riverWMWaylandInitialState = initialState
|
||||
, riverWMWaylandKeyBindings = bindings
|
||||
}
|
||||
|
||||
riverLayouts =
|
||||
renamed "Columns" EqualColumns
|
||||
||| Full
|
||||
where
|
||||
renamed name = RN.renamed [RN.Replace name]
|
||||
|
||||
riverConfig =
|
||||
(defaultRiverWMConfig riverLayouts)
|
||||
{ riverWMWorkspaces = ordinaryWorkspaces ++ specialWorkspaces
|
||||
, riverWMMouseFollowsFocus = True
|
||||
, riverWMBorderWidth = 2
|
||||
, riverWMFocusedBorderColor = rgba8 0xed 0xb4 0x43 0xee
|
||||
, riverWMUnfocusedBorderColor = rgba8 0x59 0x59 0x59 0xaa
|
||||
}
|
||||
|
||||
rgba8 :: Word32 -> Word32 -> Word32 -> Word32 -> RiverWMColor
|
||||
rgba8 red green blue alpha =
|
||||
RiverWMColor (wide red) (wide green) (wide blue) (wide alpha)
|
||||
where
|
||||
wide component = component * 0x01010101
|
||||
|
||||
keyBindings
|
||||
:: (LayoutClass l Window, Read (l Window))
|
||||
=> [RiverWMWaylandKeyBinding l]
|
||||
keyBindings =
|
||||
addHyperChordBindings hyper hyperChord $
|
||||
concat
|
||||
[ directionalBindings super directionalFocus
|
||||
, directionalBindings (super .|. shift) directionalSwap
|
||||
, directionalBindings (super .|. ctrl) (shiftFocusedToDirectionalScreen False)
|
||||
, directionalBindings (super .|. ctrl .|. shift) shiftFocusedToEmptyWorkspaceOnDirectionalScreen
|
||||
, directionalBindings hyper focusDirectionalScreen
|
||||
, directionalBindings (hyper .|. shift) (shiftFocusedToDirectionalScreen True)
|
||||
, workspaceBindings
|
||||
, layoutBindings
|
||||
, spawnBindings
|
||||
, mediaBindings
|
||||
]
|
||||
|
||||
directionalBindings
|
||||
:: RiverWMWaylandModifiers
|
||||
-> (Direction -> RiverWMWaylandAction l)
|
||||
-> [RiverWMWaylandKeyBinding l]
|
||||
directionalBindings mods command =
|
||||
[ key mods xK_w (command DirectionUp)
|
||||
, key mods xK_s (command DirectionDown)
|
||||
, key mods xK_a (command DirectionLeft)
|
||||
, key mods xK_d (command DirectionRight)
|
||||
]
|
||||
|
||||
workspaceBindings
|
||||
:: [RiverWMWaylandKeyBinding l]
|
||||
workspaceBindings =
|
||||
[ key (mods .|. super) keysym (action $ command workspace)
|
||||
| (workspace, keysym) <- zip (map show [(1 :: Int) .. 9]) [xK_1 .. xK_9]
|
||||
, (command, mods, action) <-
|
||||
[ (W.greedyView, noMods, stackAction)
|
||||
, (W.shift, shift, stackAction)
|
||||
, (\workspaceId stackSet -> W.greedyView workspaceId (W.shift workspaceId stackSet), ctrl, stackActionWarpPointer)
|
||||
]
|
||||
]
|
||||
|
||||
layoutBindings
|
||||
:: (LayoutClass l Window, Read (l Window))
|
||||
=> [RiverWMWaylandKeyBinding l]
|
||||
layoutBindings =
|
||||
[ key super xK_space (layoutAction NextLayout)
|
||||
, key (super .|. shift) xK_space (layoutAction (JumpToLayout "Columns"))
|
||||
, key (super .|. ctrl) xK_space (layoutAction (JumpToLayout "Full"))
|
||||
, key super xK_bracketleft (layoutAction Shrink)
|
||||
, key super xK_bracketright (layoutAction Expand)
|
||||
, key super xK_comma (layoutAction (IncMasterN 1))
|
||||
, key super xK_period (layoutAction (IncMasterN (-1)))
|
||||
]
|
||||
|
||||
spawnBindings
|
||||
:: [RiverWMWaylandKeyBinding l]
|
||||
spawnBindings =
|
||||
[ key super xK_Return (spawnAction "ghostty --gtk-single-instance=false")
|
||||
, key (super .|. shift) xK_Return (spawnAction "ghostty --gtk-single-instance=false")
|
||||
, key super xK_p (spawnAction "rofi -show drun -show-icons")
|
||||
, key (super .|. shift) xK_p (spawnAction "rofi -show run")
|
||||
, key super xK_Tab (selectWindowAction "windows" focusSelectedWindow)
|
||||
, key super xK_g (selectWindowAction "go to window" focusSelectedWindow)
|
||||
, key super xK_b (selectWindowAction "bring window" bringSelectedWindow)
|
||||
, key (super .|. shift) xK_b (selectWindowAction "replace window" replaceSelectedWindow)
|
||||
, key super xK_m minimizeFocusedWindow
|
||||
, key (super .|. shift) xK_m restoreLastMinimizedWindow
|
||||
, key super xK_q (spawnAction "river-xmonad-restart")
|
||||
, key (super .|. shift) xK_c closeFocusedWindow
|
||||
, key (super .|. shift) xK_q (spawnAction "riverctl exit")
|
||||
, key (super .|. alt) xK_e (toggleScratchpad "element")
|
||||
, key (super .|. alt) xK_h (toggleScratchpad "htop")
|
||||
, key (super .|. alt) xK_k (toggleScratchpad "slack")
|
||||
, key (super .|. alt) xK_s (toggleScratchpad "spotify")
|
||||
, key (super .|. alt) xK_t (toggleScratchpad "transmission")
|
||||
, key (super .|. alt) xK_v (toggleScratchpad "volume")
|
||||
, key (super .|. alt) xK_c (spawnAction "google-chrome-stable")
|
||||
, key super xK_e (spawnAction "emacsclient --eval '(emacs-everywhere)'")
|
||||
, key (super .|. ctrl) xK_e (shiftFocusedToNextEmptyWorkspace False)
|
||||
, key (super .|. shift) xK_e (shiftFocusedToNextEmptyWorkspace True)
|
||||
, key super xK_v (spawnAction "wl-paste | wtype -")
|
||||
, key super xK_x (spawnAction "rofi_command.sh")
|
||||
, key hyper xK_e viewNextEmptyWorkspace
|
||||
, key hyper xK_v (spawnAction "rofi -modi 'clipboard:greenclip print' -show clipboard")
|
||||
, key hyper xK_p (spawnAction "rofi-pass")
|
||||
, key noMods xK_Print (spawnAction "flameshot gui")
|
||||
, key hyper xK_h (spawnAction "flameshot gui")
|
||||
, key hyper xK_c (spawnAction "shell_command.sh")
|
||||
, key hyper xK_g gatherFocusedAppId
|
||||
, key (hyper .|. shift) xK_l (spawnAction "loginctl lock-session")
|
||||
, key hyper xK_k (spawnAction "rofi_kill_process.sh")
|
||||
, key (hyper .|. shift) xK_k (spawnAction "rofi_kill_all.sh")
|
||||
, key hyper xK_r (spawnAction "rofi_systemd_mono")
|
||||
, key hyper xK_9 (spawnAction "start_synergy.sh")
|
||||
, key hyper xK_backslash (spawnAction "$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")
|
||||
, key hyper xK_i (spawnAction "rofi_select_input.hs")
|
||||
, key hyper xK_o (spawnAction "rofi_paswitch")
|
||||
, key hyper xK_comma (spawnAction "rofi_wallpaper.sh")
|
||||
, key hyper xK_slash (spawnAction "toggle_taffybar")
|
||||
, key hyper xK_y (spawnAction "rofi_agentic_skill")
|
||||
]
|
||||
|
||||
mediaBindings
|
||||
:: [RiverWMWaylandKeyBinding l]
|
||||
mediaBindings =
|
||||
[ key super xK_semicolon (spawnAction "playerctl play-pause")
|
||||
, key noMods xF86XK_AudioPause (spawnAction "playerctl play-pause")
|
||||
, key noMods xF86XK_AudioPlay (spawnAction "playerctl play-pause")
|
||||
, key super xK_l (spawnAction "playerctl next")
|
||||
, key noMods xF86XK_AudioNext (spawnAction "playerctl next")
|
||||
, key super xK_j (spawnAction "playerctl previous")
|
||||
, key noMods xF86XK_AudioPrev (spawnAction "playerctl previous")
|
||||
, key noMods xF86XK_AudioRaiseVolume (spawnAction "set_volume --unmute --change-volume +5")
|
||||
, key noMods xF86XK_AudioLowerVolume (spawnAction "set_volume --unmute --change-volume -5")
|
||||
, key noMods xF86XK_AudioMute (spawnAction "set_volume --toggle-mute")
|
||||
, key super xK_i (spawnAction "set_volume --unmute --change-volume +5")
|
||||
, key super xK_k (spawnAction "set_volume --unmute --change-volume -5")
|
||||
, key super xK_u (spawnAction "set_volume --toggle-mute")
|
||||
, key (hyper .|. shift) xK_q (spawnAction "toggle_mute_current_window.sh")
|
||||
, key (hyper .|. ctrl) xK_q (spawnAction "toggle_mute_current_window.sh only")
|
||||
, key noMods xF86XK_MonBrightnessUp (spawnAction "brightness.sh up")
|
||||
, key noMods xF86XK_MonBrightnessDown (spawnAction "brightness.sh down")
|
||||
]
|
||||
|
||||
key
|
||||
:: RiverWMWaylandModifiers
|
||||
-> KeySym
|
||||
-> RiverWMWaylandAction l
|
||||
-> RiverWMWaylandKeyBinding l
|
||||
key modifiers keysym action =
|
||||
RiverWMWaylandKeyBinding
|
||||
{ riverWMWaylandKeyModifiers = modifiers
|
||||
, riverWMWaylandKeyKeysym = fromIntegral keysym
|
||||
, riverWMWaylandKeyAction = action
|
||||
}
|
||||
|
||||
spawnAction :: String -> RiverWMWaylandAction l
|
||||
spawnAction command state = do
|
||||
configLog $ "spawn start: " ++ command
|
||||
process <- spawnCommand (riverSpawnPrelude ++ command)
|
||||
_ <- forkIO $ do
|
||||
exitCode <- waitForProcess process
|
||||
configLog $ "spawn exit: " ++ command ++ " -> " ++ show exitCode
|
||||
pure ()
|
||||
pure ([], state)
|
||||
|
||||
riverSpawnPrelude :: String
|
||||
riverSpawnPrelude =
|
||||
"XDG_RUNTIME_DIR=\"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}\"; "
|
||||
++ "export XDG_RUNTIME_DIR; "
|
||||
++ "if [ -z \"${WAYLAND_DISPLAY:-}\" ]; then "
|
||||
++ "for socket in \"$XDG_RUNTIME_DIR\"/wayland-*; do "
|
||||
++ "[ -S \"$socket\" ] || continue; "
|
||||
++ "WAYLAND_DISPLAY=\"$(basename \"$socket\")\"; "
|
||||
++ "break; "
|
||||
++ "done; "
|
||||
++ "fi; "
|
||||
++ "export WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:-wayland-1}\"; "
|
||||
++ "export XDG_CURRENT_DESKTOP=river; "
|
||||
++ "export XDG_SESSION_DESKTOP=river-xmonad; "
|
||||
++ "export XDG_SESSION_TYPE=wayland; "
|
||||
++ "export IMALISON_SESSION_TYPE=wayland; "
|
||||
++ "export IMALISON_WINDOW_MANAGER=river-xmonad; "
|
||||
|
||||
configLog :: String -> IO ()
|
||||
configLog message = do
|
||||
putStrLn $ "imalison-river-xmonad: " ++ message
|
||||
hFlush stdout
|
||||
|
||||
layoutAction
|
||||
:: (LayoutClass l Window, Read (l Window), Message message)
|
||||
=> message
|
||||
-> RiverWMWaylandAction l
|
||||
layoutAction = handleRiverWMLayoutMessage
|
||||
|
||||
stackAction
|
||||
:: (W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail
|
||||
-> W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail)
|
||||
-> RiverWMWaylandAction l
|
||||
stackAction f state =
|
||||
pure $ modifyRiverWMStackSet f state
|
||||
|
||||
stackActionWarpPointer
|
||||
:: (W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail
|
||||
-> W.StackSet WorkspaceId (l Window) Window RiverWMOutputId ScreenDetail)
|
||||
-> RiverWMWaylandAction l
|
||||
stackActionWarpPointer f state =
|
||||
pure $ modifyRiverWMStackSetAndWarpPointer f state
|
||||
|
||||
data ScratchpadDefinition = ScratchpadDefinition
|
||||
{ scratchpadName :: !String
|
||||
, scratchpadCommand :: !String
|
||||
, scratchpadMatches :: !(RiverWMWindowState -> Bool)
|
||||
}
|
||||
|
||||
ordinaryWorkspaces :: [WorkspaceId]
|
||||
ordinaryWorkspaces = map show [(1 :: Int) .. 9]
|
||||
|
||||
minimizedWorkspace :: WorkspaceId
|
||||
minimizedWorkspace = "__minimized"
|
||||
|
||||
specialWorkspaces :: [WorkspaceId]
|
||||
specialWorkspaces =
|
||||
minimizedWorkspace : map (scratchpadWorkspace . scratchpadName) scratchpadDefinitions
|
||||
|
||||
scratchpadWorkspace :: String -> WorkspaceId
|
||||
scratchpadWorkspace name = "__scratchpad:" ++ name
|
||||
|
||||
isSpecialWorkspace :: WorkspaceId -> Bool
|
||||
isSpecialWorkspace workspace =
|
||||
workspace == minimizedWorkspace || "__scratchpad:" `isPrefixOf` workspace
|
||||
|
||||
scratchpadDefinitions :: [ScratchpadDefinition]
|
||||
scratchpadDefinitions =
|
||||
[ ScratchpadDefinition "element" "element-desktop" $
|
||||
anyMatcher [appIdMatches "Element", appIdMatches "element"]
|
||||
, ScratchpadDefinition "htop" "ghostty --title=htop -e htop" $
|
||||
titleContains "htop"
|
||||
, ScratchpadDefinition "slack" "slack" $
|
||||
anyMatcher [appIdMatches "Slack", appIdMatches "slack"]
|
||||
, ScratchpadDefinition "spotify" "spotify" $
|
||||
anyMatcher [appIdMatches "Spotify", appIdMatches "spotify"]
|
||||
, ScratchpadDefinition "transmission" "transmission-gtk" $
|
||||
anyMatcher [titleContains "Transmission", appIdContains "transmission"]
|
||||
, ScratchpadDefinition "volume" "pavucontrol" $
|
||||
anyMatcher [appIdMatches "Pavucontrol", appIdContains "pavucontrol"]
|
||||
]
|
||||
|
||||
anyMatcher :: [RiverWMWindowState -> Bool] -> RiverWMWindowState -> Bool
|
||||
anyMatcher matchers windowState =
|
||||
any ($ windowState) matchers
|
||||
|
||||
appIdMatches :: String -> RiverWMWindowState -> Bool
|
||||
appIdMatches expected windowState =
|
||||
lower expected == maybe "" lower (riverWMWindowAppId windowState)
|
||||
|
||||
appIdContains :: String -> RiverWMWindowState -> Bool
|
||||
appIdContains needle windowState =
|
||||
lower needle `isInfixOf` maybe "" lower (riverWMWindowAppId windowState)
|
||||
|
||||
titleContains :: String -> RiverWMWindowState -> Bool
|
||||
titleContains needle windowState =
|
||||
lower needle `isInfixOf` maybe "" lower (riverWMWindowTitle windowState)
|
||||
|
||||
lower :: String -> String
|
||||
lower = map toLower
|
||||
|
||||
closeFocusedWindow :: RiverWMWaylandAction l
|
||||
closeFocusedWindow state@RiverWMState{riverWMStackSet, riverWMWindowIds} =
|
||||
pure
|
||||
( maybe [] ((: []) . RiverWMCloseWindow) $
|
||||
W.peek riverWMStackSet >>= (`M.lookup` riverWMWindowIds)
|
||||
, state
|
||||
)
|
||||
|
||||
minimizeFocusedWindow :: RiverWMWaylandAction l
|
||||
minimizeFocusedWindow =
|
||||
stackAction $ W.shift minimizedWorkspace
|
||||
|
||||
restoreLastMinimizedWindow :: RiverWMWaylandAction l
|
||||
restoreLastMinimizedWindow =
|
||||
stackActionWarpPointer $ \stackSet ->
|
||||
case workspaceFocusedWindow minimizedWorkspace stackSet of
|
||||
Nothing -> stackSet
|
||||
Just window ->
|
||||
let currentTag = W.currentTag stackSet
|
||||
in W.focusWindow window (W.shiftWin currentTag window stackSet)
|
||||
|
||||
toggleScratchpad :: String -> RiverWMWaylandAction l
|
||||
toggleScratchpad name state@RiverWMState{riverWMStackSet} =
|
||||
case find ((== name) . scratchpadName) scratchpadDefinitions of
|
||||
Nothing ->
|
||||
pure ([], state)
|
||||
Just scratchpad ->
|
||||
case W.peek riverWMStackSet of
|
||||
Just focused | focused `elem` matchingWindows ->
|
||||
pure $ modifyRiverWMStackSet (W.shift $ scratchpadWorkspace name) state
|
||||
_ ->
|
||||
case matchingWindows of
|
||||
window : _ ->
|
||||
pure $ modifyRiverWMStackSetAndWarpPointer (showScratchpadWindow window) state
|
||||
[] ->
|
||||
spawnAction (scratchpadCommand scratchpad) state
|
||||
where
|
||||
matchingWindows = scratchpadWindows scratchpad state
|
||||
showScratchpadWindow window stackSet =
|
||||
let currentTag = W.currentTag stackSet
|
||||
in W.float window nearFullScratchpadRect $
|
||||
W.focusWindow window (W.shiftWin currentTag window stackSet)
|
||||
|
||||
nearFullScratchpadRect :: W.RationalRect
|
||||
nearFullScratchpadRect =
|
||||
W.RationalRect left top width height
|
||||
where
|
||||
width = 0.9
|
||||
height = 0.9
|
||||
left = 0.95 - width
|
||||
top = 0.95 - height
|
||||
|
||||
scratchpadWindows :: ScratchpadDefinition -> RiverWMState l -> [Window]
|
||||
scratchpadWindows ScratchpadDefinition{scratchpadMatches} RiverWMState{riverWMWindows} =
|
||||
[ riverWMWindowXWindow windowState
|
||||
| windowState <- M.elems riverWMWindows
|
||||
, scratchpadMatches windowState
|
||||
]
|
||||
|
||||
selectWindowAction
|
||||
:: String
|
||||
-> (Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l))
|
||||
-> RiverWMWaylandAction l
|
||||
selectWindowAction prompt action state = do
|
||||
selected <- rofiSelectWindow prompt state
|
||||
pure $ maybe ([], state) (`action` state) selected
|
||||
|
||||
focusSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
|
||||
focusSelectedWindow window state =
|
||||
modifyRiverWMStackSetAndWarpPointer (focusWindowEverywhere window) state
|
||||
|
||||
bringSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
|
||||
bringSelectedWindow window state =
|
||||
modifyRiverWMStackSetAndWarpPointer (bringWindowToCurrentWorkspace window) state
|
||||
|
||||
replaceSelectedWindow :: Window -> RiverWMState l -> ([RiverWMRequest], RiverWMState l)
|
||||
replaceSelectedWindow selected state =
|
||||
modifyRiverWMStackSetAndWarpPointer replaceWindow state
|
||||
where
|
||||
replaceWindow stackSet =
|
||||
case (W.peek stackSet, W.findTag selected stackSet) of
|
||||
(Just focused, Just selectedWorkspace)
|
||||
| focused /= selected ->
|
||||
W.focusWindow selected $
|
||||
W.shiftWin selectedWorkspace focused $
|
||||
W.shiftWin (W.currentTag stackSet) selected stackSet
|
||||
_ -> stackSet
|
||||
|
||||
gatherFocusedAppId :: RiverWMWaylandAction l
|
||||
gatherFocusedAppId state@RiverWMState{riverWMStackSet, riverWMWindowIds, riverWMWindows} =
|
||||
pure $ modifyRiverWMStackSet gatherMatching state
|
||||
where
|
||||
focusedAppId = do
|
||||
focused <- W.peek riverWMStackSet
|
||||
windowId <- M.lookup focused riverWMWindowIds
|
||||
riverWMWindowAppId =<< M.lookup windowId riverWMWindows
|
||||
|
||||
matchingWindows =
|
||||
[ riverWMWindowXWindow windowState
|
||||
| windowState <- M.elems riverWMWindows
|
||||
, riverWMWindowAppId windowState == focusedAppId
|
||||
]
|
||||
|
||||
gatherMatching stackSet =
|
||||
case focusedAppId of
|
||||
Nothing -> stackSet
|
||||
Just _ ->
|
||||
foldl' (\acc window -> W.shiftWin (W.currentTag acc) window acc) stackSet matchingWindows
|
||||
|
||||
rofiSelectWindow :: String -> RiverWMState l -> IO (Maybe Window)
|
||||
rofiSelectWindow prompt state =
|
||||
case windowEntries state of
|
||||
[] ->
|
||||
pure Nothing
|
||||
entries -> do
|
||||
(exitCode, selected, _stderr) <-
|
||||
readCreateProcessWithExitCode
|
||||
(shell $ "rofi -dmenu -i -show-icons -p " ++ shellQuote prompt)
|
||||
(concatMap formatWindowEntry entries)
|
||||
pure $ case exitCode of
|
||||
ExitSuccess -> parseSelectedWindow selected
|
||||
_ -> Nothing
|
||||
|
||||
data WindowEntry = WindowEntry
|
||||
{ windowEntryWindow :: !Window
|
||||
, windowEntryWorkspace :: !WorkspaceId
|
||||
, windowEntryAppId :: !String
|
||||
, windowEntryTitle :: !String
|
||||
}
|
||||
|
||||
windowEntries :: RiverWMState l -> [WindowEntry]
|
||||
windowEntries RiverWMState{riverWMStackSet, riverWMWindowIds, riverWMWindows} =
|
||||
[ WindowEntry window (W.tag workspace) appId title
|
||||
| workspace <- W.workspaces riverWMStackSet
|
||||
, not (isSpecialWorkspace $ W.tag workspace)
|
||||
, window <- W.integrate' (W.stack workspace)
|
||||
, let windowId = M.lookup window riverWMWindowIds
|
||||
, Just windowState <- [windowId >>= (`M.lookup` riverWMWindows)]
|
||||
, let appId = fromMaybe "window" (riverWMWindowAppId windowState)
|
||||
title = fromMaybe "" (riverWMWindowTitle windowState)
|
||||
]
|
||||
|
||||
formatWindowEntry :: WindowEntry -> String
|
||||
formatWindowEntry WindowEntry{..} =
|
||||
visibleLabel ++ "\0icon\x1f" ++ iconName ++ "\n"
|
||||
where
|
||||
visibleLabel =
|
||||
show windowEntryWindow
|
||||
++ "\t["
|
||||
++ windowEntryWorkspace
|
||||
++ "] "
|
||||
++ if null windowEntryTitle
|
||||
then windowEntryAppId
|
||||
else windowEntryAppId ++ " - " ++ windowEntryTitle
|
||||
iconName = if null windowEntryAppId then "application-x-executable" else windowEntryAppId
|
||||
|
||||
parseSelectedWindow :: String -> Maybe Window
|
||||
parseSelectedWindow selected =
|
||||
case reads (takeWhile (/= '\t') $ takeWhile (/= '\0') selected) of
|
||||
(window, _) : _ -> Just window
|
||||
[] -> Nothing
|
||||
|
||||
focusWindowEverywhere
|
||||
:: Eq sid
|
||||
=> Window
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
focusWindowEverywhere window stackSet =
|
||||
maybe stackSet (\workspace -> W.focusWindow window (W.greedyView workspace stackSet)) $
|
||||
W.findTag window stackSet
|
||||
|
||||
bringWindowToCurrentWorkspace
|
||||
:: Eq sid
|
||||
=> Window
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
bringWindowToCurrentWorkspace window stackSet =
|
||||
W.focusWindow window (W.shiftWin (W.currentTag stackSet) window stackSet)
|
||||
|
||||
workspaceFocusedWindow :: WorkspaceId -> W.StackSet WorkspaceId l Window sid sd -> Maybe Window
|
||||
workspaceFocusedWindow workspace stackSet =
|
||||
W.focus <$> (W.stack =<< find ((== workspace) . W.tag) (W.workspaces stackSet))
|
||||
|
||||
shellQuote :: String -> String
|
||||
shellQuote value =
|
||||
"'" ++ concatMap quoteChar value ++ "'"
|
||||
where
|
||||
quoteChar '\'' = "'\\''"
|
||||
quoteChar char = [char]
|
||||
|
||||
viewNextEmptyWorkspace :: RiverWMWaylandAction l
|
||||
viewNextEmptyWorkspace =
|
||||
stackAction $ \stackSet ->
|
||||
maybe stackSet (`W.greedyView` stackSet) (nextEmptyWorkspace stackSet)
|
||||
|
||||
shiftFocusedToNextEmptyWorkspace :: Bool -> RiverWMWaylandAction l
|
||||
shiftFocusedToNextEmptyWorkspace follow =
|
||||
(if follow then stackActionWarpPointer else stackAction) $ \stackSet ->
|
||||
maybe stackSet (`shiftFocusedToWorkspace` stackSet) (nextEmptyWorkspace stackSet)
|
||||
where
|
||||
shiftFocusedToWorkspace workspace stackSet =
|
||||
let shifted = W.shift workspace stackSet
|
||||
in if follow then W.greedyView workspace shifted else shifted
|
||||
|
||||
nextEmptyWorkspace
|
||||
:: W.StackSet WorkspaceId l Window sid sd
|
||||
-> Maybe WorkspaceId
|
||||
nextEmptyWorkspace stackSet =
|
||||
find (`workspaceIsEmpty` stackSet) candidates
|
||||
where
|
||||
currentTag = W.currentTag stackSet
|
||||
candidates =
|
||||
case break (== currentTag) ordinaryWorkspaces of
|
||||
(_before, []) -> ordinaryWorkspaces
|
||||
(before, _current : after) -> after ++ before
|
||||
|
||||
workspaceIsEmpty
|
||||
:: WorkspaceId
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
-> Bool
|
||||
workspaceIsEmpty workspace stackSet =
|
||||
maybe False (null . W.integrate' . W.stack) $
|
||||
find ((== workspace) . W.tag) (W.workspaces stackSet)
|
||||
|
||||
directionalSwap :: Direction -> RiverWMWaylandAction l
|
||||
directionalSwap direction state@RiverWMState{riverWMStackSet} =
|
||||
pure $ modifyRiverWMStackSet swapTarget state
|
||||
where
|
||||
target = directionalTargetAmong (W.index riverWMStackSet) direction state
|
||||
swapTarget stackSet =
|
||||
maybe (fallbackDirectionalSwap direction stackSet) (`swapFocusedWithWindow` stackSet) target
|
||||
|
||||
fallbackDirectionalSwap
|
||||
:: Direction
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
fallbackDirectionalSwap DirectionUp = W.swapUp
|
||||
fallbackDirectionalSwap DirectionLeft = W.swapUp
|
||||
fallbackDirectionalSwap DirectionDown = W.swapDown
|
||||
fallbackDirectionalSwap DirectionRight = W.swapDown
|
||||
|
||||
swapFocusedWithWindow
|
||||
:: Window
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
swapFocusedWithWindow target stackSet =
|
||||
case W.peek stackSet of
|
||||
Just focused | focused /= target ->
|
||||
W.modify' (swapStackOrder focused target) stackSet
|
||||
_ -> stackSet
|
||||
|
||||
swapStackOrder :: Eq a => a -> a -> W.Stack a -> W.Stack a
|
||||
swapStackOrder focused target stack =
|
||||
stackFromListFocused stack focused $
|
||||
map swapWindow (W.integrate stack)
|
||||
where
|
||||
swapWindow window
|
||||
| window == focused = target
|
||||
| window == target = focused
|
||||
| otherwise = window
|
||||
|
||||
stackFromListFocused :: Eq a => W.Stack a -> a -> [a] -> W.Stack a
|
||||
stackFromListFocused fallback focused windows =
|
||||
case break (== focused) windows of
|
||||
(before, _focused : after) -> W.Stack focused (reverse before) after
|
||||
_ -> fallback
|
||||
|
||||
focusDirectionalScreen :: Direction -> RiverWMWaylandAction l
|
||||
focusDirectionalScreen direction =
|
||||
stackAction $ \stackSet ->
|
||||
maybe stackSet ((`W.view` stackSet) . W.tag . W.workspace) $
|
||||
directionalScreenTarget direction stackSet
|
||||
|
||||
shiftFocusedToDirectionalScreen :: Bool -> Direction -> RiverWMWaylandAction l
|
||||
shiftFocusedToDirectionalScreen follow direction =
|
||||
(if follow then stackActionWarpPointer else stackAction) $ \stackSet ->
|
||||
maybe stackSet (shiftToScreen stackSet) $
|
||||
directionalScreenTarget direction stackSet
|
||||
where
|
||||
shiftToScreen stackSet screen =
|
||||
let workspace = W.tag (W.workspace screen)
|
||||
shifted = W.shift workspace stackSet
|
||||
in if follow then W.view workspace shifted else shifted
|
||||
|
||||
shiftFocusedToEmptyWorkspaceOnDirectionalScreen :: Direction -> RiverWMWaylandAction l
|
||||
shiftFocusedToEmptyWorkspaceOnDirectionalScreen direction =
|
||||
stackActionWarpPointer $ \stackSet ->
|
||||
maybe stackSet (shiftToEmptyWorkspaceOnScreen stackSet) $
|
||||
directionalScreenTarget direction stackSet
|
||||
where
|
||||
shiftToEmptyWorkspaceOnScreen stackSet screen =
|
||||
let workspace = W.tag (W.workspace screen)
|
||||
onDestination = W.view workspace (W.shift workspace stackSet)
|
||||
in maybe onDestination
|
||||
(\emptyWorkspace -> W.greedyView emptyWorkspace (W.shift emptyWorkspace onDestination))
|
||||
(nextEmptyWorkspace onDestination)
|
||||
|
||||
directionalFocus :: Direction -> RiverWMWaylandAction l
|
||||
directionalFocus direction state =
|
||||
pure $ modifyRiverWMStackSet focusDirectionalWindow state
|
||||
where
|
||||
focusDirectionalWindow stackSet =
|
||||
maybe (fallbackDirectionalFocus direction stackSet) (`W.focusWindow` stackSet) $
|
||||
directionalTarget direction state
|
||||
|
||||
fallbackDirectionalFocus
|
||||
:: Direction
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
-> W.StackSet WorkspaceId l Window sid sd
|
||||
fallbackDirectionalFocus DirectionUp = W.focusUp
|
||||
fallbackDirectionalFocus DirectionLeft = W.focusUp
|
||||
fallbackDirectionalFocus DirectionDown = W.focusDown
|
||||
fallbackDirectionalFocus DirectionRight = W.focusDown
|
||||
|
||||
directionalTarget :: Direction -> RiverWMState l -> Maybe Window
|
||||
directionalTarget direction state@RiverWMState{riverWMStackSet} =
|
||||
directionalTargetAmong (W.index riverWMStackSet) direction state
|
||||
|
||||
directionalTargetAmong :: [Window] -> Direction -> RiverWMState l -> Maybe Window
|
||||
directionalTargetAmong allowed direction RiverWMState{riverWMStackSet, riverWMWindows, riverWMWindowIds} = do
|
||||
focused <- W.peek riverWMStackSet
|
||||
focusedId <- M.lookup focused riverWMWindowIds
|
||||
focusedRect <- riverWMWindowDesired =<< M.lookup focusedId riverWMWindows
|
||||
let focusedCenter = rectCenter focusedRect
|
||||
candidates =
|
||||
[ (window, directionScore direction focusedCenter (rectCenter rect))
|
||||
| (windowId, RiverWMWindowState{riverWMWindowXWindow = window, riverWMWindowDesired = Just rect}) <-
|
||||
M.toList riverWMWindows
|
||||
, windowId /= focusedId
|
||||
, window `elem` allowed
|
||||
]
|
||||
viable = mapMaybe sequenceCandidate candidates
|
||||
fst <$> minimumMaybeBy (compare `on` snd) viable
|
||||
|
||||
directionalScreenTarget
|
||||
:: Direction
|
||||
-> W.StackSet WorkspaceId l Window sid ScreenDetail
|
||||
-> Maybe (W.Screen WorkspaceId l Window sid ScreenDetail)
|
||||
directionalScreenTarget direction stackSet =
|
||||
fst <$> minimumMaybeBy (compare `on` snd) viable
|
||||
where
|
||||
focusedCenter = screenCenter (W.current stackSet)
|
||||
candidates =
|
||||
[ (screen, directionScore direction focusedCenter (screenCenter screen))
|
||||
| screen <- W.visible stackSet
|
||||
]
|
||||
viable = mapMaybe sequenceCandidate candidates
|
||||
|
||||
screenCenter :: W.Screen WorkspaceId l Window sid ScreenDetail -> (Double, Double)
|
||||
screenCenter = rectCenter . screenRect . W.screenDetail
|
||||
|
||||
equalColumnRects :: Rectangle -> Int -> [Rectangle]
|
||||
equalColumnRects _ count | count <= 0 = []
|
||||
equalColumnRects rect 1 = [rect]
|
||||
equalColumnRects (Rectangle x y width height) count =
|
||||
[ Rectangle
|
||||
(x + fromIntegral riverOuterGap + fromIntegral (columnOffset index))
|
||||
(y + fromIntegral riverOuterGap)
|
||||
(fromIntegral (columnWidth index))
|
||||
contentHeight
|
||||
| index <- [0 .. count - 1]
|
||||
]
|
||||
where
|
||||
totalWidth = max 0 (fromIntegral width - 2 * riverOuterGap - riverInnerGap * (count - 1))
|
||||
contentHeight = fromIntegral (max 1 (fromIntegral height - 2 * riverOuterGap :: Int))
|
||||
baseWidth = totalWidth `div` count
|
||||
extraPixels = totalWidth `mod` count
|
||||
columnWidth index = baseWidth + if index < extraPixels then 1 else 0
|
||||
columnOffset index = index * baseWidth + min index extraPixels + index * riverInnerGap
|
||||
|
||||
riverOuterGap :: Int
|
||||
riverOuterGap = 10
|
||||
|
||||
riverInnerGap :: Int
|
||||
riverInnerGap = 5
|
||||
|
||||
sequenceCandidate :: (a, Maybe b) -> Maybe (a, b)
|
||||
sequenceCandidate (value, Just score) = Just (value, score)
|
||||
sequenceCandidate (_, Nothing) = Nothing
|
||||
|
||||
rectCenter :: Rectangle -> (Double, Double)
|
||||
rectCenter (Rectangle x y width height) =
|
||||
( fromIntegral x + fromIntegral width / 2
|
||||
, fromIntegral y + fromIntegral height / 2
|
||||
)
|
||||
|
||||
directionScore :: Direction -> (Double, Double) -> (Double, Double) -> Maybe (Double, Double)
|
||||
directionScore direction (fx, fy) (cx, cy) =
|
||||
case direction of
|
||||
DirectionUp | cy < fy -> Just (fy - cy, abs (cx - fx))
|
||||
DirectionDown | cy > fy -> Just (cy - fy, abs (cx - fx))
|
||||
DirectionLeft | cx < fx -> Just (fx - cx, abs (cy - fy))
|
||||
DirectionRight | cx > fx -> Just (cx - fx, abs (cy - fy))
|
||||
_ -> Nothing
|
||||
|
||||
minimumMaybeBy :: (a -> a -> Ordering) -> [a] -> Maybe a
|
||||
minimumMaybeBy _ [] = Nothing
|
||||
minimumMaybeBy compareFn xs = Just (minimumBy compareFn xs)
|
||||
|
||||
addHyperChordBindings
|
||||
:: RiverWMWaylandModifiers
|
||||
-> RiverWMWaylandModifiers
|
||||
-> [RiverWMWaylandKeyBinding l]
|
||||
-> [RiverWMWaylandKeyBinding l]
|
||||
addHyperChordBindings hyperMask chordMask bindings =
|
||||
bindings ++ M.elems chosen
|
||||
where
|
||||
existingKeys =
|
||||
M.fromList
|
||||
[ ((riverWMWaylandKeyModifiers binding, riverWMWaylandKeyKeysym binding), ())
|
||||
| binding <- bindings
|
||||
]
|
||||
|
||||
chordBinding binding@RiverWMWaylandKeyBinding{riverWMWaylandKeyModifiers} =
|
||||
binding
|
||||
{ riverWMWaylandKeyModifiers =
|
||||
(riverWMWaylandKeyModifiers .&. complement hyperMask) .|. chordMask
|
||||
}
|
||||
|
||||
candidates =
|
||||
[ ( (riverWMWaylandKeyModifiers chorded, riverWMWaylandKeyKeysym chorded)
|
||||
, (score (riverWMWaylandKeyModifiers binding), chorded)
|
||||
)
|
||||
| binding <- bindings
|
||||
, riverWMWaylandKeyModifiers binding .&. hyperMask /= 0
|
||||
, let chorded = chordBinding binding
|
||||
, M.notMember (riverWMWaylandKeyModifiers chorded, riverWMWaylandKeyKeysym chorded) existingKeys
|
||||
]
|
||||
|
||||
chosen =
|
||||
fmap snd $
|
||||
foldl' keepBest M.empty candidates
|
||||
|
||||
keepBest selected (bindingKey, candidate@(candidateScore, _binding)) =
|
||||
case M.lookup bindingKey selected of
|
||||
Nothing -> M.insert bindingKey candidate selected
|
||||
Just (bestScore, _) ->
|
||||
if candidateScore < bestScore
|
||||
then M.insert bindingKey candidate selected
|
||||
else selected
|
||||
|
||||
score modifiers =
|
||||
length $
|
||||
filter (/= 0)
|
||||
[ modifiers .&. shift
|
||||
, modifiers .&. ctrl
|
||||
, modifiers .&. alt
|
||||
, modifiers .&. hyper
|
||||
, modifiers .&. super
|
||||
, modifiers .&. riverWMWaylandModifierMod5
|
||||
]
|
||||
|
||||
noMods, shift, ctrl, alt, hyper, super, hyperChord :: RiverWMWaylandModifiers
|
||||
noMods = riverWMWaylandModifierNone
|
||||
shift = riverWMWaylandModifierShift
|
||||
ctrl = riverWMWaylandModifierCtrl
|
||||
alt = riverWMWaylandModifierAlt
|
||||
hyper = riverWMWaylandModifierHyper
|
||||
super = riverWMWaylandModifierSuper
|
||||
hyperChord = ctrl .|. alt .|. super
|
||||
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
|
||||