├── .envrc ├── .github └── workflows │ ├── cachix.yml │ └── update-flake-lock.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── flake.lock ├── flake.nix ├── package.nix └── src ├── bin └── focal-waybar.rs ├── cli ├── focal.rs ├── image.rs ├── mod.rs ├── video.rs └── waybar.rs ├── hyprland.rs ├── image.rs ├── lib.rs ├── main.rs ├── monitor.rs ├── rofi.rs ├── slurp.rs ├── sway.rs ├── video.rs └── wf_recorder.rs /.envrc: -------------------------------------------------------------------------------- 1 | watch_file flake.nix 2 | watch_file flake.lock 3 | 4 | use flake 5 | -------------------------------------------------------------------------------- /.github/workflows/cachix.yml: -------------------------------------------------------------------------------- 1 | name: "Cachix Cache" 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | package: 10 | - focal 11 | - focal-sway 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cachix/install-nix-action@v25 17 | with: 18 | nix_path: nixpkgs=channel:nixos-unstable 19 | - uses: cachix/cachix-action@v14 20 | with: 21 | name: focal 22 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 23 | - run: nix build .#${{ matrix.package }} -------------------------------------------------------------------------------- /.github/workflows/update-flake-lock.yml: -------------------------------------------------------------------------------- 1 | name: update-flake-lock 2 | on: 3 | workflow_dispatch: # allows manual triggering 4 | schedule: 5 | - cron: '0 0 1 * *' # Run monthly 6 | push: 7 | paths: 8 | - 'flake.nix' 9 | jobs: 10 | lockfile: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | - name: Install Nix 16 | uses: cachix/install-nix-action@v27 17 | with: 18 | extra_nix_config: | 19 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 20 | - name: Update flake.lock 21 | uses: DeterminateSystems/update-flake-lock@v21 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | .devenv 3 | result 4 | debug/ 5 | target/ -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "serde", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "aho-corasick" 35 | version = "1.1.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 38 | dependencies = [ 39 | "memchr", 40 | ] 41 | 42 | [[package]] 43 | name = "android-tzdata" 44 | version = "0.1.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 47 | 48 | [[package]] 49 | name = "android_system_properties" 50 | version = "0.1.5" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 53 | dependencies = [ 54 | "libc", 55 | ] 56 | 57 | [[package]] 58 | name = "anstream" 59 | version = "0.6.18" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 62 | dependencies = [ 63 | "anstyle", 64 | "anstyle-parse", 65 | "anstyle-query", 66 | "anstyle-wincon", 67 | "colorchoice", 68 | "is_terminal_polyfill", 69 | "utf8parse", 70 | ] 71 | 72 | [[package]] 73 | name = "anstyle" 74 | version = "1.0.10" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 77 | 78 | [[package]] 79 | name = "anstyle-parse" 80 | version = "0.2.6" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 83 | dependencies = [ 84 | "utf8parse", 85 | ] 86 | 87 | [[package]] 88 | name = "anstyle-query" 89 | version = "1.1.2" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 92 | dependencies = [ 93 | "windows-sys 0.59.0", 94 | ] 95 | 96 | [[package]] 97 | name = "anstyle-wincon" 98 | version = "3.0.7" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 101 | dependencies = [ 102 | "anstyle", 103 | "once_cell", 104 | "windows-sys 0.59.0", 105 | ] 106 | 107 | [[package]] 108 | name = "async-broadcast" 109 | version = "0.7.1" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" 112 | dependencies = [ 113 | "event-listener", 114 | "event-listener-strategy", 115 | "futures-core", 116 | "pin-project-lite", 117 | ] 118 | 119 | [[package]] 120 | name = "async-channel" 121 | version = "2.3.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" 124 | dependencies = [ 125 | "concurrent-queue", 126 | "event-listener-strategy", 127 | "futures-core", 128 | "pin-project-lite", 129 | ] 130 | 131 | [[package]] 132 | name = "async-executor" 133 | version = "1.13.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" 136 | dependencies = [ 137 | "async-task", 138 | "concurrent-queue", 139 | "fastrand", 140 | "futures-lite", 141 | "slab", 142 | ] 143 | 144 | [[package]] 145 | name = "async-fs" 146 | version = "2.1.2" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" 149 | dependencies = [ 150 | "async-lock", 151 | "blocking", 152 | "futures-lite", 153 | ] 154 | 155 | [[package]] 156 | name = "async-io" 157 | version = "2.4.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" 160 | dependencies = [ 161 | "async-lock", 162 | "cfg-if", 163 | "concurrent-queue", 164 | "futures-io", 165 | "futures-lite", 166 | "parking", 167 | "polling", 168 | "rustix 0.38.41", 169 | "slab", 170 | "tracing", 171 | "windows-sys 0.59.0", 172 | ] 173 | 174 | [[package]] 175 | name = "async-lock" 176 | version = "3.4.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" 179 | dependencies = [ 180 | "event-listener", 181 | "event-listener-strategy", 182 | "pin-project-lite", 183 | ] 184 | 185 | [[package]] 186 | name = "async-process" 187 | version = "2.3.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" 190 | dependencies = [ 191 | "async-channel", 192 | "async-io", 193 | "async-lock", 194 | "async-signal", 195 | "async-task", 196 | "blocking", 197 | "cfg-if", 198 | "event-listener", 199 | "futures-lite", 200 | "rustix 0.38.41", 201 | "tracing", 202 | ] 203 | 204 | [[package]] 205 | name = "async-recursion" 206 | version = "1.1.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 209 | dependencies = [ 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "async-signal" 217 | version = "0.2.10" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" 220 | dependencies = [ 221 | "async-io", 222 | "async-lock", 223 | "atomic-waker", 224 | "cfg-if", 225 | "futures-core", 226 | "futures-io", 227 | "rustix 0.38.41", 228 | "signal-hook-registry", 229 | "slab", 230 | "windows-sys 0.59.0", 231 | ] 232 | 233 | [[package]] 234 | name = "async-stream" 235 | version = "0.3.6" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 238 | dependencies = [ 239 | "async-stream-impl", 240 | "futures-core", 241 | "pin-project-lite", 242 | ] 243 | 244 | [[package]] 245 | name = "async-stream-impl" 246 | version = "0.3.6" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 249 | dependencies = [ 250 | "proc-macro2", 251 | "quote", 252 | "syn", 253 | ] 254 | 255 | [[package]] 256 | name = "async-task" 257 | version = "4.7.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 260 | 261 | [[package]] 262 | name = "async-trait" 263 | version = "0.1.83" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 266 | dependencies = [ 267 | "proc-macro2", 268 | "quote", 269 | "syn", 270 | ] 271 | 272 | [[package]] 273 | name = "atomic-waker" 274 | version = "1.1.2" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 277 | 278 | [[package]] 279 | name = "autocfg" 280 | version = "1.4.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 283 | 284 | [[package]] 285 | name = "backtrace" 286 | version = "0.3.74" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 289 | dependencies = [ 290 | "addr2line", 291 | "cfg-if", 292 | "libc", 293 | "miniz_oxide", 294 | "object", 295 | "rustc-demangle", 296 | "windows-targets", 297 | ] 298 | 299 | [[package]] 300 | name = "bitflags" 301 | version = "2.6.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 304 | 305 | [[package]] 306 | name = "block" 307 | version = "0.1.6" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 310 | 311 | [[package]] 312 | name = "blocking" 313 | version = "1.6.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" 316 | dependencies = [ 317 | "async-channel", 318 | "async-task", 319 | "futures-io", 320 | "futures-lite", 321 | "piper", 322 | ] 323 | 324 | [[package]] 325 | name = "bumpalo" 326 | version = "3.16.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 329 | 330 | [[package]] 331 | name = "bytes" 332 | version = "1.8.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 335 | 336 | [[package]] 337 | name = "cc" 338 | version = "1.2.13" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" 341 | dependencies = [ 342 | "shlex", 343 | ] 344 | 345 | [[package]] 346 | name = "cfg-if" 347 | version = "1.0.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 350 | 351 | [[package]] 352 | name = "cfg_aliases" 353 | version = "0.2.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 356 | 357 | [[package]] 358 | name = "chrono" 359 | version = "0.4.41" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 362 | dependencies = [ 363 | "android-tzdata", 364 | "iana-time-zone", 365 | "js-sys", 366 | "num-traits", 367 | "wasm-bindgen", 368 | "windows-link", 369 | ] 370 | 371 | [[package]] 372 | name = "clap" 373 | version = "4.5.38" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 376 | dependencies = [ 377 | "clap_builder", 378 | "clap_derive", 379 | ] 380 | 381 | [[package]] 382 | name = "clap_builder" 383 | version = "4.5.38" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 386 | dependencies = [ 387 | "anstream", 388 | "anstyle", 389 | "clap_lex", 390 | "strsim", 391 | ] 392 | 393 | [[package]] 394 | name = "clap_complete" 395 | version = "4.5.50" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1" 398 | dependencies = [ 399 | "clap", 400 | ] 401 | 402 | [[package]] 403 | name = "clap_derive" 404 | version = "4.5.32" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 407 | dependencies = [ 408 | "heck", 409 | "proc-macro2", 410 | "quote", 411 | "syn", 412 | ] 413 | 414 | [[package]] 415 | name = "clap_lex" 416 | version = "0.7.4" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 419 | 420 | [[package]] 421 | name = "clap_mangen" 422 | version = "0.2.26" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" 425 | dependencies = [ 426 | "clap", 427 | "roff", 428 | ] 429 | 430 | [[package]] 431 | name = "colorchoice" 432 | version = "1.0.3" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 435 | 436 | [[package]] 437 | name = "concurrent-queue" 438 | version = "2.5.0" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 441 | dependencies = [ 442 | "crossbeam-utils", 443 | ] 444 | 445 | [[package]] 446 | name = "core-foundation-sys" 447 | version = "0.8.7" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 450 | 451 | [[package]] 452 | name = "crossbeam-utils" 453 | version = "0.8.21" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 456 | 457 | [[package]] 458 | name = "ctrlc" 459 | version = "3.4.7" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" 462 | dependencies = [ 463 | "nix 0.30.1", 464 | "windows-sys 0.59.0", 465 | ] 466 | 467 | [[package]] 468 | name = "deranged" 469 | version = "0.3.11" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 472 | dependencies = [ 473 | "powerfmt", 474 | ] 475 | 476 | [[package]] 477 | name = "derive_more" 478 | version = "1.0.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 481 | dependencies = [ 482 | "derive_more-impl", 483 | ] 484 | 485 | [[package]] 486 | name = "derive_more-impl" 487 | version = "1.0.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 490 | dependencies = [ 491 | "proc-macro2", 492 | "quote", 493 | "syn", 494 | "unicode-xid", 495 | ] 496 | 497 | [[package]] 498 | name = "dirs" 499 | version = "6.0.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 502 | dependencies = [ 503 | "dirs-sys", 504 | ] 505 | 506 | [[package]] 507 | name = "dirs-next" 508 | version = "2.0.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 511 | dependencies = [ 512 | "cfg-if", 513 | "dirs-sys-next", 514 | ] 515 | 516 | [[package]] 517 | name = "dirs-sys" 518 | version = "0.5.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 521 | dependencies = [ 522 | "libc", 523 | "option-ext", 524 | "redox_users 0.5.0", 525 | "windows-sys 0.59.0", 526 | ] 527 | 528 | [[package]] 529 | name = "dirs-sys-next" 530 | version = "0.1.2" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 533 | dependencies = [ 534 | "libc", 535 | "redox_users 0.4.6", 536 | "winapi", 537 | ] 538 | 539 | [[package]] 540 | name = "either" 541 | version = "1.13.0" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 544 | 545 | [[package]] 546 | name = "endi" 547 | version = "1.1.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" 550 | 551 | [[package]] 552 | name = "enumflags2" 553 | version = "0.7.10" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" 556 | dependencies = [ 557 | "enumflags2_derive", 558 | "serde", 559 | ] 560 | 561 | [[package]] 562 | name = "enumflags2_derive" 563 | version = "0.7.10" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" 566 | dependencies = [ 567 | "proc-macro2", 568 | "quote", 569 | "syn", 570 | ] 571 | 572 | [[package]] 573 | name = "env_home" 574 | version = "0.1.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" 577 | 578 | [[package]] 579 | name = "equivalent" 580 | version = "1.0.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 583 | 584 | [[package]] 585 | name = "errno" 586 | version = "0.3.11" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 589 | dependencies = [ 590 | "libc", 591 | "windows-sys 0.59.0", 592 | ] 593 | 594 | [[package]] 595 | name = "event-listener" 596 | version = "5.3.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" 599 | dependencies = [ 600 | "concurrent-queue", 601 | "parking", 602 | "pin-project-lite", 603 | ] 604 | 605 | [[package]] 606 | name = "event-listener-strategy" 607 | version = "0.5.2" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" 610 | dependencies = [ 611 | "event-listener", 612 | "pin-project-lite", 613 | ] 614 | 615 | [[package]] 616 | name = "execute" 617 | version = "0.2.13" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "3a82608ee96ce76aeab659e9b8d3c2b787bffd223199af88c674923d861ada10" 620 | dependencies = [ 621 | "execute-command-macro", 622 | "execute-command-tokens", 623 | "generic-array", 624 | ] 625 | 626 | [[package]] 627 | name = "execute-command-macro" 628 | version = "0.1.9" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "90dec53d547564e911dc4ff3ecb726a64cf41a6fa01a2370ebc0d95175dd08bd" 631 | dependencies = [ 632 | "execute-command-macro-impl", 633 | ] 634 | 635 | [[package]] 636 | name = "execute-command-macro-impl" 637 | version = "0.1.10" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b" 640 | dependencies = [ 641 | "execute-command-tokens", 642 | "quote", 643 | "syn", 644 | ] 645 | 646 | [[package]] 647 | name = "execute-command-tokens" 648 | version = "0.1.7" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "69dc321eb6be977f44674620ca3aa21703cb20ffbe560e1ae97da08401ffbcad" 651 | 652 | [[package]] 653 | name = "fastrand" 654 | version = "2.3.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 657 | 658 | [[package]] 659 | name = "focal" 660 | version = "0.1.0" 661 | dependencies = [ 662 | "chrono", 663 | "clap", 664 | "clap_complete", 665 | "clap_mangen", 666 | "ctrlc", 667 | "dirs", 668 | "execute", 669 | "hyprland", 670 | "notify-rust", 671 | "regex", 672 | "serde", 673 | "serde_derive", 674 | "serde_json", 675 | "which", 676 | ] 677 | 678 | [[package]] 679 | name = "futures-core" 680 | version = "0.3.31" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 683 | 684 | [[package]] 685 | name = "futures-io" 686 | version = "0.3.31" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 689 | 690 | [[package]] 691 | name = "futures-lite" 692 | version = "2.6.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" 695 | dependencies = [ 696 | "fastrand", 697 | "futures-core", 698 | "futures-io", 699 | "parking", 700 | "pin-project-lite", 701 | ] 702 | 703 | [[package]] 704 | name = "futures-task" 705 | version = "0.3.31" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 708 | 709 | [[package]] 710 | name = "futures-util" 711 | version = "0.3.31" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 714 | dependencies = [ 715 | "futures-core", 716 | "futures-io", 717 | "futures-task", 718 | "memchr", 719 | "pin-project-lite", 720 | "pin-utils", 721 | "slab", 722 | ] 723 | 724 | [[package]] 725 | name = "generic-array" 726 | version = "1.1.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a" 729 | dependencies = [ 730 | "typenum", 731 | ] 732 | 733 | [[package]] 734 | name = "getrandom" 735 | version = "0.2.15" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 738 | dependencies = [ 739 | "cfg-if", 740 | "libc", 741 | "wasi", 742 | ] 743 | 744 | [[package]] 745 | name = "gimli" 746 | version = "0.31.1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 749 | 750 | [[package]] 751 | name = "hashbrown" 752 | version = "0.15.2" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 755 | 756 | [[package]] 757 | name = "heck" 758 | version = "0.5.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 761 | 762 | [[package]] 763 | name = "hermit-abi" 764 | version = "0.3.9" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 767 | 768 | [[package]] 769 | name = "hermit-abi" 770 | version = "0.4.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 773 | 774 | [[package]] 775 | name = "hex" 776 | version = "0.4.3" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 779 | 780 | [[package]] 781 | name = "hyprland" 782 | version = "0.4.0-beta.2" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "dc9c1413b6f0fd10b2e4463479490e30b2497ae4449f044da16053f5f2cb03b8" 785 | dependencies = [ 786 | "ahash", 787 | "async-stream", 788 | "derive_more", 789 | "either", 790 | "futures-lite", 791 | "hyprland-macros", 792 | "num-traits", 793 | "once_cell", 794 | "paste", 795 | "phf", 796 | "serde", 797 | "serde_json", 798 | "serde_repr", 799 | "tokio", 800 | ] 801 | 802 | [[package]] 803 | name = "hyprland-macros" 804 | version = "0.4.0-beta.2" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "69e3cbed6e560408051175d29a9ed6ad1e64a7ff443836addf797b0479f58983" 807 | dependencies = [ 808 | "proc-macro2", 809 | "quote", 810 | "syn", 811 | ] 812 | 813 | [[package]] 814 | name = "iana-time-zone" 815 | version = "0.1.61" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 818 | dependencies = [ 819 | "android_system_properties", 820 | "core-foundation-sys", 821 | "iana-time-zone-haiku", 822 | "js-sys", 823 | "wasm-bindgen", 824 | "windows-core 0.52.0", 825 | ] 826 | 827 | [[package]] 828 | name = "iana-time-zone-haiku" 829 | version = "0.1.2" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 832 | dependencies = [ 833 | "cc", 834 | ] 835 | 836 | [[package]] 837 | name = "indexmap" 838 | version = "2.7.1" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 841 | dependencies = [ 842 | "equivalent", 843 | "hashbrown", 844 | ] 845 | 846 | [[package]] 847 | name = "is_terminal_polyfill" 848 | version = "1.70.1" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 851 | 852 | [[package]] 853 | name = "itoa" 854 | version = "1.0.14" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 857 | 858 | [[package]] 859 | name = "js-sys" 860 | version = "0.3.72" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 863 | dependencies = [ 864 | "wasm-bindgen", 865 | ] 866 | 867 | [[package]] 868 | name = "libc" 869 | version = "0.2.172" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 872 | 873 | [[package]] 874 | name = "libredox" 875 | version = "0.1.3" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 878 | dependencies = [ 879 | "bitflags", 880 | "libc", 881 | ] 882 | 883 | [[package]] 884 | name = "linux-raw-sys" 885 | version = "0.4.14" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 888 | 889 | [[package]] 890 | name = "linux-raw-sys" 891 | version = "0.9.4" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 894 | 895 | [[package]] 896 | name = "log" 897 | version = "0.4.25" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 900 | 901 | [[package]] 902 | name = "mac-notification-sys" 903 | version = "0.6.2" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91" 906 | dependencies = [ 907 | "cc", 908 | "dirs-next", 909 | "objc-foundation", 910 | "objc_id", 911 | "time", 912 | ] 913 | 914 | [[package]] 915 | name = "malloc_buf" 916 | version = "0.0.6" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 919 | dependencies = [ 920 | "libc", 921 | ] 922 | 923 | [[package]] 924 | name = "memchr" 925 | version = "2.7.4" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 928 | 929 | [[package]] 930 | name = "memoffset" 931 | version = "0.9.1" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 934 | dependencies = [ 935 | "autocfg", 936 | ] 937 | 938 | [[package]] 939 | name = "miniz_oxide" 940 | version = "0.8.0" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 943 | dependencies = [ 944 | "adler2", 945 | ] 946 | 947 | [[package]] 948 | name = "mio" 949 | version = "1.0.2" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 952 | dependencies = [ 953 | "hermit-abi 0.3.9", 954 | "libc", 955 | "wasi", 956 | "windows-sys 0.52.0", 957 | ] 958 | 959 | [[package]] 960 | name = "nix" 961 | version = "0.29.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 964 | dependencies = [ 965 | "bitflags", 966 | "cfg-if", 967 | "cfg_aliases", 968 | "libc", 969 | "memoffset", 970 | ] 971 | 972 | [[package]] 973 | name = "nix" 974 | version = "0.30.1" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 977 | dependencies = [ 978 | "bitflags", 979 | "cfg-if", 980 | "cfg_aliases", 981 | "libc", 982 | ] 983 | 984 | [[package]] 985 | name = "notify-rust" 986 | version = "4.11.7" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" 989 | dependencies = [ 990 | "futures-lite", 991 | "log", 992 | "mac-notification-sys", 993 | "serde", 994 | "tauri-winrt-notification", 995 | "zbus", 996 | ] 997 | 998 | [[package]] 999 | name = "num-conv" 1000 | version = "0.1.0" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1003 | 1004 | [[package]] 1005 | name = "num-traits" 1006 | version = "0.2.19" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1009 | dependencies = [ 1010 | "autocfg", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "objc" 1015 | version = "0.2.7" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 1018 | dependencies = [ 1019 | "malloc_buf", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "objc-foundation" 1024 | version = "0.1.1" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 1027 | dependencies = [ 1028 | "block", 1029 | "objc", 1030 | "objc_id", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "objc_id" 1035 | version = "0.1.1" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 1038 | dependencies = [ 1039 | "objc", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "object" 1044 | version = "0.36.5" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 1047 | dependencies = [ 1048 | "memchr", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "once_cell" 1053 | version = "1.20.3" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 1056 | 1057 | [[package]] 1058 | name = "option-ext" 1059 | version = "0.2.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1062 | 1063 | [[package]] 1064 | name = "ordered-stream" 1065 | version = "0.2.0" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" 1068 | dependencies = [ 1069 | "futures-core", 1070 | "pin-project-lite", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "parking" 1075 | version = "2.2.1" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1078 | 1079 | [[package]] 1080 | name = "paste" 1081 | version = "1.0.15" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1084 | 1085 | [[package]] 1086 | name = "phf" 1087 | version = "0.11.2" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 1090 | dependencies = [ 1091 | "phf_macros", 1092 | "phf_shared", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "phf_generator" 1097 | version = "0.11.2" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 1100 | dependencies = [ 1101 | "phf_shared", 1102 | "rand", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "phf_macros" 1107 | version = "0.11.2" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" 1110 | dependencies = [ 1111 | "phf_generator", 1112 | "phf_shared", 1113 | "proc-macro2", 1114 | "quote", 1115 | "syn", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "phf_shared" 1120 | version = "0.11.2" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 1123 | dependencies = [ 1124 | "siphasher", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "pin-project-lite" 1129 | version = "0.2.15" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 1132 | 1133 | [[package]] 1134 | name = "pin-utils" 1135 | version = "0.1.0" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1138 | 1139 | [[package]] 1140 | name = "piper" 1141 | version = "0.2.4" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 1144 | dependencies = [ 1145 | "atomic-waker", 1146 | "fastrand", 1147 | "futures-io", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "polling" 1152 | version = "3.7.4" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" 1155 | dependencies = [ 1156 | "cfg-if", 1157 | "concurrent-queue", 1158 | "hermit-abi 0.4.0", 1159 | "pin-project-lite", 1160 | "rustix 0.38.41", 1161 | "tracing", 1162 | "windows-sys 0.59.0", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "powerfmt" 1167 | version = "0.2.0" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1170 | 1171 | [[package]] 1172 | name = "proc-macro-crate" 1173 | version = "3.2.0" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 1176 | dependencies = [ 1177 | "toml_edit", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "proc-macro2" 1182 | version = "1.0.93" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1185 | dependencies = [ 1186 | "unicode-ident", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "quick-xml" 1191 | version = "0.37.4" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" 1194 | dependencies = [ 1195 | "memchr", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "quote" 1200 | version = "1.0.38" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1203 | dependencies = [ 1204 | "proc-macro2", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "rand" 1209 | version = "0.8.5" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1212 | dependencies = [ 1213 | "rand_core", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "rand_core" 1218 | version = "0.6.4" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1221 | 1222 | [[package]] 1223 | name = "redox_users" 1224 | version = "0.4.6" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 1227 | dependencies = [ 1228 | "getrandom", 1229 | "libredox", 1230 | "thiserror 1.0.69", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "redox_users" 1235 | version = "0.5.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 1238 | dependencies = [ 1239 | "getrandom", 1240 | "libredox", 1241 | "thiserror 2.0.11", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "regex" 1246 | version = "1.11.1" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1249 | dependencies = [ 1250 | "aho-corasick", 1251 | "memchr", 1252 | "regex-automata", 1253 | "regex-syntax", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "regex-automata" 1258 | version = "0.4.8" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 1261 | dependencies = [ 1262 | "aho-corasick", 1263 | "memchr", 1264 | "regex-syntax", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "regex-syntax" 1269 | version = "0.8.5" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1272 | 1273 | [[package]] 1274 | name = "roff" 1275 | version = "0.2.2" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" 1278 | 1279 | [[package]] 1280 | name = "rustc-demangle" 1281 | version = "0.1.24" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1284 | 1285 | [[package]] 1286 | name = "rustix" 1287 | version = "0.38.41" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" 1290 | dependencies = [ 1291 | "bitflags", 1292 | "errno", 1293 | "libc", 1294 | "linux-raw-sys 0.4.14", 1295 | "windows-sys 0.52.0", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "rustix" 1300 | version = "1.0.5" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 1303 | dependencies = [ 1304 | "bitflags", 1305 | "errno", 1306 | "libc", 1307 | "linux-raw-sys 0.9.4", 1308 | "windows-sys 0.59.0", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "ryu" 1313 | version = "1.0.19" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 1316 | 1317 | [[package]] 1318 | name = "serde" 1319 | version = "1.0.219" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1322 | dependencies = [ 1323 | "serde_derive", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "serde_derive" 1328 | version = "1.0.219" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1331 | dependencies = [ 1332 | "proc-macro2", 1333 | "quote", 1334 | "syn", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "serde_json" 1339 | version = "1.0.140" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1342 | dependencies = [ 1343 | "itoa", 1344 | "memchr", 1345 | "ryu", 1346 | "serde", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "serde_repr" 1351 | version = "0.1.19" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 1354 | dependencies = [ 1355 | "proc-macro2", 1356 | "quote", 1357 | "syn", 1358 | ] 1359 | 1360 | [[package]] 1361 | name = "shlex" 1362 | version = "1.3.0" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1365 | 1366 | [[package]] 1367 | name = "signal-hook-registry" 1368 | version = "1.4.2" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1371 | dependencies = [ 1372 | "libc", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "siphasher" 1377 | version = "0.3.11" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1380 | 1381 | [[package]] 1382 | name = "slab" 1383 | version = "0.4.9" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1386 | dependencies = [ 1387 | "autocfg", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "socket2" 1392 | version = "0.5.7" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1395 | dependencies = [ 1396 | "libc", 1397 | "windows-sys 0.52.0", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "static_assertions" 1402 | version = "1.1.0" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1405 | 1406 | [[package]] 1407 | name = "strsim" 1408 | version = "0.11.1" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1411 | 1412 | [[package]] 1413 | name = "syn" 1414 | version = "2.0.98" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 1417 | dependencies = [ 1418 | "proc-macro2", 1419 | "quote", 1420 | "unicode-ident", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "tauri-winrt-notification" 1425 | version = "0.7.1" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "b35c1cfd7d68090c13eebd54e8bb2e81c13e7779f949be5f7316179f397eeb60" 1428 | dependencies = [ 1429 | "quick-xml", 1430 | "thiserror 2.0.11", 1431 | "windows", 1432 | "windows-version", 1433 | ] 1434 | 1435 | [[package]] 1436 | name = "tempfile" 1437 | version = "3.14.0" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 1440 | dependencies = [ 1441 | "cfg-if", 1442 | "fastrand", 1443 | "once_cell", 1444 | "rustix 0.38.41", 1445 | "windows-sys 0.59.0", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "thiserror" 1450 | version = "1.0.69" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1453 | dependencies = [ 1454 | "thiserror-impl 1.0.69", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "thiserror" 1459 | version = "2.0.11" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1462 | dependencies = [ 1463 | "thiserror-impl 2.0.11", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "thiserror-impl" 1468 | version = "1.0.69" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1471 | dependencies = [ 1472 | "proc-macro2", 1473 | "quote", 1474 | "syn", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "thiserror-impl" 1479 | version = "2.0.11" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1482 | dependencies = [ 1483 | "proc-macro2", 1484 | "quote", 1485 | "syn", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "time" 1490 | version = "0.3.36" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1493 | dependencies = [ 1494 | "deranged", 1495 | "num-conv", 1496 | "powerfmt", 1497 | "serde", 1498 | "time-core", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "time-core" 1503 | version = "0.1.2" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1506 | 1507 | [[package]] 1508 | name = "tokio" 1509 | version = "1.42.0" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" 1512 | dependencies = [ 1513 | "backtrace", 1514 | "bytes", 1515 | "libc", 1516 | "mio", 1517 | "pin-project-lite", 1518 | "socket2", 1519 | "tokio-macros", 1520 | "windows-sys 0.52.0", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "tokio-macros" 1525 | version = "2.4.0" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 1528 | dependencies = [ 1529 | "proc-macro2", 1530 | "quote", 1531 | "syn", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "toml_datetime" 1536 | version = "0.6.8" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1539 | 1540 | [[package]] 1541 | name = "toml_edit" 1542 | version = "0.22.24" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1545 | dependencies = [ 1546 | "indexmap", 1547 | "toml_datetime", 1548 | "winnow 0.7.4", 1549 | ] 1550 | 1551 | [[package]] 1552 | name = "tracing" 1553 | version = "0.1.40" 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" 1555 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1556 | dependencies = [ 1557 | "pin-project-lite", 1558 | "tracing-attributes", 1559 | "tracing-core", 1560 | ] 1561 | 1562 | [[package]] 1563 | name = "tracing-attributes" 1564 | version = "0.1.27" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1567 | dependencies = [ 1568 | "proc-macro2", 1569 | "quote", 1570 | "syn", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "tracing-core" 1575 | version = "0.1.32" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1578 | dependencies = [ 1579 | "once_cell", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "typenum" 1584 | version = "1.17.0" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1587 | 1588 | [[package]] 1589 | name = "uds_windows" 1590 | version = "1.1.0" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" 1593 | dependencies = [ 1594 | "memoffset", 1595 | "tempfile", 1596 | "winapi", 1597 | ] 1598 | 1599 | [[package]] 1600 | name = "unicode-ident" 1601 | version = "1.0.16" 1602 | source = "registry+https://github.com/rust-lang/crates.io-index" 1603 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 1604 | 1605 | [[package]] 1606 | name = "unicode-xid" 1607 | version = "0.2.6" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1610 | 1611 | [[package]] 1612 | name = "utf8parse" 1613 | version = "0.2.2" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1616 | 1617 | [[package]] 1618 | name = "version_check" 1619 | version = "0.9.5" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1622 | 1623 | [[package]] 1624 | name = "wasi" 1625 | version = "0.11.0+wasi-snapshot-preview1" 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" 1627 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1628 | 1629 | [[package]] 1630 | name = "wasm-bindgen" 1631 | version = "0.2.99" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 1634 | dependencies = [ 1635 | "cfg-if", 1636 | "once_cell", 1637 | "wasm-bindgen-macro", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "wasm-bindgen-backend" 1642 | version = "0.2.99" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 1645 | dependencies = [ 1646 | "bumpalo", 1647 | "log", 1648 | "proc-macro2", 1649 | "quote", 1650 | "syn", 1651 | "wasm-bindgen-shared", 1652 | ] 1653 | 1654 | [[package]] 1655 | name = "wasm-bindgen-macro" 1656 | version = "0.2.99" 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" 1658 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 1659 | dependencies = [ 1660 | "quote", 1661 | "wasm-bindgen-macro-support", 1662 | ] 1663 | 1664 | [[package]] 1665 | name = "wasm-bindgen-macro-support" 1666 | version = "0.2.99" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 1669 | dependencies = [ 1670 | "proc-macro2", 1671 | "quote", 1672 | "syn", 1673 | "wasm-bindgen-backend", 1674 | "wasm-bindgen-shared", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "wasm-bindgen-shared" 1679 | version = "0.2.99" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 1682 | 1683 | [[package]] 1684 | name = "which" 1685 | version = "7.0.3" 1686 | source = "registry+https://github.com/rust-lang/crates.io-index" 1687 | checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" 1688 | dependencies = [ 1689 | "either", 1690 | "env_home", 1691 | "rustix 1.0.5", 1692 | "winsafe", 1693 | ] 1694 | 1695 | [[package]] 1696 | name = "winapi" 1697 | version = "0.3.9" 1698 | source = "registry+https://github.com/rust-lang/crates.io-index" 1699 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1700 | dependencies = [ 1701 | "winapi-i686-pc-windows-gnu", 1702 | "winapi-x86_64-pc-windows-gnu", 1703 | ] 1704 | 1705 | [[package]] 1706 | name = "winapi-i686-pc-windows-gnu" 1707 | version = "0.4.0" 1708 | source = "registry+https://github.com/rust-lang/crates.io-index" 1709 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1710 | 1711 | [[package]] 1712 | name = "winapi-x86_64-pc-windows-gnu" 1713 | version = "0.4.0" 1714 | source = "registry+https://github.com/rust-lang/crates.io-index" 1715 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1716 | 1717 | [[package]] 1718 | name = "windows" 1719 | version = "0.60.0" 1720 | source = "registry+https://github.com/rust-lang/crates.io-index" 1721 | checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" 1722 | dependencies = [ 1723 | "windows-collections", 1724 | "windows-core 0.60.1", 1725 | "windows-future", 1726 | "windows-link", 1727 | "windows-numerics", 1728 | ] 1729 | 1730 | [[package]] 1731 | name = "windows-collections" 1732 | version = "0.1.1" 1733 | source = "registry+https://github.com/rust-lang/crates.io-index" 1734 | checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" 1735 | dependencies = [ 1736 | "windows-core 0.60.1", 1737 | ] 1738 | 1739 | [[package]] 1740 | name = "windows-core" 1741 | version = "0.52.0" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1744 | dependencies = [ 1745 | "windows-targets", 1746 | ] 1747 | 1748 | [[package]] 1749 | name = "windows-core" 1750 | version = "0.60.1" 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" 1752 | checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" 1753 | dependencies = [ 1754 | "windows-implement", 1755 | "windows-interface", 1756 | "windows-link", 1757 | "windows-result", 1758 | "windows-strings", 1759 | ] 1760 | 1761 | [[package]] 1762 | name = "windows-future" 1763 | version = "0.1.1" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" 1766 | dependencies = [ 1767 | "windows-core 0.60.1", 1768 | "windows-link", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "windows-implement" 1773 | version = "0.59.0" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" 1776 | dependencies = [ 1777 | "proc-macro2", 1778 | "quote", 1779 | "syn", 1780 | ] 1781 | 1782 | [[package]] 1783 | name = "windows-interface" 1784 | version = "0.59.1" 1785 | source = "registry+https://github.com/rust-lang/crates.io-index" 1786 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1787 | dependencies = [ 1788 | "proc-macro2", 1789 | "quote", 1790 | "syn", 1791 | ] 1792 | 1793 | [[package]] 1794 | name = "windows-link" 1795 | version = "0.1.0" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" 1798 | 1799 | [[package]] 1800 | name = "windows-numerics" 1801 | version = "0.1.1" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" 1804 | dependencies = [ 1805 | "windows-core 0.60.1", 1806 | "windows-link", 1807 | ] 1808 | 1809 | [[package]] 1810 | name = "windows-result" 1811 | version = "0.3.1" 1812 | source = "registry+https://github.com/rust-lang/crates.io-index" 1813 | checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" 1814 | dependencies = [ 1815 | "windows-link", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "windows-strings" 1820 | version = "0.3.1" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 1823 | dependencies = [ 1824 | "windows-link", 1825 | ] 1826 | 1827 | [[package]] 1828 | name = "windows-sys" 1829 | version = "0.52.0" 1830 | source = "registry+https://github.com/rust-lang/crates.io-index" 1831 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1832 | dependencies = [ 1833 | "windows-targets", 1834 | ] 1835 | 1836 | [[package]] 1837 | name = "windows-sys" 1838 | version = "0.59.0" 1839 | source = "registry+https://github.com/rust-lang/crates.io-index" 1840 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1841 | dependencies = [ 1842 | "windows-targets", 1843 | ] 1844 | 1845 | [[package]] 1846 | name = "windows-targets" 1847 | version = "0.52.6" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1850 | dependencies = [ 1851 | "windows_aarch64_gnullvm", 1852 | "windows_aarch64_msvc", 1853 | "windows_i686_gnu", 1854 | "windows_i686_gnullvm", 1855 | "windows_i686_msvc", 1856 | "windows_x86_64_gnu", 1857 | "windows_x86_64_gnullvm", 1858 | "windows_x86_64_msvc", 1859 | ] 1860 | 1861 | [[package]] 1862 | name = "windows-version" 1863 | version = "0.1.1" 1864 | source = "registry+https://github.com/rust-lang/crates.io-index" 1865 | checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" 1866 | dependencies = [ 1867 | "windows-targets", 1868 | ] 1869 | 1870 | [[package]] 1871 | name = "windows_aarch64_gnullvm" 1872 | version = "0.52.6" 1873 | source = "registry+https://github.com/rust-lang/crates.io-index" 1874 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1875 | 1876 | [[package]] 1877 | name = "windows_aarch64_msvc" 1878 | version = "0.52.6" 1879 | source = "registry+https://github.com/rust-lang/crates.io-index" 1880 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1881 | 1882 | [[package]] 1883 | name = "windows_i686_gnu" 1884 | version = "0.52.6" 1885 | source = "registry+https://github.com/rust-lang/crates.io-index" 1886 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1887 | 1888 | [[package]] 1889 | name = "windows_i686_gnullvm" 1890 | version = "0.52.6" 1891 | source = "registry+https://github.com/rust-lang/crates.io-index" 1892 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1893 | 1894 | [[package]] 1895 | name = "windows_i686_msvc" 1896 | version = "0.52.6" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1899 | 1900 | [[package]] 1901 | name = "windows_x86_64_gnu" 1902 | version = "0.52.6" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1905 | 1906 | [[package]] 1907 | name = "windows_x86_64_gnullvm" 1908 | version = "0.52.6" 1909 | source = "registry+https://github.com/rust-lang/crates.io-index" 1910 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1911 | 1912 | [[package]] 1913 | name = "windows_x86_64_msvc" 1914 | version = "0.52.6" 1915 | source = "registry+https://github.com/rust-lang/crates.io-index" 1916 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1917 | 1918 | [[package]] 1919 | name = "winnow" 1920 | version = "0.6.20" 1921 | source = "registry+https://github.com/rust-lang/crates.io-index" 1922 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 1923 | dependencies = [ 1924 | "memchr", 1925 | ] 1926 | 1927 | [[package]] 1928 | name = "winnow" 1929 | version = "0.7.4" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 1932 | dependencies = [ 1933 | "memchr", 1934 | ] 1935 | 1936 | [[package]] 1937 | name = "winsafe" 1938 | version = "0.0.19" 1939 | source = "registry+https://github.com/rust-lang/crates.io-index" 1940 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 1941 | 1942 | [[package]] 1943 | name = "xdg-home" 1944 | version = "1.3.0" 1945 | source = "registry+https://github.com/rust-lang/crates.io-index" 1946 | checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" 1947 | dependencies = [ 1948 | "libc", 1949 | "windows-sys 0.59.0", 1950 | ] 1951 | 1952 | [[package]] 1953 | name = "zbus" 1954 | version = "5.3.1" 1955 | source = "registry+https://github.com/rust-lang/crates.io-index" 1956 | checksum = "2494e4b3f44d8363eef79a8a75fc0649efb710eef65a66b5e688a5eb4afe678a" 1957 | dependencies = [ 1958 | "async-broadcast", 1959 | "async-executor", 1960 | "async-fs", 1961 | "async-io", 1962 | "async-lock", 1963 | "async-process", 1964 | "async-recursion", 1965 | "async-task", 1966 | "async-trait", 1967 | "blocking", 1968 | "enumflags2", 1969 | "event-listener", 1970 | "futures-core", 1971 | "futures-util", 1972 | "hex", 1973 | "nix 0.29.0", 1974 | "ordered-stream", 1975 | "serde", 1976 | "serde_repr", 1977 | "static_assertions", 1978 | "tracing", 1979 | "uds_windows", 1980 | "windows-sys 0.59.0", 1981 | "winnow 0.6.20", 1982 | "xdg-home", 1983 | "zbus_macros", 1984 | "zbus_names", 1985 | "zvariant", 1986 | ] 1987 | 1988 | [[package]] 1989 | name = "zbus_macros" 1990 | version = "5.3.1" 1991 | source = "registry+https://github.com/rust-lang/crates.io-index" 1992 | checksum = "445efc01929302aee95e2b25bbb62a301ea8a6369466e4278e58e7d1dfb23631" 1993 | dependencies = [ 1994 | "proc-macro-crate", 1995 | "proc-macro2", 1996 | "quote", 1997 | "syn", 1998 | "zbus_names", 1999 | "zvariant", 2000 | "zvariant_utils", 2001 | ] 2002 | 2003 | [[package]] 2004 | name = "zbus_names" 2005 | version = "4.1.1" 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" 2007 | checksum = "519629a3f80976d89c575895b05677cbc45eaf9f70d62a364d819ba646409cc8" 2008 | dependencies = [ 2009 | "serde", 2010 | "static_assertions", 2011 | "winnow 0.6.20", 2012 | "zvariant", 2013 | ] 2014 | 2015 | [[package]] 2016 | name = "zerocopy" 2017 | version = "0.7.35" 2018 | source = "registry+https://github.com/rust-lang/crates.io-index" 2019 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2020 | dependencies = [ 2021 | "zerocopy-derive", 2022 | ] 2023 | 2024 | [[package]] 2025 | name = "zerocopy-derive" 2026 | version = "0.7.35" 2027 | source = "registry+https://github.com/rust-lang/crates.io-index" 2028 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2029 | dependencies = [ 2030 | "proc-macro2", 2031 | "quote", 2032 | "syn", 2033 | ] 2034 | 2035 | [[package]] 2036 | name = "zvariant" 2037 | version = "5.2.0" 2038 | source = "registry+https://github.com/rust-lang/crates.io-index" 2039 | checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9" 2040 | dependencies = [ 2041 | "endi", 2042 | "enumflags2", 2043 | "serde", 2044 | "static_assertions", 2045 | "winnow 0.6.20", 2046 | "zvariant_derive", 2047 | "zvariant_utils", 2048 | ] 2049 | 2050 | [[package]] 2051 | name = "zvariant_derive" 2052 | version = "5.2.0" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b" 2055 | dependencies = [ 2056 | "proc-macro-crate", 2057 | "proc-macro2", 2058 | "quote", 2059 | "syn", 2060 | "zvariant_utils", 2061 | ] 2062 | 2063 | [[package]] 2064 | name = "zvariant_utils" 2065 | version = "3.1.0" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50" 2068 | dependencies = [ 2069 | "proc-macro2", 2070 | "quote", 2071 | "serde", 2072 | "static_assertions", 2073 | "syn", 2074 | "winnow 0.6.20", 2075 | ] 2076 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "focal" 3 | version = "0.1.0" 4 | authors = ["iynaix"] 5 | 6 | edition = "2024" 7 | build = "build.rs" 8 | 9 | [dependencies] 10 | chrono = "0.4.41" 11 | clap = { version = "4.5.38", features = ["derive", "string"] } 12 | clap_complete = "4.5.50" 13 | clap_mangen = "0.2.26" 14 | ctrlc = "3.4.7" 15 | dirs = "6.0.0" 16 | execute = "0.2.13" 17 | hyprland = { version = "0.4.0-beta.2" } 18 | notify-rust = "4.11.7" 19 | regex = "1.11.1" 20 | serde = "1.0.219" 21 | serde_derive = "1.0.219" 22 | serde_json = "1.0.140" 23 | which = "7.0.3" 24 | 25 | [build-dependencies] 26 | clap = { version = "4.5.38", features = ["derive", "string"] } 27 | clap_complete = "4.5.50" 28 | clap_mangen = "0.2.26" 29 | 30 | [features] 31 | default = ["hyprland", "ocr", "video"] 32 | hyprland = [] 33 | sway = [] 34 | ocr = [] 35 | video = [] 36 | waybar = [] 37 | 38 | [[bin]] 39 | name = "focal-waybar" 40 | path = "src/bin/focal-waybar.rs" 41 | required-features = ["waybar"] 42 | 43 | [lints.rust] 44 | unsafe_code = "forbid" 45 | 46 | [lints.clippy] 47 | enum_glob_use = "deny" 48 | missing_errors_doc = { level = "allow", priority = 1 } 49 | missing_panics_doc = { level = "allow", priority = 1 } 50 | must_use_candidate = { level = "allow", priority = 1 } 51 | nursery = { level = "deny", priority = -1 } 52 | pedantic = { level = "deny", priority = -1 } 53 | unwrap_used = "deny" 54 | 55 | [profile.release] 56 | strip = true 57 | lto = true 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Xianyi Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # focal 2 | 3 | focal is a cli / rofi menu for capturing and copying screenshots or videos on hyprland / sway. 4 | 5 | 6 | main menu delay menu 7 | selection selection 8 |
9 | Wallpaper made by the awesome Rosuuri 10 | 11 | ## Features 12 | 13 | - rofi menu to select area / window / entire screen to capture 14 | - rofi menu to select delay before capture 15 | - image / video is automatically copied to clipboard, ready for pasting into other programs 16 | - notifications that open captured file when clicked 17 | - all options are also available via the CLI 18 | - supports either hyprland or sway 19 | - OCR support to select text from captured image (CLI only) 20 | 21 | ## Installation 22 | 23 | ### NixOS 24 | ```nix 25 | { 26 | inputs.focal.url = "github:iynaix/focal"; 27 | } 28 | ``` 29 | 30 | A [focal cachix](https://focal.cachix.org) is also available, providing prebuilt binaries. To use it, add the following to your configuration: 31 | ```nix 32 | { 33 | nix.settings = { 34 | substituters = ["https://focal.cachix.org"]; 35 | trusted-public-keys = ["focal.cachix.org-1:/YkOWkXNH2uK7TnskrVMvda8LyCe4iIbMM1sZN2AOXY="]; 36 | }; 37 | } 38 | ``` 39 | 40 | > [!Warning] 41 | > Overriding the `wfetch` input using a `inputs.nixpkgs.follows` invalidates the cache and will cause the package to be rebuilt. 42 | 43 | 44 | Then, include it in your `environment.systemPackages` or `home.packages` by referencing the input: 45 | ```nix 46 | # for hyprland 47 | inputs.focal.packages.${pkgs.system}.default 48 | # for sway 49 | inputs.focal.packages.${pkgs.system}.focal-sway 50 | ``` 51 | 52 | Alternatively, it can also be run directly: 53 | 54 | ```sh 55 | # for hyprland 56 | nix run github:iynaix/focal 57 | # for sway 58 | nix run github:iynaix/focal#focal-sway 59 | ``` 60 | 61 | OCR support can be optionally disabled through the use of an override: 62 | ```nix 63 | (inputs.focal.packages.${pkgs.system}.default.override { ocr = false; }) 64 | ``` 65 | 66 | ### Arch Linux 67 | 68 | Arch Linux users can install from the [AUR](https://aur.archlinux.org/) or [AUR-git](https://aur.archlinux.org/packages/focal-hyprland-git). 69 | 70 | ```sh 71 | # for hyprland 72 | paru -S focal-hyprland-git 73 | # for sway 74 | paru -S focal-sway-git 75 | ``` 76 | 77 | ## Usage 78 | 79 | ```console 80 | $ focal --help 81 | focal is a cli / rofi menu for capturing and copying screenshots or videos on hyprland / sway. 82 | 83 | Usage: focal image [OPTIONS] <--rofi|--area |--selection|--monitor|--all> [FILE] 84 | focal video [OPTIONS] <--rofi|--area |--selection|--monitor|--stop> [FILE] 85 | focal help [COMMAND]... 86 | 87 | Options: 88 | -h, --help Print help 89 | -V, --version Print version 90 | 91 | focal image: 92 | Captures a screenshot. 93 | -a, --area Type of area to capture [aliases: capture] [possible values: monitor, selection, all] 94 | --selection 95 | --monitor 96 | --all 97 | --freeze Freezes the screen before selecting an area. 98 | -t, --delay Delay in seconds before capturing 99 | -s, --slurp Options to pass to slurp 100 | --no-rounded-windows Do not show rounded corners when capturing a window. (Hyprland only) 101 | --no-notify Do not show notifications 102 | --no-save Do not save the file permanently 103 | --rofi Display rofi menu for selection options 104 | --no-icons Do not show icons for rofi menu 105 | --theme Path to a rofi theme 106 | -e, --edit Edit screenshot using COMMAND 107 | The image path will be passed as $IMAGE 108 | --ocr [] Runs OCR on the selected text 109 | -h, --help Print help (see more with '--help') 110 | [FILE] Files are created in XDG_PICTURES_DIR/Screenshots if not specified 111 | 112 | focal video: 113 | Captures a video. 114 | -a, --area Type of area to capture [aliases: capture] [possible values: monitor, selection] 115 | --selection 116 | --monitor 117 | -t, --delay Delay in seconds before capturing 118 | -s, --slurp Options to pass to slurp 119 | --no-rounded-windows Do not show rounded corners when capturing a window. (Hyprland only) 120 | --no-notify Do not show notifications 121 | --no-save Do not save the file permanently 122 | --rofi Display rofi menu for selection options 123 | --no-icons Do not show icons for rofi menu 124 | --theme Path to a rofi theme 125 | --stop Stops any previous video recordings 126 | --audio [] Capture video with audio, optionally specifying an audio device 127 | --duration Duration in seconds to record 128 | -h, --help Print help (see more with '--help') 129 | [FILE] Files are created in XDG_VIDEOS_DIR/Screencasts if not specified 130 | 131 | focal help: 132 | Print this message or the help of the given subcommand(s) 133 | [COMMAND]... Print help for the subcommand(s) 134 | ``` 135 | 136 | > [!TIP] 137 | > Invoking `focal video` a second time stops any currently recording videos. 138 | 139 | Example usage as a **hyprland** keybinding: 140 | ``` 141 | bind=$mainMod, backslash, exec, focal image --area selection 142 | ``` 143 | 144 | Similarly, for a **sway** keybinding: 145 | ``` 146 | bindsym $mod+backslash exec "focal image --area selection" 147 | ``` 148 | 149 | ### Optional Waybar Module 150 | 151 | An optional `focal-waybar` script is available for [waybar](https://github.com/Alexays/Waybar) to indicate when a recording is in progress. 152 | 153 | ```console 154 | $ focal-waybar --help 155 | Updates waybar module with focal's recording status. 156 | 157 | Usage: focal-waybar [OPTIONS] 158 | 159 | Options: 160 | --recording Message to display in waybar module when recording [default: REC] 161 | --stopped Message to display in waybar module when not recording [default: ] 162 | -h, --help Print help 163 | -V, --version Print version 164 | ``` 165 | 166 | Create a custom waybar module similar to the following: 167 | 168 | ```jsonc 169 | { 170 | "custom/focal": { 171 | "exec": "focal-waybar --recording 'REC'", 172 | "format": "{}", 173 | // interval to poll for updated recording status 174 | "interval": 1, 175 | "on-click": "focal video --stop", 176 | }, 177 | } 178 | ``` 179 | 180 | focal video recordings can then be started / stopped using keybindings such as: 181 | 182 | **hyprland**: 183 | ``` 184 | bind=$mainMod, backslash, exec, focal video --rofi --audio 185 | ``` 186 | 187 | **sway**: 188 | ``` 189 | bindsym $mod+backslash exec "focal video --rofi --audio" 190 | ``` 191 | 192 | ## Packaging 193 | 194 | To build focal from source 195 | 196 | - Build dependencies 197 | * Rust (cargo, rustc) 198 | - Runtime dependencies 199 | * [grim](https://sr.ht/~emersion/grim/) 200 | * [slurp](https://github.com/emersion/slurp) 201 | * [hyprland](https://hyprland.org/) 202 | * [sway](https://swaywm.org/) 203 | * [rofi-wayland](https://github.com/lbonn/rofi) 204 | * [wl-clipboard](https://github.com/bugaevc/wl-clipboard) 205 | * [wf-recorder](https://github.com/ammen99/wf-recorder) 206 | * [ffmpeg](https://www.ffmpeg.org/) 207 | 208 | ## Hacking 209 | 210 | Just use `nix develop` -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | #[path = "src/cli/mod.rs"] 3 | mod cli; 4 | 5 | use clap::CommandFactory; 6 | use clap_mangen::Man; 7 | use std::{fs, path::PathBuf}; 8 | 9 | fn generate_man_pages() -> Result<(), Box> { 10 | let focal_cmd = cli::Cli::command(); 11 | let man_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/man"); 12 | fs::create_dir_all(&man_dir)?; 13 | 14 | // main focal man page 15 | let mut buffer = Vec::default(); 16 | Man::new(focal_cmd.clone()).render(&mut buffer)?; 17 | fs::write(man_dir.join("focal.1"), buffer)?; 18 | 19 | // subcommand man pages 20 | for subcmd in focal_cmd.get_subcommands().filter(|c| !c.is_hide_set()) { 21 | let subcmd_name = format!("focal-{}", subcmd.get_name()); 22 | let subcmd = subcmd.clone().name(&subcmd_name); 23 | 24 | let mut buffer = Vec::default(); 25 | 26 | Man::new(subcmd) 27 | .title(subcmd_name.to_uppercase()) 28 | .render(&mut buffer)?; 29 | 30 | fs::write(man_dir.join(subcmd_name + ".1"), buffer)?; 31 | } 32 | 33 | // focal-waybar man page 34 | let mut buffer = Vec::default(); 35 | Man::new(cli::waybar::Cli::command()).render(&mut buffer)?; 36 | fs::write(man_dir.join("focal-waybar.1"), buffer)?; 37 | 38 | Ok(()) 39 | } 40 | 41 | fn main() { 42 | // override with the version passed in from nix 43 | // https://github.com/rust-lang/cargo/issues/6583#issuecomment-1259871885 44 | if let Ok(val) = std::env::var("NIX_RELEASE_VERSION") { 45 | println!("cargo:rustc-env=CARGO_PKG_VERSION={val}"); 46 | } 47 | println!("cargo:rerun-if-env-changed=NIX_RELEASE_VERSION"); 48 | 49 | if let Err(err) = generate_man_pages() { 50 | println!("cargo:warning=Error generating man pages: {err}"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1743550720, 9 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "c621e8422220273271f52058f618c94e405bb0f5", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1745930157, 24 | "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "lastModified": 1743296961, 40 | "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", 41 | "owner": "nix-community", 42 | "repo": "nixpkgs.lib", 43 | "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nix-community", 48 | "repo": "nixpkgs.lib", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "flake-parts": "flake-parts", 55 | "nixpkgs": "nixpkgs", 56 | "systems": "systems" 57 | } 58 | }, 59 | "systems": { 60 | "locked": { 61 | "lastModified": 1689347949, 62 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 63 | "owner": "nix-systems", 64 | "repo": "default-linux", 65 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "nix-systems", 70 | "repo": "default-linux", 71 | "type": "github" 72 | } 73 | } 74 | }, 75 | "root": "root", 76 | "version": 7 77 | } 78 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | systems.url = "github:nix-systems/default-linux"; 6 | }; 7 | 8 | outputs = 9 | inputs@{ 10 | flake-parts, 11 | nixpkgs, 12 | self, 13 | ... 14 | }: 15 | flake-parts.lib.mkFlake { inherit inputs; } { 16 | systems = import inputs.systems; 17 | 18 | perSystem = 19 | { pkgs, ... }: 20 | { 21 | devShells = { 22 | default = pkgs.mkShell { 23 | packages = with pkgs; [ 24 | cargo-edit 25 | grim 26 | hyprland 27 | rofi-wayland 28 | slurp 29 | sway 30 | tesseract 31 | hyprpicker 32 | wl-clipboard 33 | xdg-utils # xdg-open 34 | ]; 35 | 36 | env = { 37 | # Required by rust-analyzer 38 | RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; 39 | }; 40 | 41 | nativeBuildInputs = with pkgs; [ 42 | cargo 43 | rustc 44 | rust-analyzer 45 | rustfmt 46 | clippy 47 | ]; 48 | }; 49 | }; 50 | 51 | packages = rec { 52 | focal = pkgs.callPackage ./package.nix { 53 | version = 54 | if self ? "shortRev" then 55 | self.shortRev 56 | else 57 | nixpkgs.lib.replaceStrings [ "-dirty" ] [ "" ] self.dirtyShortRev; 58 | }; 59 | default = focal; 60 | no-ocr = focal.override { ocr = false; }; 61 | no-waybar = focal.override { focalWaybar = false; }; 62 | focal-hyprland = focal.override { backend = "hyprland"; }; 63 | focal-sway = focal.override { backend = "sway"; }; 64 | focal-image = focal.override { video = false; }; 65 | }; 66 | }; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | version, 3 | lib, 4 | installShellFiles, 5 | rustPlatform, 6 | makeWrapper, 7 | ffmpeg, 8 | grim, 9 | procps, 10 | rofi-wayland, 11 | slurp, 12 | tesseract, 13 | hyprpicker, 14 | wf-recorder, 15 | wl-clipboard, 16 | xdg-utils, 17 | hyprland, 18 | sway, 19 | backend ? "hyprland", 20 | ocr ? true, 21 | video ? true, 22 | focalWaybar ? true, 23 | }: 24 | assert lib.assertOneOf "backend" backend [ 25 | "hyprland" 26 | "sway" 27 | ]; 28 | rustPlatform.buildRustPackage { 29 | pname = "focal"; 30 | 31 | src = lib.fileset.toSource { 32 | root = ./.; 33 | fileset = lib.fileset.difference ./. ( 34 | # don't include in build 35 | lib.fileset.unions [ 36 | ./README.md 37 | ./LICENSE 38 | # ./PKGBUILD 39 | ] 40 | ); 41 | }; 42 | 43 | inherit version; 44 | 45 | # inject version from nix into the build 46 | env.NIX_RELEASE_VERSION = version; 47 | 48 | cargoLock.lockFile = ./Cargo.lock; 49 | 50 | buildNoDefaultFeatures = true; 51 | buildFeatures = 52 | [ backend ] 53 | ++ lib.optionals video [ "video" ] 54 | ++ lib.optionals ocr [ "ocr" ] 55 | ++ lib.optionals focalWaybar [ "waybar" ]; 56 | 57 | nativeBuildInputs = [ 58 | installShellFiles 59 | makeWrapper 60 | ]; 61 | 62 | postInstall = 63 | let 64 | bins = [ "focal" ] ++ lib.optionals focalWaybar [ "focal-waybar" ]; 65 | in 66 | '' 67 | for cmd in ${lib.concatStringsSep " " bins}; do 68 | installShellCompletion --cmd $cmd \ 69 | --bash <($out/bin/$cmd generate bash) \ 70 | --fish <($out/bin/$cmd generate fish) \ 71 | --zsh <($out/bin/$cmd generate zsh) 72 | done 73 | 74 | installManPage target/man/* 75 | ''; 76 | 77 | postFixup = 78 | let 79 | binaries = 80 | [ 81 | grim 82 | procps 83 | rofi-wayland 84 | slurp 85 | hyprpicker 86 | wl-clipboard 87 | xdg-utils 88 | ] 89 | ++ lib.optionals (backend == "hyprland") [ hyprland ] 90 | ++ lib.optionals (backend == "sway") [ sway ] 91 | ++ lib.optionals video [ 92 | ffmpeg 93 | wf-recorder 94 | ] 95 | ++ lib.optionals ocr [ tesseract ]; 96 | in 97 | "wrapProgram $out/bin/focal --prefix PATH : ${lib.makeBinPath binaries}"; 98 | 99 | meta = with lib; { 100 | description = "Focal captures screenshots / videos using rofi, with clipboard support on hyprland"; 101 | mainProgram = "focal"; 102 | homepage = "https://github.com/iynaix/focal"; 103 | license = licenses.mit; 104 | maintainers = [ maintainers.iynaix ]; 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /src/bin/focal-waybar.rs: -------------------------------------------------------------------------------- 1 | use clap::{CommandFactory, Parser}; 2 | use focal::{ 3 | cli::{ 4 | generate_completions, 5 | waybar::{Cli, FocalWaybarSubcommands}, 6 | }, 7 | video::LockFile, 8 | }; 9 | 10 | fn main() { 11 | let args = Cli::parse(); 12 | 13 | if let Some(FocalWaybarSubcommands::Generate(args)) = args.command { 14 | generate_completions("focal-waybar", &mut Cli::command(), &args.shell); 15 | return; 16 | } 17 | 18 | let output = if LockFile::exists() { 19 | args.recording 20 | } else { 21 | args.stopped 22 | }; 23 | println!("{output}"); 24 | } 25 | -------------------------------------------------------------------------------- /src/cli/focal.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Args, Parser, Subcommand, ValueEnum}; 4 | use clap_complete::{generate, Shell}; 5 | 6 | #[allow(clippy::module_name_repetitions)] 7 | #[derive(Subcommand, Debug)] 8 | pub enum FocalSubcommand { 9 | #[command(name = "image", about = "Captures a screenshot.")] 10 | Image(super::image::ImageArgs), 11 | 12 | #[cfg(feature = "video")] 13 | #[command(name = "video", about = "Captures a video.")] 14 | Video(super::video::VideoArgs), 15 | 16 | #[command(name = "generate", about = "Generate shell completions", hide = true)] 17 | Generate(GenerateArgs), 18 | } 19 | 20 | #[derive(Subcommand, ValueEnum, Debug, Clone)] 21 | pub enum ShellCompletion { 22 | Bash, 23 | Zsh, 24 | Fish, 25 | } 26 | 27 | #[derive(Args, Debug)] 28 | pub struct GenerateArgs { 29 | #[arg(value_enum, help = "Type of shell completion to generate")] 30 | pub shell: ShellCompletion, 31 | } 32 | 33 | #[derive(Args, Debug)] 34 | pub struct CommonArgs { 35 | #[arg(short = 't', long, help = "Delay in seconds before capturing")] 36 | pub delay: Option, // sleep uses u64 37 | 38 | #[arg(short, long, help = "Options to pass to slurp")] 39 | pub slurp: Option, 40 | 41 | // not available for sway 42 | #[arg( 43 | long, 44 | hide = cfg!(not(feature = "hyprland")), 45 | help = "Do not show rounded corners when capturing a window. (Hyprland only)" 46 | )] 47 | pub no_rounded_windows: bool, 48 | 49 | #[arg(long, action, help = "Do not show notifications")] 50 | pub no_notify: bool, 51 | 52 | #[arg(long, action, help = "Do not save the file permanently")] 53 | pub no_save: bool, 54 | } 55 | 56 | #[allow(clippy::module_name_repetitions)] 57 | #[derive(Args, Debug)] 58 | pub struct RofiArgs { 59 | #[arg(long, action, help = "Display rofi menu for selection options")] 60 | pub rofi: bool, 61 | 62 | #[arg(long, action, help = "Do not show icons for rofi menu")] 63 | pub no_icons: bool, 64 | 65 | #[arg(long, action, help = "Path to a rofi theme")] 66 | pub theme: Option, 67 | } 68 | 69 | #[derive(Parser, Debug)] 70 | #[command( 71 | name = "focal", 72 | about = "focal is a cli / rofi menu for capturing and copying screenshots or videos on hyprland / sway.", 73 | author, 74 | version = env!("CARGO_PKG_VERSION"), 75 | infer_subcommands = true, 76 | flatten_help = true 77 | )] 78 | pub struct Cli { 79 | #[command(subcommand)] 80 | pub command: FocalSubcommand, 81 | } 82 | 83 | pub fn generate_completions( 84 | progname: &str, 85 | cmd: &mut clap::Command, 86 | shell_completion: &ShellCompletion, 87 | ) { 88 | match shell_completion { 89 | ShellCompletion::Bash => generate(Shell::Bash, cmd, progname, &mut std::io::stdout()), 90 | ShellCompletion::Zsh => generate(Shell::Zsh, cmd, progname, &mut std::io::stdout()), 91 | ShellCompletion::Fish => generate(Shell::Fish, cmd, progname, &mut std::io::stdout()), 92 | } 93 | } 94 | 95 | // write tests for exclusive arguments 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | use clap::error::ErrorKind; 100 | 101 | fn assert_cmd(cmd: &str, err_kind: ErrorKind, msg: &str) { 102 | let args = Cli::try_parse_from(cmd.split_whitespace()); 103 | assert!(args.is_err(), "{msg}"); 104 | assert_eq!(args.expect_err("").kind(), err_kind, "{msg}"); 105 | } 106 | 107 | #[test] 108 | fn test_exclusive_args() { 109 | assert_cmd( 110 | "focal video --rofi --area monitor", 111 | ErrorKind::ArgumentConflict, 112 | "--rofi and --area are exclusive", 113 | ); 114 | 115 | assert_cmd( 116 | "focal image --rofi --area monitor", 117 | ErrorKind::ArgumentConflict, 118 | "--rofi and --area are exclusive", 119 | ); 120 | 121 | assert_cmd( 122 | "focal image --area monitor --ocr --edit gimp", 123 | ErrorKind::ArgumentConflict, 124 | "--ocr and --edit are exclusive", 125 | ); 126 | 127 | let res = Cli::try_parse_from("focal generate fish".split_whitespace()); 128 | assert!(res.is_ok(), "generate should still work"); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/cli/image.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::{CommonArgs, RofiArgs}; 4 | use clap::{ArgGroup, Args, Subcommand, ValueEnum}; 5 | 6 | #[derive(Subcommand, ValueEnum, Debug, Clone)] 7 | pub enum CaptureArea { 8 | Monitor, 9 | Selection, 10 | All, 11 | } 12 | 13 | #[derive(Args, Debug)] 14 | #[command(group( 15 | ArgGroup::new("area_shortcuts") 16 | .args(["area", "selection", "monitor", "all"]) 17 | .multiple(false) 18 | ))] 19 | pub struct AreaArgs { 20 | #[arg( 21 | short, 22 | long, 23 | visible_alias = "capture", 24 | value_enum, 25 | help = "Type of area to capture", 26 | long_help = "Type of area to capture\nShorthand aliases are also available" 27 | )] 28 | pub area: Option, 29 | 30 | #[arg( 31 | long, 32 | group = "area_shortcuts", 33 | help = "", 34 | long_help = "Shorthand for --area selection" 35 | )] 36 | pub selection: bool, 37 | 38 | #[arg( 39 | long, 40 | group = "area_shortcuts", 41 | help = "", 42 | long_help = "Shorthand for --area monitor" 43 | )] 44 | pub monitor: bool, 45 | 46 | #[arg( 47 | long, 48 | group = "area_shortcuts", 49 | help = "", 50 | long_help = "Shorthand for --area all" 51 | )] 52 | pub all: bool, 53 | } 54 | 55 | impl AreaArgs { 56 | pub fn parse(&self) -> Option { 57 | if self.selection { 58 | Some(CaptureArea::Selection) 59 | } else if self.monitor { 60 | Some(CaptureArea::Monitor) 61 | } else if self.all { 62 | Some(CaptureArea::All) 63 | } else { 64 | self.area.clone() 65 | } 66 | } 67 | } 68 | 69 | #[allow(clippy::module_name_repetitions)] 70 | #[derive(Args, Debug)] 71 | #[command(group( 72 | ArgGroup::new("required_mode") 73 | .required(true) 74 | .multiple(false) 75 | .args(["rofi", "area", "selection", "monitor", "all"]), 76 | ))] 77 | #[command(group( 78 | ArgGroup::new("freeze_mode") 79 | .required(false) 80 | .multiple(false) 81 | .args(["rofi", "area", "selection"]), 82 | ))] 83 | pub struct ImageArgs { 84 | #[command(flatten)] 85 | pub area_args: AreaArgs, 86 | 87 | #[arg(long, action, help = "Freezes the screen before selecting an area.")] 88 | pub freeze: bool, 89 | 90 | #[command(flatten)] 91 | pub common_args: CommonArgs, 92 | 93 | #[command(flatten)] 94 | pub rofi_args: RofiArgs, 95 | 96 | #[arg( 97 | short, 98 | long, 99 | action, 100 | help = "Edit screenshot using COMMAND\nThe image path will be passed as $IMAGE", 101 | value_name = "COMMAND", 102 | conflicts_with = "ocr" 103 | )] 104 | pub edit: Option, 105 | 106 | #[arg( 107 | long, 108 | num_args = 0..=1, 109 | value_name = "LANG", 110 | default_missing_value = "", 111 | action, 112 | help = "Runs OCR on the selected text", 113 | long_help = "Runs OCR on the selected text, defaulting to English\nSupported languages can be shown using 'tesseract --list-langs'", 114 | conflicts_with = "edit", 115 | hide = cfg!(not(feature = "ocr")) 116 | )] 117 | pub ocr: Option, 118 | 119 | #[arg( 120 | name = "FILE", 121 | help = "Files are created in XDG_PICTURES_DIR/Screenshots if not specified" 122 | )] 123 | pub filename: Option, 124 | } 125 | 126 | impl ImageArgs { 127 | pub fn required_programs(&self) -> Vec<&str> { 128 | let mut progs = vec!["grim"]; 129 | 130 | if self.rofi_args.rofi { 131 | progs.push("rofi"); 132 | progs.push("slurp"); 133 | } 134 | 135 | if matches!(self.area_args.parse(), Some(CaptureArea::Selection)) { 136 | progs.push("slurp"); 137 | } 138 | 139 | if self.ocr.is_some() { 140 | progs.push("tesseract"); 141 | } 142 | 143 | progs 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | mod focal; 2 | pub mod image; 3 | pub mod video; 4 | pub mod waybar; 5 | 6 | pub use focal::*; 7 | -------------------------------------------------------------------------------- /src/cli/video.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::{CommonArgs, RofiArgs}; 4 | use clap::{ArgGroup, Args, Subcommand, ValueEnum}; 5 | 6 | #[derive(Subcommand, ValueEnum, Debug, Clone)] 7 | pub enum CaptureArea { 8 | Monitor, 9 | Selection, 10 | } 11 | 12 | #[derive(Args, Debug)] 13 | #[command(group( 14 | ArgGroup::new("area_shortcuts") 15 | .args(["area", "selection", "monitor"]) 16 | .multiple(false) 17 | ))] 18 | pub struct AreaArgs { 19 | #[arg( 20 | short, 21 | long, 22 | visible_alias = "capture", 23 | value_enum, 24 | help = "Type of area to capture", 25 | long_help = "Type of area to capture\nShorthand aliases are also available" 26 | )] 27 | pub area: Option, 28 | 29 | #[arg( 30 | long, 31 | group = "area_shortcuts", 32 | help = "", 33 | long_help = "Shorthand for --area selection" 34 | )] 35 | pub selection: bool, 36 | 37 | #[arg( 38 | long, 39 | group = "area_shortcuts", 40 | help = "", 41 | long_help = "Shorthand for --area monitor" 42 | )] 43 | pub monitor: bool, 44 | } 45 | 46 | impl AreaArgs { 47 | pub fn parse(&self) -> Option { 48 | if self.selection { 49 | Some(CaptureArea::Selection) 50 | } else if self.monitor { 51 | Some(CaptureArea::Monitor) 52 | } else { 53 | self.area.clone() 54 | } 55 | } 56 | } 57 | 58 | #[allow(clippy::module_name_repetitions)] 59 | #[derive(Args, Debug)] 60 | #[command(group( 61 | ArgGroup::new("required_mode") 62 | .required(true) 63 | .multiple(false) 64 | .args(["rofi", "area", "selection", "monitor", "stop"]), 65 | ))] 66 | pub struct VideoArgs { 67 | #[command(flatten)] 68 | pub area_args: AreaArgs, 69 | 70 | #[command(flatten)] 71 | pub common_args: CommonArgs, 72 | 73 | #[command(flatten)] 74 | pub rofi_args: RofiArgs, 75 | 76 | #[arg(long, action, help = "Stops any previous video recordings")] 77 | pub stop: bool, 78 | 79 | #[arg( 80 | long, 81 | num_args = 0..=1, 82 | value_name = "DEVICE", 83 | default_missing_value = "", 84 | help = "Capture video with audio, optionally specifying an audio device", 85 | long_help = "Capture video with audio, optionally specifying an audio device\nYou can find your device by running: pactl list sources | grep Name" 86 | )] 87 | pub audio: Option, 88 | 89 | #[arg( 90 | long, 91 | value_name = "SECONDS", 92 | action, 93 | help = "Duration in seconds to record" 94 | )] 95 | pub duration: Option, 96 | 97 | #[arg( 98 | name = "FILE", 99 | help = "Files are created in XDG_VIDEOS_DIR/Screencasts if not specified" 100 | )] 101 | pub filename: Option, 102 | } 103 | 104 | impl VideoArgs { 105 | pub fn required_programs(&self) -> Vec<&str> { 106 | let mut progs = vec!["wf-recorder", "pkill"]; 107 | 108 | if self.rofi_args.rofi { 109 | progs.push("rofi"); 110 | progs.push("slurp"); 111 | } 112 | 113 | if matches!(self.area_args.parse(), Some(CaptureArea::Selection)) { 114 | progs.push("slurp"); 115 | } 116 | 117 | progs 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/cli/waybar.rs: -------------------------------------------------------------------------------- 1 | use super::GenerateArgs; 2 | use clap::{Parser, Subcommand}; 3 | 4 | #[derive(Subcommand, Debug)] 5 | pub enum FocalWaybarSubcommands { 6 | #[command(name = "generate", about = "Generate shell completions", hide = true)] 7 | Generate(GenerateArgs), 8 | } 9 | 10 | #[derive(Parser, Debug)] 11 | #[command( 12 | name = "focal-waybar", 13 | about = "Updates waybar module with focal's recording status.", 14 | author, 15 | version = env!("CARGO_PKG_VERSION"), 16 | )] 17 | pub struct Cli { 18 | // subcommand for generating shell completions 19 | #[command(subcommand)] 20 | pub command: Option, 21 | 22 | #[arg( 23 | long, 24 | value_name = "MESSAGE", 25 | default_value = "REC", 26 | help = "Message to display in waybar module when recording" 27 | )] 28 | pub recording: String, 29 | 30 | #[arg( 31 | long, 32 | value_name = "MESSAGE", 33 | default_value = "", 34 | help = "Message to display in waybar module when not recording" 35 | )] 36 | pub stopped: String, 37 | } 38 | -------------------------------------------------------------------------------- /src/hyprland.rs: -------------------------------------------------------------------------------- 1 | use hyprland::{ 2 | data::{Clients, Monitor, Monitors, Transforms}, 3 | shared::{HyprData, HyprDataActive}, 4 | }; 5 | 6 | use crate::{ 7 | monitor::{FocalMonitor, FocalMonitors, Rotation}, 8 | SlurpGeom, 9 | }; 10 | 11 | fn to_focal_monitor(mon: &Monitor) -> FocalMonitor { 12 | FocalMonitor { 13 | name: mon.name.clone(), 14 | x: mon.x, 15 | y: mon.y, 16 | w: mon.width.into(), 17 | h: mon.height.into(), 18 | scale: mon.scale, 19 | rotation: match mon.transform { 20 | Transforms::Normal => Rotation::Normal, 21 | Transforms::Normal90 => Rotation::Normal90, 22 | Transforms::Normal180 => Rotation::Normal180, 23 | Transforms::Normal270 => Rotation::Normal270, 24 | Transforms::Flipped => Rotation::Flipped, 25 | Transforms::Flipped90 => Rotation::Flipped90, 26 | Transforms::Flipped180 => Rotation::Flipped180, 27 | Transforms::Flipped270 => Rotation::Flipped270, 28 | }, 29 | } 30 | } 31 | 32 | pub struct HyprMonitors; 33 | 34 | impl FocalMonitors for HyprMonitors { 35 | fn all() -> Vec { 36 | Monitors::get() 37 | .expect("unable to get monitors") 38 | .iter() 39 | .map(to_focal_monitor) 40 | .collect() 41 | } 42 | 43 | fn focused() -> FocalMonitor { 44 | to_focal_monitor(&Monitor::get_active().expect("unable to get active monitor")) 45 | } 46 | 47 | fn window_geoms() -> Vec { 48 | let active_wksps: Vec<_> = Monitors::get() 49 | .expect("unable to get monitors") 50 | .iter() 51 | .map(|mon| mon.active_workspace.id) 52 | .collect(); 53 | 54 | // do not error out on a different version of hyprland where the serialization might fail 55 | Clients::get().map_or(Vec::new(), |windows| { 56 | windows 57 | .iter() 58 | .filter(|&win| (active_wksps.contains(&win.workspace.id))) 59 | .map(|win| SlurpGeom { 60 | x: win.at.0.into(), 61 | y: win.at.1.into(), 62 | w: win.size.0.into(), 63 | h: win.size.1.into(), 64 | }) 65 | .collect() 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | process::{Command, Stdio}, 4 | }; 5 | 6 | use crate::{ 7 | Monitors, Rofi, SlurpGeom, check_programs, 8 | cli::{ 9 | Cli, 10 | image::{CaptureArea, ImageArgs}, 11 | }, 12 | create_parent_dirs, iso8601_filename, 13 | monitor::FocalMonitors, 14 | show_notification, 15 | }; 16 | use clap::CommandFactory; 17 | use execute::Execute; 18 | 19 | #[derive(Default)] 20 | struct Grim { 21 | monitor: String, 22 | geometry: String, 23 | output: PathBuf, 24 | } 25 | 26 | impl Grim { 27 | pub fn new(output: PathBuf) -> Self { 28 | Self { 29 | output, 30 | ..Default::default() 31 | } 32 | } 33 | 34 | pub fn geometry(mut self, geometry: &str) -> Self { 35 | self.geometry = geometry.to_string(); 36 | self 37 | } 38 | 39 | pub fn monitor(mut self, monitor: &str) -> Self { 40 | self.monitor = monitor.to_string(); 41 | self 42 | } 43 | 44 | pub fn capture(self, notify: bool) { 45 | let mut grim = Command::new("grim"); 46 | 47 | if !self.monitor.is_empty() { 48 | grim.arg("-o").arg(self.monitor); 49 | } 50 | 51 | if !self.geometry.is_empty() { 52 | grim.arg("-g").arg(self.geometry); 53 | } 54 | 55 | grim.arg(&self.output) 56 | .execute() 57 | .expect("unable to execute grim"); 58 | 59 | // show a notification 60 | if notify { 61 | show_notification( 62 | &format!("Screenshot captured to {}", &self.output.display()), 63 | Some(&self.output), 64 | ); 65 | } 66 | } 67 | } 68 | 69 | #[allow(clippy::struct_excessive_bools)] 70 | pub struct Screenshot { 71 | pub delay: Option, 72 | pub no_rounded_windows: bool, 73 | pub freeze: bool, 74 | pub edit: Option, 75 | pub icons: bool, 76 | pub notify: bool, 77 | pub slurp: Option, 78 | pub ocr: Option, 79 | pub output: PathBuf, 80 | } 81 | 82 | impl Screenshot { 83 | fn capture(&self, monitor: &str, geometry: &str) { 84 | // small delay before capture 85 | std::thread::sleep(std::time::Duration::from_millis(500)); 86 | 87 | Grim::new(self.output.clone()) 88 | .geometry(geometry) 89 | .monitor(monitor) 90 | .capture(self.ocr.is_none() && self.notify); 91 | 92 | if self.ocr.is_some() { 93 | self.ocr(); 94 | } else { 95 | if self.edit.is_some() { 96 | self.edit(); 97 | } 98 | 99 | let mut img = std::fs::File::open(&self.output).expect("failed to open image"); 100 | Command::new("wl-copy") 101 | .arg("--type") 102 | .arg("image/png") 103 | .execute_input_reader(&mut img) 104 | .expect("failed to copy image to clipboard"); 105 | } 106 | } 107 | 108 | pub fn monitor(&self) { 109 | std::thread::sleep(std::time::Duration::from_secs(self.delay.unwrap_or(0))); 110 | self.capture(&Monitors::focused().name, ""); 111 | } 112 | 113 | pub fn selection(&self) { 114 | if self.freeze { 115 | Command::new("hyprpicker") 116 | .arg("-r") 117 | .arg("-z") 118 | .spawn() 119 | .expect("could not freeze screen") 120 | .wait() 121 | .expect("could not wait for freeze screen"); 122 | std::thread::sleep(std::time::Duration::from_millis(200)); 123 | } 124 | 125 | std::thread::sleep(std::time::Duration::from_secs(self.delay.unwrap_or(0))); 126 | let (geom, is_window) = SlurpGeom::prompt(self.slurp.as_deref()); 127 | 128 | if self.freeze { 129 | Command::new("pkill") 130 | .arg("hyprpicker") 131 | .spawn() 132 | .expect("could not unfreeze screen") 133 | .wait() 134 | .expect("could not wait for unfreeze screen"); 135 | } 136 | 137 | let do_capture = || { 138 | self.capture("", &geom.to_string()); 139 | }; 140 | 141 | #[cfg(feature = "hyprland")] 142 | if is_window && self.no_rounded_windows { 143 | use hyprland::keyword::Keyword; 144 | 145 | if let Ok(Keyword { 146 | value: rounding, .. 147 | }) = Keyword::get("decoration:rounding") 148 | { 149 | Keyword::set("decoration:rounding", 0).expect("unable to disable rounding"); 150 | do_capture(); 151 | Keyword::set("decoration:rounding", rounding).expect("unable to restore rounding"); 152 | return; 153 | } 154 | } 155 | 156 | do_capture(); 157 | } 158 | 159 | pub fn all(&self) { 160 | let (w, h) = Monitors::total_dimensions(); 161 | 162 | std::thread::sleep(std::time::Duration::from_secs(self.delay.unwrap_or(0))); 163 | self.capture("", &format!("0,0 {w}x{h}")); 164 | } 165 | 166 | fn edit(&self) { 167 | if let Some(prog) = &self.edit { 168 | if prog.ends_with("swappy") { 169 | Command::new("swappy") 170 | .arg("--file") 171 | .arg(self.output.clone()) 172 | .arg("--output-file") 173 | .arg(self.output.clone()) 174 | .execute() 175 | .expect("Failed to edit screenshot with swappy"); 176 | } else { 177 | std::process::Command::new(prog) 178 | .arg(self.output.clone()) 179 | .execute() 180 | .expect("Failed to edit screenshot"); 181 | } 182 | } 183 | } 184 | 185 | fn ocr(&self) { 186 | let mut cmd = Command::new("tesseract"); 187 | cmd.arg(&self.output).arg("-"); 188 | 189 | if let Some(lang) = &self.ocr { 190 | if !lang.is_empty() { 191 | cmd.arg("-l").arg(lang); 192 | } 193 | } 194 | 195 | let output = cmd 196 | .stdout(Stdio::piped()) 197 | .execute_output() 198 | .expect("Failed to run tesseract"); 199 | 200 | Command::new("wl-copy") 201 | .stdout(Stdio::piped()) 202 | .execute_input(&output.stdout) 203 | .expect("unable to copy ocr text"); 204 | 205 | if self.notify { 206 | if let Ok(copied_text) = std::str::from_utf8(&output.stdout) { 207 | show_notification(copied_text, None); 208 | } 209 | } 210 | } 211 | 212 | pub fn rofi(&mut self, theme: Option<&PathBuf>) { 213 | let mut opts = vec!["󰒉\tSelection", "󰍹\tMonitor", "󰍺\tAll"]; 214 | 215 | // don't show "All" option if single monitor 216 | if Monitors::all().len() == 1 { 217 | opts.pop(); 218 | } 219 | 220 | if !self.icons { 221 | opts = opts 222 | .iter() 223 | .map(|s| s.split('\t').collect::>()[1]) 224 | .collect(); 225 | } 226 | 227 | let mut rofi = Rofi::new(&opts); 228 | 229 | if let Some(theme) = theme { 230 | rofi = rofi.theme(theme.clone()); 231 | } 232 | 233 | // only show edit message if an editor is provided 234 | let sel = if self.edit.is_some() { 235 | let (sel, exit_code) = rofi 236 | .arg("-kb-custom-1") 237 | .arg("Alt-e") 238 | .message("Screenshots can be edited with Alt+e") 239 | .run(); 240 | 241 | // no alt keycode selected, do not edit 242 | if exit_code != 10 { 243 | self.edit = None; 244 | } 245 | 246 | sel 247 | } else { 248 | rofi.run().0 249 | }; 250 | 251 | let sel = sel 252 | .split('\t') 253 | .collect::>() 254 | .pop() 255 | .unwrap_or_default(); 256 | 257 | match sel { 258 | "Selection" => self.selection(), 259 | "Monitor" => { 260 | self.delay = Some(Self::rofi_delay(theme)); 261 | self.monitor(); 262 | } 263 | "All" => { 264 | self.delay = Some(Self::rofi_delay(theme)); 265 | self.all(); 266 | } 267 | "" => { 268 | eprintln!("No capture selection was made."); 269 | std::process::exit(1); 270 | } 271 | _ => unimplemented!("Invalid rofi selection"), 272 | } 273 | } 274 | 275 | /// prompts the user for delay using rofi if not provided as a cli flag 276 | fn rofi_delay(theme: Option<&PathBuf>) -> u64 { 277 | let delay_options = ["0s", "3s", "5s", "10s"]; 278 | 279 | let mut rofi = Rofi::new(&delay_options).message("Select a delay"); 280 | if let Some(theme) = theme { 281 | rofi = rofi.theme(theme.clone()); 282 | } 283 | 284 | let (sel, _) = rofi.run(); 285 | 286 | if sel.is_empty() { 287 | eprintln!("No delay selection was made."); 288 | std::process::exit(1); 289 | } 290 | 291 | sel.replace('s', "") 292 | .parse::() 293 | .expect("Invalid delay specified") 294 | } 295 | } 296 | 297 | pub fn main(args: ImageArgs) { 298 | if !cfg!(feature = "ocr") && args.ocr.is_some() { 299 | Cli::command() 300 | .error( 301 | clap::error::ErrorKind::UnknownArgument, 302 | "OCR support was not built in this version of focal.", 303 | ) 304 | .exit() 305 | } 306 | 307 | // check if all required programs are installed 308 | check_programs(&args.required_programs()); 309 | 310 | let fname = format!("{}.png", iso8601_filename()); 311 | 312 | let output = if args.common_args.no_save { 313 | PathBuf::from(format!("/tmp/{fname}")) 314 | } else { 315 | create_parent_dirs(args.filename.unwrap_or_else(|| { 316 | dirs::picture_dir() 317 | .expect("could not get $XDG_PICTURES_DIR") 318 | .join(format!("Screenshots/{fname}")) 319 | })) 320 | }; 321 | 322 | let mut screenshot = Screenshot { 323 | output, 324 | delay: args.common_args.delay, 325 | freeze: args.freeze, 326 | edit: args.edit, 327 | no_rounded_windows: args.common_args.no_rounded_windows, 328 | icons: !args.rofi_args.no_icons, 329 | notify: !args.common_args.no_notify, 330 | ocr: args.ocr, 331 | slurp: args.common_args.slurp, 332 | }; 333 | 334 | if args.rofi_args.rofi { 335 | screenshot.rofi(args.rofi_args.theme.as_ref()); 336 | } else if let Some(area) = args.area_args.parse() { 337 | match area { 338 | CaptureArea::Monitor => screenshot.monitor(), 339 | CaptureArea::Selection => screenshot.selection(), 340 | CaptureArea::All => screenshot.all(), 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process::Command}; 2 | 3 | #[cfg(feature = "hyprland")] 4 | mod hyprland; 5 | #[cfg(feature = "hyprland")] 6 | use hyprland::HyprMonitors as Monitors; 7 | 8 | #[cfg(feature = "sway")] 9 | mod sway; 10 | #[cfg(feature = "sway")] 11 | use sway::SwayMonitors as Monitors; 12 | 13 | pub mod cli; 14 | pub mod image; 15 | mod monitor; 16 | pub mod rofi; 17 | mod slurp; 18 | pub mod video; 19 | mod wf_recorder; 20 | 21 | pub use image::Screenshot; 22 | pub use rofi::Rofi; 23 | pub use slurp::SlurpGeom; 24 | pub use video::Screencast; 25 | 26 | pub fn create_parent_dirs(path: PathBuf) -> PathBuf { 27 | if let Some(parent) = path.parent() { 28 | if !parent.exists() { 29 | std::fs::create_dir_all(parent).expect("failed to create parent directories"); 30 | } 31 | } 32 | 33 | path 34 | } 35 | 36 | pub fn iso8601_filename() -> String { 37 | chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) 38 | } 39 | 40 | pub fn command_json(cmd: &mut Command) -> T { 41 | let output = cmd.output().expect("Failed to execute command"); 42 | let output_str = String::from_utf8(output.stdout).expect("unable to parse utf8 from command"); 43 | 44 | serde_json::from_str(&output_str).expect("unable to parse json from command") 45 | } 46 | 47 | pub fn show_notification(body: &str, output: Option<&PathBuf>) { 48 | let mut notification = notify_rust::Notification::new(); 49 | 50 | notification.body(body); 51 | 52 | if let Some(output) = output { 53 | notification.icon(&output.to_string_lossy()); 54 | } 55 | 56 | let notification = notification 57 | .appname("focal") 58 | .timeout(3000) 59 | .action("open", "open") 60 | .show() 61 | .expect("Failed to send notification"); 62 | 63 | if let Some(output) = output { 64 | notification.wait_for_action(|action| { 65 | if action == "open" { 66 | std::process::Command::new("xdg-open") 67 | .arg(output) 68 | .spawn() 69 | .expect("Failed to open file") 70 | .wait() 71 | .expect("Failed to wait for xdg-open"); 72 | } 73 | }); 74 | } 75 | } 76 | 77 | /// check if all required programs are installed 78 | pub fn check_programs(progs: &[&str]) { 79 | let mut all_progs = std::collections::HashSet::from(["wl-copy", "xdg-open"]); 80 | 81 | all_progs.extend(progs); 82 | 83 | let not_found: Vec<_> = all_progs 84 | .into_iter() 85 | .filter(|prog| which::which(prog).is_err()) 86 | .collect(); 87 | 88 | if !not_found.is_empty() { 89 | eprintln!( 90 | "The following programs are required but not installed: {}", 91 | not_found.join(", ") 92 | ); 93 | std::process::exit(1); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{CommandFactory, Parser}; 2 | use focal::cli::{generate_completions, Cli, FocalSubcommand}; 3 | 4 | fn main() { 5 | let args = Cli::parse(); 6 | 7 | match args.command { 8 | FocalSubcommand::Generate(args) => { 9 | generate_completions("focal", &mut Cli::command(), &args.shell); 10 | } 11 | FocalSubcommand::Image(image_args) => focal::image::main(image_args), 12 | #[cfg(feature = "video")] 13 | FocalSubcommand::Video(video_args) => focal::video::main(video_args), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/monitor.rs: -------------------------------------------------------------------------------- 1 | use crate::SlurpGeom; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum Rotation { 5 | Normal, 6 | /// Clockwise 7 | Normal90, 8 | /// 180 degrees 9 | Normal180, 10 | /// Anti-clockwise 11 | Normal270, 12 | /// Flipped 13 | Flipped, 14 | /// Flipped and rotated clockwise 15 | Flipped90, 16 | /// Flipped and rotated 180 degrees 17 | Flipped180, 18 | /// Flipped and rotated anti-clockwise 19 | Flipped270, 20 | } 21 | 22 | impl Rotation { 23 | pub fn ffmpeg_transpose(&self) -> String { 24 | (match self { 25 | Self::Normal => "", 26 | Self::Normal90 => "transpose=1", 27 | Self::Normal270 => "transpose=2", 28 | Self::Normal180 => "transpose=1,transpose=1", 29 | Self::Flipped => "hflip", 30 | Self::Flipped90 => "transpose=0", 31 | Self::Flipped270 => "transpose=3", 32 | Self::Flipped180 => "hflip,transpose=1,transpose=1", 33 | }) 34 | .to_string() 35 | } 36 | } 37 | 38 | #[allow(clippy::module_name_repetitions)] 39 | #[derive(Debug, Clone)] 40 | pub struct FocalMonitor { 41 | pub name: String, 42 | pub x: i32, 43 | pub y: i32, 44 | pub w: i32, 45 | pub h: i32, 46 | pub scale: f32, 47 | pub rotation: Rotation, 48 | } 49 | 50 | pub trait FocalMonitors { 51 | /// returns a vector of all monitors 52 | fn all() -> Vec 53 | where 54 | Self: std::marker::Sized; 55 | 56 | /// returns the focused monitor 57 | fn focused() -> FocalMonitor; 58 | 59 | /// returns geometries of all visible (active) windows across all monitors 60 | fn window_geoms() -> Vec; 61 | 62 | /// total dimensions across all monitors 63 | fn total_dimensions() -> (i32, i32) 64 | where 65 | Self: std::marker::Sized, 66 | { 67 | let mut w = 0; 68 | let mut h = 0; 69 | for mon in Self::all() { 70 | w = w.max(mon.x + mon.w); 71 | h = h.max(mon.y + mon.h); 72 | } 73 | 74 | (w, h) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/rofi.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | process::{Command, Stdio}, 4 | }; 5 | 6 | use execute::Execute; 7 | 8 | pub struct Rofi { 9 | choices: Vec, 10 | command: Command, 11 | message: String, 12 | theme: PathBuf, 13 | } 14 | 15 | impl Rofi { 16 | pub fn new(choices: &[S]) -> Self 17 | where 18 | S: AsRef, 19 | { 20 | let mut cmd = Command::new("rofi"); 21 | 22 | cmd.arg("-dmenu") 23 | // hide the search input 24 | .arg("-theme-str") 25 | .arg("mainbox { children: [listview, message]; }") 26 | // use | as separator 27 | .arg("-sep") 28 | .arg("|") 29 | .arg("-disable-history") 30 | .arg("true") 31 | .arg("-cycle") 32 | .arg("true"); 33 | 34 | Self { 35 | choices: choices.iter().map(|s| s.as_ref().to_string()).collect(), 36 | command: cmd, 37 | message: String::new(), 38 | theme: dirs::cache_dir() 39 | .expect("could not get $XDG_CACHE_HOME") 40 | .join("wallust/rofi-menu-noinput.rasi"), 41 | } 42 | } 43 | 44 | #[must_use] 45 | pub fn arg>(mut self, arg: S) -> Self { 46 | self.command.arg(arg); 47 | self 48 | } 49 | 50 | #[must_use] 51 | pub fn theme(mut self, theme: PathBuf) -> Self { 52 | self.theme = theme; 53 | self 54 | } 55 | 56 | #[must_use] 57 | pub fn message(mut self, message: &str) -> Self { 58 | self.message = message.to_string(); 59 | self 60 | } 61 | 62 | pub fn run(self) -> (String, i32) { 63 | let mut cmd = self.command; 64 | 65 | if self.theme.exists() { 66 | cmd.arg("-theme").arg(self.theme); 67 | } 68 | 69 | if !self.message.is_empty() { 70 | cmd.arg("-mesg").arg(&self.message); 71 | } 72 | 73 | // hide the search input, show message if necessary 74 | cmd.arg("-theme-str").arg(format!( 75 | "mainbox {{ children: {}; }}", 76 | if self.message.is_empty() { 77 | "[ listview ]" 78 | } else { 79 | "[ listview, message ]" 80 | } 81 | )); 82 | 83 | let output = cmd 84 | .stdout(Stdio::piped()) 85 | // use | as separator 86 | .execute_input_output(self.choices.join("|").as_bytes()) 87 | .expect("failed to run rofi"); 88 | 89 | let exit_code = output.status.code().expect("rofi has not exited"); 90 | let selection = std::str::from_utf8(&output.stdout) 91 | .expect("failed to parse utf8 from rofi selection") 92 | .strip_suffix('\n') 93 | .unwrap_or_default() 94 | .to_string(); 95 | 96 | (selection, exit_code) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/slurp.rs: -------------------------------------------------------------------------------- 1 | use execute::Execute; 2 | use std::{ 3 | fmt, 4 | process::{Command, Stdio}, 5 | }; 6 | 7 | use crate::{monitor::FocalMonitors, Monitors}; 8 | 9 | #[derive(Debug)] 10 | pub struct ParseError { 11 | message: String, 12 | } 13 | 14 | impl ParseError { 15 | fn new(msg: &str) -> Self { 16 | Self { 17 | message: msg.to_string(), 18 | } 19 | } 20 | } 21 | 22 | impl fmt::Display for ParseError { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | write!(f, "{}", self.message) 25 | } 26 | } 27 | 28 | #[allow(clippy::module_name_repetitions)] 29 | #[derive(Debug, Clone, Copy)] 30 | pub struct SlurpGeom { 31 | pub w: i32, 32 | pub h: i32, 33 | pub x: i32, 34 | pub y: i32, 35 | } 36 | 37 | impl fmt::Display for SlurpGeom { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | write!(f, "{},{} {}x{}", self.x, self.y, self.w, self.h) 40 | } 41 | } 42 | 43 | impl std::str::FromStr for SlurpGeom { 44 | type Err = ParseError; 45 | 46 | fn from_str(s: &str) -> Result { 47 | let re = regex::Regex::new(r"[,\sx]+").expect("Failed to create regex for slurp geom"); 48 | 49 | let parts: Vec<_> = re 50 | .split(s) 51 | .map(|s| s.parse::().expect("Failed to parse slurp")) 52 | .collect(); 53 | 54 | if parts.len() != 4 { 55 | return Err(ParseError::new("Slurp geom must have 4 parts")); 56 | } 57 | 58 | Ok(Self { 59 | x: parts[0], 60 | y: parts[1], 61 | w: parts[2], 62 | h: parts[3], 63 | }) 64 | } 65 | } 66 | 67 | const fn round2(n: i32) -> i32 { 68 | if n % 2 == 1 { 69 | n - 1 70 | } else { 71 | n 72 | } 73 | } 74 | 75 | impl SlurpGeom { 76 | pub fn to_ffmpeg_geom(self) -> (String, String) { 77 | let Self { x, y, w, h } = self; 78 | 79 | let monitors = Monitors::all(); 80 | let mon = monitors 81 | .iter() 82 | .find(|m| x >= m.x && x <= m.x + m.w && y >= m.y && y <= m.y + m.h) 83 | .unwrap_or_else(|| { 84 | panic!("No monitor found for slurp region"); 85 | }); 86 | 87 | // get coordinates relative to monitor 88 | let (mut w, mut h) = (w, h); 89 | let (mut x, mut y) = (x - mon.x, y - mon.y); 90 | 91 | // handle monitor scaling 92 | #[allow(clippy::cast_precision_loss)] 93 | #[allow(clippy::cast_possible_truncation)] 94 | // mon.scale != 1.0 95 | if (mon.scale - 1.0).abs() > f32::EPSILON { 96 | x = (x as f32 * mon.scale).round() as i32; 97 | y = (y as f32 * mon.scale).round() as i32; 98 | w = (w as f32 * mon.scale).round() as i32; 99 | h = (h as f32 * mon.scale).round() as i32; 100 | } 101 | 102 | // h264 requires the width and height to be even 103 | w = round2(w); 104 | h = round2(h); 105 | 106 | let transpose = mon.rotation.ffmpeg_transpose(); 107 | let filter = format!( 108 | "{}crop=w={w}:h={h}:x={x}:y={y}", 109 | if transpose.is_empty() { 110 | String::new() 111 | } else { 112 | format!("{transpose}, ") 113 | } 114 | ); 115 | 116 | (mon.name.clone(), filter) 117 | } 118 | 119 | #[cfg(feature = "hyprland")] 120 | pub fn disable_fade_animation() -> Option { 121 | use hyprland::{ 122 | data::{Animations, BezierIdent}, 123 | shared::HyprData, 124 | }; 125 | 126 | // remove fade animation 127 | let anims = Animations::get().expect("unable to get animations"); 128 | anims.0.iter().find_map(|a| { 129 | (a.name == "fadeLayers").then(|| { 130 | let beizer = match &a.bezier { 131 | BezierIdent::None => "", 132 | BezierIdent::Default => "default", 133 | BezierIdent::Specified(s) => s.as_str(), 134 | }; 135 | format!( 136 | "{},{},{},{}", 137 | a.name, 138 | std::convert::Into::::into(a.enabled), 139 | a.speed, 140 | beizer 141 | ) 142 | }) 143 | }) 144 | } 145 | 146 | #[cfg(feature = "hyprland")] 147 | pub fn reset_fade_animation(anim: Option<&str>) { 148 | use hyprland::keyword::Keyword; 149 | 150 | if let Some(anim) = anim { 151 | Keyword::set("animations", anim).expect("unable to set animations"); 152 | } 153 | } 154 | 155 | /// returns the selected geometry and if a window was selected 156 | pub fn prompt(slurp_args: Option<&str>) -> (Self, bool) { 157 | let window_geoms = Monitors::window_geoms(); 158 | 159 | #[cfg(feature = "hyprland")] 160 | let orig_fade_anim = Self::disable_fade_animation(); 161 | 162 | let slurp_geoms = window_geoms 163 | .iter() 164 | .map(std::string::ToString::to_string) 165 | .collect::>() 166 | .join("\n"); 167 | 168 | let mut slurp_cmd = Command::new("slurp"); 169 | if let Some(slurp_args) = slurp_args { 170 | slurp_cmd.args(slurp_args.split_whitespace()); 171 | } else { 172 | // sane slurp defaults 173 | slurp_cmd 174 | .arg("-c") // selection border 175 | .arg("#FFFFFFC0") // 0.75 opaque white 176 | .arg("-b") // background 177 | .arg("#000000C0") // 0.75 opaque black 178 | .arg("-B") // boxes 179 | .arg("#0000007F"); // 0.5 opaque black 180 | } 181 | 182 | let sel = slurp_cmd 183 | .stdout(Stdio::piped()) 184 | .execute_input_output(&slurp_geoms) 185 | .map(|s| { 186 | std::str::from_utf8(&s.stdout).map_or_else( 187 | |_| String::new(), 188 | |s| s.strip_suffix("\n").unwrap_or_default().to_string(), 189 | ) 190 | }); 191 | 192 | // restore the original fade animation 193 | #[cfg(feature = "hyprland")] 194 | Self::reset_fade_animation(orig_fade_anim.as_deref()); 195 | 196 | match sel { 197 | Ok(ref s) if s.is_empty() => { 198 | eprintln!("No slurp selection made"); 199 | std::process::exit(1); 200 | } 201 | Err(_) => { 202 | eprintln!("Invalid slurp selection"); 203 | std::process::exit(1); 204 | } 205 | Ok(sel) => window_geoms 206 | .into_iter() 207 | .find(|geom| geom.to_string() == sel) 208 | .map_or_else( 209 | || (sel.parse().expect("Failed to parse slurp selection"), false), 210 | |sel| (sel, true), 211 | ), 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/sway.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use crate::{ 4 | command_json, 5 | monitor::{FocalMonitor, FocalMonitors, Rotation}, 6 | SlurpGeom, 7 | }; 8 | 9 | use serde_derive::Deserialize; 10 | 11 | #[derive(Debug, Deserialize)] 12 | pub struct GetOutput { 13 | pub name: String, 14 | pub rect: Rect, 15 | pub scale: f32, 16 | pub transform: String, 17 | pub focused: bool, 18 | } 19 | 20 | #[derive(Debug, Deserialize)] 21 | pub struct Rect { 22 | pub x: i32, 23 | pub y: i32, 24 | pub width: i32, 25 | pub height: i32, 26 | } 27 | 28 | #[derive(Debug, Deserialize)] 29 | pub struct GetTreeWindowNode { 30 | pub rect: Rect, 31 | pub nodes: Vec, 32 | // visible is only available in leaf (window) nodes 33 | pub visible: Option, 34 | } 35 | 36 | impl GetTreeWindowNode { 37 | /// recursively collects all leaf nodes 38 | pub fn leaf_nodes(&self) -> Vec<&Self> { 39 | let mut leaf_nodes = Vec::new(); 40 | self._leaf_nodes(&mut leaf_nodes); 41 | leaf_nodes 42 | } 43 | 44 | /// helper function for recursion 45 | fn _leaf_nodes<'a>(&'a self, leaf_nodes: &mut Vec<&'a Self>) { 46 | if self.nodes.is_empty() { 47 | leaf_nodes.push(self); 48 | } else { 49 | // recurse into child nodes 50 | for node in &self.nodes { 51 | node._leaf_nodes(leaf_nodes); 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[allow(clippy::module_name_repetitions)] 58 | pub struct SwayMonitors; 59 | 60 | fn to_focal_monitor(mon: &GetOutput) -> FocalMonitor { 61 | FocalMonitor { 62 | name: mon.name.clone(), 63 | x: mon.rect.x, 64 | y: mon.rect.y, 65 | w: mon.rect.width, 66 | h: mon.rect.height, 67 | scale: mon.scale, 68 | rotation: match mon.transform.as_str() { 69 | "normal" => Rotation::Normal, 70 | "90" => Rotation::Normal90, 71 | "270" => Rotation::Normal270, 72 | "180" => Rotation::Normal180, 73 | "flipped" => Rotation::Flipped, 74 | "flipped-90" => Rotation::Flipped90, 75 | "flipped-180" => Rotation::Flipped180, 76 | "flipped-270" => Rotation::Flipped270, 77 | _ => unimplemented!("Invalid monitor transform"), 78 | }, 79 | } 80 | } 81 | 82 | fn window_geoms_cmd(cmd: &mut Command) -> Vec { 83 | let tree: GetTreeWindowNode = command_json(cmd); 84 | 85 | tree.leaf_nodes() 86 | .iter() 87 | .filter(|&node| node.visible == Some(true)) 88 | .map(|win_node| { 89 | let rect = &win_node.rect; 90 | SlurpGeom { 91 | x: rect.x, 92 | y: rect.y, 93 | w: rect.width, 94 | h: rect.height, 95 | } 96 | }) 97 | .collect() 98 | } 99 | 100 | impl FocalMonitors for SwayMonitors { 101 | fn all() -> Vec { 102 | let monitors: Vec = command_json( 103 | Command::new("swaymsg") 104 | .arg("-t") 105 | .arg("get_outputs") 106 | .arg("--raw"), 107 | ); 108 | 109 | monitors.iter().map(to_focal_monitor).collect() 110 | } 111 | 112 | fn focused() -> FocalMonitor { 113 | let monitors: Vec = command_json( 114 | Command::new("swaymsg") 115 | .arg("-t") 116 | .arg("get_outputs") 117 | .arg("--raw"), 118 | ); 119 | 120 | monitors 121 | .iter() 122 | .find_map(|m| m.focused.then_some(to_focal_monitor(m))) 123 | .expect("no focused monitor") 124 | } 125 | 126 | fn window_geoms() -> Vec { 127 | window_geoms_cmd( 128 | Command::new("swaymsg") 129 | .arg("-t") 130 | .arg("get_tree") 131 | .arg("--raw"), 132 | ) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/video.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{path::PathBuf, process::Command, vec}; 3 | 4 | use crate::{ 5 | Monitors, Rofi, SlurpGeom, check_programs, 6 | cli::video::{CaptureArea, VideoArgs}, 7 | create_parent_dirs, iso8601_filename, 8 | monitor::FocalMonitors, 9 | show_notification, 10 | wf_recorder::WfRecorder, 11 | }; 12 | use execute::Execute; 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct LockFile { 16 | pub video: PathBuf, 17 | pub rounding: Option, 18 | } 19 | 20 | impl LockFile { 21 | fn path() -> PathBuf { 22 | dirs::runtime_dir() 23 | .expect("could not get $XDG_RUNTIME_DIR") 24 | .join("focal.lock") 25 | } 26 | 27 | pub fn exists() -> bool { 28 | Self::path().exists() 29 | } 30 | 31 | pub fn write(&self) -> std::io::Result<()> { 32 | let content = serde_json::to_string(&self).expect("failed to serialize focal.lock"); 33 | std::fs::write(Self::path(), content) 34 | } 35 | 36 | pub fn read() -> std::io::Result { 37 | let content = std::fs::read_to_string(Self::path())?; 38 | serde_json::from_str(&content) 39 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) 40 | } 41 | 42 | pub fn remove() { 43 | if Self::exists() { 44 | std::fs::remove_file(Self::path()).expect("failed to delete focal.lock"); 45 | } 46 | } 47 | } 48 | 49 | #[allow(clippy::struct_excessive_bools)] 50 | pub struct Screencast { 51 | pub delay: Option, 52 | pub icons: bool, 53 | pub audio: Option, 54 | pub no_rounded_windows: bool, 55 | pub notify: bool, 56 | pub duration: Option, 57 | pub slurp: Option, 58 | pub output: PathBuf, 59 | } 60 | 61 | impl Screencast { 62 | fn capture(&self, mon: &str, filter: &str, rounding: Option) { 63 | ctrlc::set_handler(move || { 64 | Self::stop(false); 65 | }) 66 | .expect("unable to set ctrl-c handler"); 67 | 68 | // copy the video file to clipboard 69 | Command::new("wl-copy") 70 | .arg("--type") 71 | .arg("text/uri-list") 72 | .execute_input(&format!("file://{}", self.output.display())) 73 | .expect("failed to copy video to clipboard"); 74 | 75 | // small delay before recording 76 | std::thread::sleep(std::time::Duration::from_millis(500)); 77 | 78 | let lock = LockFile { 79 | video: self.output.clone(), 80 | rounding, 81 | }; 82 | 83 | WfRecorder::new(mon, self.output.clone()) 84 | .audio(self.audio.as_deref()) 85 | .filter(filter) 86 | .record(); 87 | 88 | // write the lock file 89 | lock.write().expect("failed to write to focal.lock"); 90 | 91 | // duration provied, recording will stop by itself so no lock file is needed 92 | if let Some(duration) = self.duration { 93 | std::thread::sleep(std::time::Duration::from_secs(duration)); 94 | 95 | Self::stop(false); 96 | } 97 | } 98 | 99 | pub fn stop(notify: bool) -> bool { 100 | // kill all wf-recorder processes 101 | let wf_process = std::process::Command::new("pkill") 102 | .arg("--echo") 103 | .arg("-SIGINT") 104 | .arg("wf-recorder") 105 | .output() 106 | .expect("failed to pkill wf-recorder") 107 | .stdout; 108 | 109 | let is_killed = String::from_utf8(wf_process) 110 | .expect("failed to parse pkill output") 111 | .lines() 112 | .count() 113 | > 0; 114 | 115 | if let Ok(LockFile { video, rounding }) = LockFile::read() { 116 | LockFile::remove(); 117 | 118 | #[cfg(feature = "hyprland")] 119 | if let Some(rounding) = rounding { 120 | hyprland::keyword::Keyword::set("decoration:rounding", rounding) 121 | .expect("unable to restore rounding"); 122 | } 123 | 124 | // show notification with the video thumbnail 125 | if notify { 126 | Self::notify(&video); 127 | } 128 | 129 | return true; 130 | } 131 | 132 | is_killed 133 | } 134 | 135 | fn notify(video: &PathBuf) { 136 | let thumb_path = PathBuf::from("/tmp/focal-thumbnail.jpg"); 137 | 138 | if thumb_path.exists() { 139 | std::fs::remove_file(&thumb_path).expect("failed to remove notification thumbnail"); 140 | } 141 | 142 | Command::new("ffmpeg") 143 | .arg("-i") 144 | .arg(video) 145 | // from 3s in the video 146 | .arg("-ss") 147 | .arg("00:00:03.000") 148 | .arg("-vframes") 149 | .arg("1") 150 | .arg("-s") 151 | .arg("128x72") 152 | .arg(&thumb_path) 153 | .execute() 154 | .expect("failed to create notification thumbnail"); 155 | 156 | // show notifcation with the video thumbnail 157 | show_notification( 158 | &format!("Video captured to {}", video.display()), 159 | Some(&thumb_path), 160 | ); 161 | } 162 | 163 | pub fn selection(&self) { 164 | let (geom, is_window) = SlurpGeom::prompt(self.slurp.as_deref()); 165 | let (mon, filter) = geom.to_ffmpeg_geom(); 166 | 167 | let do_capture = |rounding: Option| { 168 | std::thread::sleep(std::time::Duration::from_secs(self.delay.unwrap_or(0))); 169 | self.capture(&mon, &filter, rounding); 170 | }; 171 | 172 | #[cfg(feature = "hyprland")] 173 | if is_window && self.no_rounded_windows { 174 | use hyprland::keyword::{Keyword, OptionValue}; 175 | 176 | if let Ok(Keyword { 177 | value: OptionValue::Int(rounding), 178 | .. 179 | }) = Keyword::get("decoration:rounding") 180 | { 181 | Keyword::set("decoration:rounding", 0).expect("unable to disable rounding"); 182 | 183 | do_capture(Some(rounding)); 184 | } 185 | } 186 | 187 | do_capture(None); 188 | } 189 | 190 | pub fn monitor(&self) { 191 | std::thread::sleep(std::time::Duration::from_secs(self.delay.unwrap_or(0))); 192 | 193 | let mon = Monitors::focused(); 194 | let transpose = mon.rotation.ffmpeg_transpose(); 195 | self.capture(&mon.name, &transpose, None); 196 | } 197 | 198 | pub fn rofi(&mut self, theme: Option<&PathBuf>) { 199 | let mut opts = vec!["󰒉\tSelection", "󰍹\tMonitor", "󰍺\tAll"]; 200 | 201 | // don't show "All" option if single monitor 202 | if Monitors::all().len() == 1 { 203 | opts.pop(); 204 | } 205 | 206 | if !self.icons { 207 | opts = opts 208 | .iter() 209 | .map(|s| s.split('\t').collect::>()[1]) 210 | .collect(); 211 | } 212 | 213 | let mut rofi = Rofi::new(&opts); 214 | 215 | if let Some(theme) = theme { 216 | rofi = rofi.theme(theme.clone()); 217 | } 218 | 219 | let (sel, exit_code) = rofi 220 | // record audio with Alt+a 221 | .arg("-kb-custom-1") 222 | .arg("Alt-a") 223 | .message("Audio can be recorded using Alt+a") 224 | .run(); 225 | 226 | // custom keyboard code selected 227 | if self.audio.is_none() { 228 | self.audio = (exit_code == 10).then_some(String::new()); 229 | } 230 | 231 | let sel = sel 232 | .split('\t') 233 | .collect::>() 234 | .pop() 235 | .unwrap_or_default(); 236 | 237 | match sel { 238 | "Monitor" => { 239 | self.delay = Some(Self::rofi_delay(theme)); 240 | self.monitor(); 241 | } 242 | "Selection" => { 243 | self.delay = Some(Self::rofi_delay(theme)); 244 | self.selection(); 245 | } 246 | "" => { 247 | eprintln!("No rofi selection was made."); 248 | std::process::exit(1); 249 | } 250 | _ => unimplemented!("Invalid rofi selection"), 251 | } 252 | } 253 | 254 | /// prompts the user for delay using rofi if not provided as a cli flag 255 | fn rofi_delay(theme: Option<&PathBuf>) -> u64 { 256 | let delay_options = ["0s", "3s", "5s", "10s"]; 257 | 258 | let mut rofi = Rofi::new(&delay_options).message("Select a delay"); 259 | if let Some(theme) = theme { 260 | rofi = rofi.theme(theme.clone()); 261 | } 262 | 263 | let (sel, _) = rofi.run(); 264 | 265 | if sel.is_empty() { 266 | eprintln!("No delay selection was made."); 267 | std::process::exit(1); 268 | } 269 | 270 | sel.replace('s', "") 271 | .parse::() 272 | .expect("Invalid delay specified") 273 | } 274 | } 275 | 276 | pub fn main(args: VideoArgs) { 277 | // stop any currently recording videos 278 | if Screencast::stop(!args.common_args.no_notify) { 279 | println!("Stopping previous recording..."); 280 | return; 281 | } 282 | 283 | // nothing left to do 284 | if args.stop { 285 | return; 286 | } 287 | 288 | // check if all required programs are installed 289 | check_programs(&args.required_programs()); 290 | 291 | let fname = format!("{}.mp4", iso8601_filename()); 292 | 293 | let output = if args.common_args.no_save { 294 | PathBuf::from(format!("/tmp/{fname}")) 295 | } else { 296 | create_parent_dirs(args.filename.unwrap_or_else(|| { 297 | dirs::video_dir() 298 | .expect("could not get $XDG_VIDEOS_DIR") 299 | .join(format!("Screencasts/{fname}")) 300 | })) 301 | }; 302 | 303 | let mut screencast = Screencast { 304 | output, 305 | icons: !args.rofi_args.no_icons, 306 | notify: !args.common_args.no_notify, 307 | no_rounded_windows: args.common_args.no_rounded_windows, 308 | delay: args.common_args.delay, 309 | duration: args.duration, 310 | audio: args.audio, 311 | slurp: args.common_args.slurp, 312 | }; 313 | 314 | if args.rofi_args.rofi { 315 | screencast.rofi(args.rofi_args.theme.as_ref()); 316 | } else if let Some(area) = args.area_args.parse() { 317 | match area { 318 | CaptureArea::Monitor => screencast.monitor(), 319 | CaptureArea::Selection => screencast.selection(), 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/wf_recorder.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | process::{Command, Stdio}, 4 | }; 5 | 6 | #[derive(Default)] 7 | pub struct WfRecorder { 8 | monitor: String, 9 | audio: Option, 10 | video: PathBuf, 11 | filter: String, 12 | } 13 | 14 | impl WfRecorder { 15 | pub fn new(monitor: &str, video: PathBuf) -> Self { 16 | Self { 17 | monitor: monitor.to_string(), 18 | video, 19 | ..Default::default() 20 | } 21 | } 22 | 23 | pub fn audio(mut self, audio: Option<&str>) -> Self { 24 | self.audio = audio.map(std::string::ToString::to_string); 25 | self 26 | } 27 | 28 | pub fn filter(mut self, filter: &str) -> Self { 29 | self.filter = filter.to_string(); 30 | self 31 | } 32 | 33 | pub fn record(self) { 34 | let mut wfrecorder = Command::new("wf-recorder"); 35 | 36 | if !self.filter.is_empty() { 37 | wfrecorder.arg("--filter").arg(&self.filter); 38 | } 39 | 40 | if let Some(device) = &self.audio { 41 | wfrecorder.arg("--audio"); 42 | 43 | if !device.is_empty() { 44 | wfrecorder.arg("--device").arg(device); 45 | } 46 | } 47 | 48 | wfrecorder 49 | .arg("--output") 50 | .arg(&self.monitor) 51 | .arg("--overwrite") 52 | .arg("-f") 53 | .arg(&self.video) 54 | .stdout(Stdio::inherit()) 55 | .stderr(Stdio::inherit()) 56 | .spawn() 57 | .expect("failed to spawn wf-recorder") 58 | .wait() 59 | .expect("failed to wait for wf-recorder"); 60 | } 61 | } 62 | --------------------------------------------------------------------------------