├── .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 |
7 |
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 |
--------------------------------------------------------------------------------