gardesk/garfield / c781055

Browse files

init garfield: window, list view, keyboard nav, IPC types

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c7810552e425f63dc0562ac9855be653df2c0742
Tree
0eb6820

15 changed files

StatusFile+-
A .gitignore 5 0
A Cargo.lock 1306 0
A Cargo.toml 45 0
A garfield-ipc/Cargo.toml 11 0
A garfield-ipc/src/lib.rs 113 0
A garfield/Cargo.toml 38 0
A garfield/src/app.rs 284 0
A garfield/src/core/entry.rs 214 0
A garfield/src/core/mod.rs 7 0
A garfield/src/lib.rs 8 0
A garfield/src/main.rs 32 0
A garfield/src/ui/list_view.rs 264 0
A garfield/src/ui/mod.rs 5 0
A garfieldctl/Cargo.toml 24 0
A garfieldctl/src/main.rs 79 0
.gitignoreadded
@@ -0,0 +1,5 @@
1
+docs/
2
+.fackr/
3
+.vscode/
4
+target/
5
+CLAUDE.md
Cargo.lockadded
1306 lines changed — click to load
@@ -0,0 +1,1306 @@
1
+# This file is automatically @generated by Cargo.
2
+# It is not intended for manual editing.
3
+version = 4
4
+
5
+[[package]]
6
+name = "aho-corasick"
7
+version = "1.1.4"
8
+source = "registry+https://github.com/rust-lang/crates.io-index"
9
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10
+dependencies = [
11
+ "memchr",
12
+]
13
+
14
+[[package]]
15
+name = "android_system_properties"
16
+version = "0.1.5"
17
+source = "registry+https://github.com/rust-lang/crates.io-index"
18
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
19
+dependencies = [
20
+ "libc",
21
+]
22
+
23
+[[package]]
24
+name = "anstream"
25
+version = "0.6.21"
26
+source = "registry+https://github.com/rust-lang/crates.io-index"
27
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
28
+dependencies = [
29
+ "anstyle",
30
+ "anstyle-parse",
31
+ "anstyle-query",
32
+ "anstyle-wincon",
33
+ "colorchoice",
34
+ "is_terminal_polyfill",
35
+ "utf8parse",
36
+]
37
+
38
+[[package]]
39
+name = "anstyle"
40
+version = "1.0.13"
41
+source = "registry+https://github.com/rust-lang/crates.io-index"
42
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
43
+
44
+[[package]]
45
+name = "anstyle-parse"
46
+version = "0.2.7"
47
+source = "registry+https://github.com/rust-lang/crates.io-index"
48
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
49
+dependencies = [
50
+ "utf8parse",
51
+]
52
+
53
+[[package]]
54
+name = "anstyle-query"
55
+version = "1.1.5"
56
+source = "registry+https://github.com/rust-lang/crates.io-index"
57
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
58
+dependencies = [
59
+ "windows-sys 0.61.2",
60
+]
61
+
62
+[[package]]
63
+name = "anstyle-wincon"
64
+version = "3.0.11"
65
+source = "registry+https://github.com/rust-lang/crates.io-index"
66
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
67
+dependencies = [
68
+ "anstyle",
69
+ "once_cell_polyfill",
70
+ "windows-sys 0.61.2",
71
+]
72
+
73
+[[package]]
74
+name = "anyhow"
75
+version = "1.0.100"
76
+source = "registry+https://github.com/rust-lang/crates.io-index"
77
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
78
+
79
+[[package]]
80
+name = "as-raw-xcb-connection"
81
+version = "1.0.1"
82
+source = "registry+https://github.com/rust-lang/crates.io-index"
83
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
84
+
85
+[[package]]
86
+name = "autocfg"
87
+version = "1.5.0"
88
+source = "registry+https://github.com/rust-lang/crates.io-index"
89
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
90
+
91
+[[package]]
92
+name = "bitflags"
93
+version = "2.10.0"
94
+source = "registry+https://github.com/rust-lang/crates.io-index"
95
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
96
+
97
+[[package]]
98
+name = "bumpalo"
99
+version = "3.19.1"
100
+source = "registry+https://github.com/rust-lang/crates.io-index"
101
+checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
102
+
103
+[[package]]
104
+name = "cairo-rs"
105
+version = "0.20.12"
106
+source = "registry+https://github.com/rust-lang/crates.io-index"
107
+checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0"
108
+dependencies = [
109
+ "bitflags",
110
+ "cairo-sys-rs",
111
+ "glib",
112
+ "libc",
113
+]
114
+
115
+[[package]]
116
+name = "cairo-sys-rs"
117
+version = "0.20.10"
118
+source = "registry+https://github.com/rust-lang/crates.io-index"
119
+checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b"
120
+dependencies = [
121
+ "glib-sys",
122
+ "libc",
123
+ "system-deps",
124
+]
125
+
126
+[[package]]
127
+name = "cc"
128
+version = "1.2.53"
129
+source = "registry+https://github.com/rust-lang/crates.io-index"
130
+checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
131
+dependencies = [
132
+ "find-msvc-tools",
133
+ "shlex",
134
+]
135
+
136
+[[package]]
137
+name = "cfg-expr"
138
+version = "0.20.5"
139
+source = "registry+https://github.com/rust-lang/crates.io-index"
140
+checksum = "21be0e1ce6cdb2ee7fff840f922fb04ead349e5cfb1e750b769132d44ce04720"
141
+dependencies = [
142
+ "smallvec",
143
+ "target-lexicon",
144
+]
145
+
146
+[[package]]
147
+name = "cfg-if"
148
+version = "1.0.4"
149
+source = "registry+https://github.com/rust-lang/crates.io-index"
150
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
151
+
152
+[[package]]
153
+name = "chrono"
154
+version = "0.4.43"
155
+source = "registry+https://github.com/rust-lang/crates.io-index"
156
+checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
157
+dependencies = [
158
+ "iana-time-zone",
159
+ "js-sys",
160
+ "num-traits",
161
+ "wasm-bindgen",
162
+ "windows-link",
163
+]
164
+
165
+[[package]]
166
+name = "clap"
167
+version = "4.5.54"
168
+source = "registry+https://github.com/rust-lang/crates.io-index"
169
+checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
170
+dependencies = [
171
+ "clap_builder",
172
+ "clap_derive",
173
+]
174
+
175
+[[package]]
176
+name = "clap_builder"
177
+version = "4.5.54"
178
+source = "registry+https://github.com/rust-lang/crates.io-index"
179
+checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
180
+dependencies = [
181
+ "anstream",
182
+ "anstyle",
183
+ "clap_lex",
184
+ "strsim",
185
+]
186
+
187
+[[package]]
188
+name = "clap_derive"
189
+version = "4.5.49"
190
+source = "registry+https://github.com/rust-lang/crates.io-index"
191
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
192
+dependencies = [
193
+ "heck",
194
+ "proc-macro2",
195
+ "quote",
196
+ "syn",
197
+]
198
+
199
+[[package]]
200
+name = "clap_lex"
201
+version = "0.7.7"
202
+source = "registry+https://github.com/rust-lang/crates.io-index"
203
+checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
204
+
205
+[[package]]
206
+name = "colorchoice"
207
+version = "1.0.4"
208
+source = "registry+https://github.com/rust-lang/crates.io-index"
209
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
210
+
211
+[[package]]
212
+name = "core-foundation-sys"
213
+version = "0.8.7"
214
+source = "registry+https://github.com/rust-lang/crates.io-index"
215
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
216
+
217
+[[package]]
218
+name = "dirs"
219
+version = "6.0.0"
220
+source = "registry+https://github.com/rust-lang/crates.io-index"
221
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
222
+dependencies = [
223
+ "dirs-sys",
224
+]
225
+
226
+[[package]]
227
+name = "dirs-sys"
228
+version = "0.5.0"
229
+source = "registry+https://github.com/rust-lang/crates.io-index"
230
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
231
+dependencies = [
232
+ "libc",
233
+ "option-ext",
234
+ "redox_users",
235
+ "windows-sys 0.61.2",
236
+]
237
+
238
+[[package]]
239
+name = "equivalent"
240
+version = "1.0.2"
241
+source = "registry+https://github.com/rust-lang/crates.io-index"
242
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
243
+
244
+[[package]]
245
+name = "errno"
246
+version = "0.3.14"
247
+source = "registry+https://github.com/rust-lang/crates.io-index"
248
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
249
+dependencies = [
250
+ "libc",
251
+ "windows-sys 0.61.2",
252
+]
253
+
254
+[[package]]
255
+name = "find-msvc-tools"
256
+version = "0.1.8"
257
+source = "registry+https://github.com/rust-lang/crates.io-index"
258
+checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
259
+
260
+[[package]]
261
+name = "futures-channel"
262
+version = "0.3.31"
263
+source = "registry+https://github.com/rust-lang/crates.io-index"
264
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
265
+dependencies = [
266
+ "futures-core",
267
+]
268
+
269
+[[package]]
270
+name = "futures-core"
271
+version = "0.3.31"
272
+source = "registry+https://github.com/rust-lang/crates.io-index"
273
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
274
+
275
+[[package]]
276
+name = "futures-executor"
277
+version = "0.3.31"
278
+source = "registry+https://github.com/rust-lang/crates.io-index"
279
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
280
+dependencies = [
281
+ "futures-core",
282
+ "futures-task",
283
+ "futures-util",
284
+]
285
+
286
+[[package]]
287
+name = "futures-io"
288
+version = "0.3.31"
289
+source = "registry+https://github.com/rust-lang/crates.io-index"
290
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
291
+
292
+[[package]]
293
+name = "futures-macro"
294
+version = "0.3.31"
295
+source = "registry+https://github.com/rust-lang/crates.io-index"
296
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
297
+dependencies = [
298
+ "proc-macro2",
299
+ "quote",
300
+ "syn",
301
+]
302
+
303
+[[package]]
304
+name = "futures-task"
305
+version = "0.3.31"
306
+source = "registry+https://github.com/rust-lang/crates.io-index"
307
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
308
+
309
+[[package]]
310
+name = "futures-util"
311
+version = "0.3.31"
312
+source = "registry+https://github.com/rust-lang/crates.io-index"
313
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
314
+dependencies = [
315
+ "futures-core",
316
+ "futures-macro",
317
+ "futures-task",
318
+ "pin-project-lite",
319
+ "pin-utils",
320
+ "slab",
321
+]
322
+
323
+[[package]]
324
+name = "garfield"
325
+version = "0.1.0"
326
+dependencies = [
327
+ "anyhow",
328
+ "chrono",
329
+ "dirs",
330
+ "garfield-ipc",
331
+ "gartk-core",
332
+ "gartk-render",
333
+ "gartk-x11",
334
+ "serde",
335
+ "serde_json",
336
+ "thiserror",
337
+ "tracing",
338
+ "tracing-subscriber",
339
+ "x11rb",
340
+]
341
+
342
+[[package]]
343
+name = "garfield-ipc"
344
+version = "0.1.0"
345
+dependencies = [
346
+ "serde",
347
+ "serde_json",
348
+ "thiserror",
349
+]
350
+
351
+[[package]]
352
+name = "garfieldctl"
353
+version = "0.1.0"
354
+dependencies = [
355
+ "anyhow",
356
+ "clap",
357
+ "garfield-ipc",
358
+ "serde",
359
+ "serde_json",
360
+]
361
+
362
+[[package]]
363
+name = "gartk-core"
364
+version = "0.1.0"
365
+dependencies = [
366
+ "serde",
367
+ "thiserror",
368
+]
369
+
370
+[[package]]
371
+name = "gartk-render"
372
+version = "0.1.0"
373
+dependencies = [
374
+ "cairo-rs",
375
+ "gartk-core",
376
+ "gartk-x11",
377
+ "pango",
378
+ "pangocairo",
379
+ "thiserror",
380
+ "tracing",
381
+ "x11rb",
382
+]
383
+
384
+[[package]]
385
+name = "gartk-x11"
386
+version = "0.1.0"
387
+dependencies = [
388
+ "gartk-core",
389
+ "thiserror",
390
+ "tracing",
391
+ "x11rb",
392
+]
393
+
394
+[[package]]
395
+name = "gethostname"
396
+version = "1.1.0"
397
+source = "registry+https://github.com/rust-lang/crates.io-index"
398
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
399
+dependencies = [
400
+ "rustix",
401
+ "windows-link",
402
+]
403
+
404
+[[package]]
405
+name = "getrandom"
406
+version = "0.2.17"
407
+source = "registry+https://github.com/rust-lang/crates.io-index"
408
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
409
+dependencies = [
410
+ "cfg-if",
411
+ "libc",
412
+ "wasi",
413
+]
414
+
415
+[[package]]
416
+name = "gio"
417
+version = "0.20.12"
418
+source = "registry+https://github.com/rust-lang/crates.io-index"
419
+checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831"
420
+dependencies = [
421
+ "futures-channel",
422
+ "futures-core",
423
+ "futures-io",
424
+ "futures-util",
425
+ "gio-sys",
426
+ "glib",
427
+ "libc",
428
+ "pin-project-lite",
429
+ "smallvec",
430
+]
431
+
432
+[[package]]
433
+name = "gio-sys"
434
+version = "0.20.10"
435
+source = "registry+https://github.com/rust-lang/crates.io-index"
436
+checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83"
437
+dependencies = [
438
+ "glib-sys",
439
+ "gobject-sys",
440
+ "libc",
441
+ "system-deps",
442
+ "windows-sys 0.59.0",
443
+]
444
+
445
+[[package]]
446
+name = "glib"
447
+version = "0.20.12"
448
+source = "registry+https://github.com/rust-lang/crates.io-index"
449
+checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683"
450
+dependencies = [
451
+ "bitflags",
452
+ "futures-channel",
453
+ "futures-core",
454
+ "futures-executor",
455
+ "futures-task",
456
+ "futures-util",
457
+ "gio-sys",
458
+ "glib-macros",
459
+ "glib-sys",
460
+ "gobject-sys",
461
+ "libc",
462
+ "memchr",
463
+ "smallvec",
464
+]
465
+
466
+[[package]]
467
+name = "glib-macros"
468
+version = "0.20.12"
469
+source = "registry+https://github.com/rust-lang/crates.io-index"
470
+checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145"
471
+dependencies = [
472
+ "heck",
473
+ "proc-macro-crate",
474
+ "proc-macro2",
475
+ "quote",
476
+ "syn",
477
+]
478
+
479
+[[package]]
480
+name = "glib-sys"
481
+version = "0.20.10"
482
+source = "registry+https://github.com/rust-lang/crates.io-index"
483
+checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215"
484
+dependencies = [
485
+ "libc",
486
+ "system-deps",
487
+]
488
+
489
+[[package]]
490
+name = "gobject-sys"
491
+version = "0.20.10"
492
+source = "registry+https://github.com/rust-lang/crates.io-index"
493
+checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda"
494
+dependencies = [
495
+ "glib-sys",
496
+ "libc",
497
+ "system-deps",
498
+]
499
+
500
+[[package]]
501
+name = "hashbrown"
502
+version = "0.16.1"
503
+source = "registry+https://github.com/rust-lang/crates.io-index"
504
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
505
+
506
+[[package]]
507
+name = "heck"
508
+version = "0.5.0"
509
+source = "registry+https://github.com/rust-lang/crates.io-index"
510
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
511
+
512
+[[package]]
513
+name = "iana-time-zone"
514
+version = "0.1.64"
515
+source = "registry+https://github.com/rust-lang/crates.io-index"
516
+checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
517
+dependencies = [
518
+ "android_system_properties",
519
+ "core-foundation-sys",
520
+ "iana-time-zone-haiku",
521
+ "js-sys",
522
+ "log",
523
+ "wasm-bindgen",
524
+ "windows-core",
525
+]
526
+
527
+[[package]]
528
+name = "iana-time-zone-haiku"
529
+version = "0.1.2"
530
+source = "registry+https://github.com/rust-lang/crates.io-index"
531
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
532
+dependencies = [
533
+ "cc",
534
+]
535
+
536
+[[package]]
537
+name = "indexmap"
538
+version = "2.13.0"
539
+source = "registry+https://github.com/rust-lang/crates.io-index"
540
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
541
+dependencies = [
542
+ "equivalent",
543
+ "hashbrown",
544
+]
545
+
546
+[[package]]
547
+name = "is_terminal_polyfill"
548
+version = "1.70.2"
549
+source = "registry+https://github.com/rust-lang/crates.io-index"
550
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
551
+
552
+[[package]]
553
+name = "itoa"
554
+version = "1.0.17"
555
+source = "registry+https://github.com/rust-lang/crates.io-index"
556
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
557
+
558
+[[package]]
559
+name = "js-sys"
560
+version = "0.3.85"
561
+source = "registry+https://github.com/rust-lang/crates.io-index"
562
+checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
563
+dependencies = [
564
+ "once_cell",
565
+ "wasm-bindgen",
566
+]
567
+
568
+[[package]]
569
+name = "lazy_static"
570
+version = "1.5.0"
571
+source = "registry+https://github.com/rust-lang/crates.io-index"
572
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
573
+
574
+[[package]]
575
+name = "libc"
576
+version = "0.2.180"
577
+source = "registry+https://github.com/rust-lang/crates.io-index"
578
+checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
579
+
580
+[[package]]
581
+name = "libredox"
582
+version = "0.1.12"
583
+source = "registry+https://github.com/rust-lang/crates.io-index"
584
+checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
585
+dependencies = [
586
+ "bitflags",
587
+ "libc",
588
+]
589
+
590
+[[package]]
591
+name = "linux-raw-sys"
592
+version = "0.11.0"
593
+source = "registry+https://github.com/rust-lang/crates.io-index"
594
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
595
+
596
+[[package]]
597
+name = "log"
598
+version = "0.4.29"
599
+source = "registry+https://github.com/rust-lang/crates.io-index"
600
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
601
+
602
+[[package]]
603
+name = "matchers"
604
+version = "0.2.0"
605
+source = "registry+https://github.com/rust-lang/crates.io-index"
606
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
607
+dependencies = [
608
+ "regex-automata",
609
+]
610
+
611
+[[package]]
612
+name = "memchr"
613
+version = "2.7.6"
614
+source = "registry+https://github.com/rust-lang/crates.io-index"
615
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
616
+
617
+[[package]]
618
+name = "nu-ansi-term"
619
+version = "0.50.3"
620
+source = "registry+https://github.com/rust-lang/crates.io-index"
621
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
622
+dependencies = [
623
+ "windows-sys 0.61.2",
624
+]
625
+
626
+[[package]]
627
+name = "num-traits"
628
+version = "0.2.19"
629
+source = "registry+https://github.com/rust-lang/crates.io-index"
630
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
631
+dependencies = [
632
+ "autocfg",
633
+]
634
+
635
+[[package]]
636
+name = "once_cell"
637
+version = "1.21.3"
638
+source = "registry+https://github.com/rust-lang/crates.io-index"
639
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
640
+
641
+[[package]]
642
+name = "once_cell_polyfill"
643
+version = "1.70.2"
644
+source = "registry+https://github.com/rust-lang/crates.io-index"
645
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
646
+
647
+[[package]]
648
+name = "option-ext"
649
+version = "0.2.0"
650
+source = "registry+https://github.com/rust-lang/crates.io-index"
651
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
652
+
653
+[[package]]
654
+name = "pango"
655
+version = "0.20.12"
656
+source = "registry+https://github.com/rust-lang/crates.io-index"
657
+checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c"
658
+dependencies = [
659
+ "gio",
660
+ "glib",
661
+ "libc",
662
+ "pango-sys",
663
+]
664
+
665
+[[package]]
666
+name = "pango-sys"
667
+version = "0.20.10"
668
+source = "registry+https://github.com/rust-lang/crates.io-index"
669
+checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa"
670
+dependencies = [
671
+ "glib-sys",
672
+ "gobject-sys",
673
+ "libc",
674
+ "system-deps",
675
+]
676
+
677
+[[package]]
678
+name = "pangocairo"
679
+version = "0.20.10"
680
+source = "registry+https://github.com/rust-lang/crates.io-index"
681
+checksum = "58890dc451db9964ac2d8874f903a4370a4b3932aa5281ff0c8d9810937ad84f"
682
+dependencies = [
683
+ "cairo-rs",
684
+ "glib",
685
+ "libc",
686
+ "pango",
687
+ "pangocairo-sys",
688
+]
689
+
690
+[[package]]
691
+name = "pangocairo-sys"
692
+version = "0.20.10"
693
+source = "registry+https://github.com/rust-lang/crates.io-index"
694
+checksum = "b9952903f88aa93e2927e7bca2d1ebae64fc26545a9280b4ce6bddeda26b5c42"
695
+dependencies = [
696
+ "cairo-sys-rs",
697
+ "glib-sys",
698
+ "libc",
699
+ "pango-sys",
700
+ "system-deps",
701
+]
702
+
703
+[[package]]
704
+name = "pin-project-lite"
705
+version = "0.2.16"
706
+source = "registry+https://github.com/rust-lang/crates.io-index"
707
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
708
+
709
+[[package]]
710
+name = "pin-utils"
711
+version = "0.1.0"
712
+source = "registry+https://github.com/rust-lang/crates.io-index"
713
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
714
+
715
+[[package]]
716
+name = "pkg-config"
717
+version = "0.3.32"
718
+source = "registry+https://github.com/rust-lang/crates.io-index"
719
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
720
+
721
+[[package]]
722
+name = "proc-macro-crate"
723
+version = "3.4.0"
724
+source = "registry+https://github.com/rust-lang/crates.io-index"
725
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
726
+dependencies = [
727
+ "toml_edit",
728
+]
729
+
730
+[[package]]
731
+name = "proc-macro2"
732
+version = "1.0.106"
733
+source = "registry+https://github.com/rust-lang/crates.io-index"
734
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
735
+dependencies = [
736
+ "unicode-ident",
737
+]
738
+
739
+[[package]]
740
+name = "quote"
741
+version = "1.0.43"
742
+source = "registry+https://github.com/rust-lang/crates.io-index"
743
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
744
+dependencies = [
745
+ "proc-macro2",
746
+]
747
+
748
+[[package]]
749
+name = "redox_users"
750
+version = "0.5.2"
751
+source = "registry+https://github.com/rust-lang/crates.io-index"
752
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
753
+dependencies = [
754
+ "getrandom",
755
+ "libredox",
756
+ "thiserror",
757
+]
758
+
759
+[[package]]
760
+name = "regex-automata"
761
+version = "0.4.13"
762
+source = "registry+https://github.com/rust-lang/crates.io-index"
763
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
764
+dependencies = [
765
+ "aho-corasick",
766
+ "memchr",
767
+ "regex-syntax",
768
+]
769
+
770
+[[package]]
771
+name = "regex-syntax"
772
+version = "0.8.8"
773
+source = "registry+https://github.com/rust-lang/crates.io-index"
774
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
775
+
776
+[[package]]
777
+name = "rustix"
778
+version = "1.1.3"
779
+source = "registry+https://github.com/rust-lang/crates.io-index"
780
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
781
+dependencies = [
782
+ "bitflags",
783
+ "errno",
784
+ "libc",
785
+ "linux-raw-sys",
786
+ "windows-sys 0.61.2",
787
+]
788
+
789
+[[package]]
790
+name = "rustversion"
791
+version = "1.0.22"
792
+source = "registry+https://github.com/rust-lang/crates.io-index"
793
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
794
+
795
+[[package]]
796
+name = "serde"
797
+version = "1.0.228"
798
+source = "registry+https://github.com/rust-lang/crates.io-index"
799
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
800
+dependencies = [
801
+ "serde_core",
802
+ "serde_derive",
803
+]
804
+
805
+[[package]]
806
+name = "serde_core"
807
+version = "1.0.228"
808
+source = "registry+https://github.com/rust-lang/crates.io-index"
809
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
810
+dependencies = [
811
+ "serde_derive",
812
+]
813
+
814
+[[package]]
815
+name = "serde_derive"
816
+version = "1.0.228"
817
+source = "registry+https://github.com/rust-lang/crates.io-index"
818
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
819
+dependencies = [
820
+ "proc-macro2",
821
+ "quote",
822
+ "syn",
823
+]
824
+
825
+[[package]]
826
+name = "serde_json"
827
+version = "1.0.149"
828
+source = "registry+https://github.com/rust-lang/crates.io-index"
829
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
830
+dependencies = [
831
+ "itoa",
832
+ "memchr",
833
+ "serde",
834
+ "serde_core",
835
+ "zmij",
836
+]
837
+
838
+[[package]]
839
+name = "serde_spanned"
840
+version = "1.0.4"
841
+source = "registry+https://github.com/rust-lang/crates.io-index"
842
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
843
+dependencies = [
844
+ "serde_core",
845
+]
846
+
847
+[[package]]
848
+name = "sharded-slab"
849
+version = "0.1.7"
850
+source = "registry+https://github.com/rust-lang/crates.io-index"
851
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
852
+dependencies = [
853
+ "lazy_static",
854
+]
855
+
856
+[[package]]
857
+name = "shlex"
858
+version = "1.3.0"
859
+source = "registry+https://github.com/rust-lang/crates.io-index"
860
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
861
+
862
+[[package]]
863
+name = "slab"
864
+version = "0.4.11"
865
+source = "registry+https://github.com/rust-lang/crates.io-index"
866
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
867
+
868
+[[package]]
869
+name = "smallvec"
870
+version = "1.15.1"
871
+source = "registry+https://github.com/rust-lang/crates.io-index"
872
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
873
+
874
+[[package]]
875
+name = "strsim"
876
+version = "0.11.1"
877
+source = "registry+https://github.com/rust-lang/crates.io-index"
878
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
879
+
880
+[[package]]
881
+name = "syn"
882
+version = "2.0.114"
883
+source = "registry+https://github.com/rust-lang/crates.io-index"
884
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
885
+dependencies = [
886
+ "proc-macro2",
887
+ "quote",
888
+ "unicode-ident",
889
+]
890
+
891
+[[package]]
892
+name = "system-deps"
893
+version = "7.0.7"
894
+source = "registry+https://github.com/rust-lang/crates.io-index"
895
+checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
896
+dependencies = [
897
+ "cfg-expr",
898
+ "heck",
899
+ "pkg-config",
900
+ "toml",
901
+ "version-compare",
902
+]
903
+
904
+[[package]]
905
+name = "target-lexicon"
906
+version = "0.13.3"
907
+source = "registry+https://github.com/rust-lang/crates.io-index"
908
+checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
909
+
910
+[[package]]
911
+name = "thiserror"
912
+version = "2.0.18"
913
+source = "registry+https://github.com/rust-lang/crates.io-index"
914
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
915
+dependencies = [
916
+ "thiserror-impl",
917
+]
918
+
919
+[[package]]
920
+name = "thiserror-impl"
921
+version = "2.0.18"
922
+source = "registry+https://github.com/rust-lang/crates.io-index"
923
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
924
+dependencies = [
925
+ "proc-macro2",
926
+ "quote",
927
+ "syn",
928
+]
929
+
930
+[[package]]
931
+name = "thread_local"
932
+version = "1.1.9"
933
+source = "registry+https://github.com/rust-lang/crates.io-index"
934
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
935
+dependencies = [
936
+ "cfg-if",
937
+]
938
+
939
+[[package]]
940
+name = "toml"
941
+version = "0.9.11+spec-1.1.0"
942
+source = "registry+https://github.com/rust-lang/crates.io-index"
943
+checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
944
+dependencies = [
945
+ "indexmap",
946
+ "serde_core",
947
+ "serde_spanned",
948
+ "toml_datetime",
949
+ "toml_parser",
950
+ "toml_writer",
951
+ "winnow",
952
+]
953
+
954
+[[package]]
955
+name = "toml_datetime"
956
+version = "0.7.5+spec-1.1.0"
957
+source = "registry+https://github.com/rust-lang/crates.io-index"
958
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
959
+dependencies = [
960
+ "serde_core",
961
+]
962
+
963
+[[package]]
964
+name = "toml_edit"
965
+version = "0.23.10+spec-1.0.0"
966
+source = "registry+https://github.com/rust-lang/crates.io-index"
967
+checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
968
+dependencies = [
969
+ "indexmap",
970
+ "toml_datetime",
971
+ "toml_parser",
972
+ "winnow",
973
+]
974
+
975
+[[package]]
976
+name = "toml_parser"
977
+version = "1.0.6+spec-1.1.0"
978
+source = "registry+https://github.com/rust-lang/crates.io-index"
979
+checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
980
+dependencies = [
981
+ "winnow",
982
+]
983
+
984
+[[package]]
985
+name = "toml_writer"
986
+version = "1.0.6+spec-1.1.0"
987
+source = "registry+https://github.com/rust-lang/crates.io-index"
988
+checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
989
+
990
+[[package]]
991
+name = "tracing"
992
+version = "0.1.44"
993
+source = "registry+https://github.com/rust-lang/crates.io-index"
994
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
995
+dependencies = [
996
+ "pin-project-lite",
997
+ "tracing-attributes",
998
+ "tracing-core",
999
+]
1000
+
1001
+[[package]]
1002
+name = "tracing-attributes"
1003
+version = "0.1.31"
1004
+source = "registry+https://github.com/rust-lang/crates.io-index"
1005
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1006
+dependencies = [
1007
+ "proc-macro2",
1008
+ "quote",
1009
+ "syn",
1010
+]
1011
+
1012
+[[package]]
1013
+name = "tracing-core"
1014
+version = "0.1.36"
1015
+source = "registry+https://github.com/rust-lang/crates.io-index"
1016
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1017
+dependencies = [
1018
+ "once_cell",
1019
+ "valuable",
1020
+]
1021
+
1022
+[[package]]
1023
+name = "tracing-log"
1024
+version = "0.2.0"
1025
+source = "registry+https://github.com/rust-lang/crates.io-index"
1026
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1027
+dependencies = [
1028
+ "log",
1029
+ "once_cell",
1030
+ "tracing-core",
1031
+]
1032
+
1033
+[[package]]
1034
+name = "tracing-subscriber"
1035
+version = "0.3.22"
1036
+source = "registry+https://github.com/rust-lang/crates.io-index"
1037
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
1038
+dependencies = [
1039
+ "matchers",
1040
+ "nu-ansi-term",
1041
+ "once_cell",
1042
+ "regex-automata",
1043
+ "sharded-slab",
1044
+ "smallvec",
1045
+ "thread_local",
1046
+ "tracing",
1047
+ "tracing-core",
1048
+ "tracing-log",
1049
+]
1050
+
1051
+[[package]]
1052
+name = "unicode-ident"
1053
+version = "1.0.22"
1054
+source = "registry+https://github.com/rust-lang/crates.io-index"
1055
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
1056
+
1057
+[[package]]
1058
+name = "utf8parse"
1059
+version = "0.2.2"
1060
+source = "registry+https://github.com/rust-lang/crates.io-index"
1061
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1062
+
1063
+[[package]]
1064
+name = "valuable"
1065
+version = "0.1.1"
1066
+source = "registry+https://github.com/rust-lang/crates.io-index"
1067
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1068
+
1069
+[[package]]
1070
+name = "version-compare"
1071
+version = "0.2.1"
1072
+source = "registry+https://github.com/rust-lang/crates.io-index"
1073
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
1074
+
1075
+[[package]]
1076
+name = "wasi"
1077
+version = "0.11.1+wasi-snapshot-preview1"
1078
+source = "registry+https://github.com/rust-lang/crates.io-index"
1079
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1080
+
1081
+[[package]]
1082
+name = "wasm-bindgen"
1083
+version = "0.2.108"
1084
+source = "registry+https://github.com/rust-lang/crates.io-index"
1085
+checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
1086
+dependencies = [
1087
+ "cfg-if",
1088
+ "once_cell",
1089
+ "rustversion",
1090
+ "wasm-bindgen-macro",
1091
+ "wasm-bindgen-shared",
1092
+]
1093
+
1094
+[[package]]
1095
+name = "wasm-bindgen-macro"
1096
+version = "0.2.108"
1097
+source = "registry+https://github.com/rust-lang/crates.io-index"
1098
+checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
1099
+dependencies = [
1100
+ "quote",
1101
+ "wasm-bindgen-macro-support",
1102
+]
1103
+
1104
+[[package]]
1105
+name = "wasm-bindgen-macro-support"
1106
+version = "0.2.108"
1107
+source = "registry+https://github.com/rust-lang/crates.io-index"
1108
+checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
1109
+dependencies = [
1110
+ "bumpalo",
1111
+ "proc-macro2",
1112
+ "quote",
1113
+ "syn",
1114
+ "wasm-bindgen-shared",
1115
+]
1116
+
1117
+[[package]]
1118
+name = "wasm-bindgen-shared"
1119
+version = "0.2.108"
1120
+source = "registry+https://github.com/rust-lang/crates.io-index"
1121
+checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
1122
+dependencies = [
1123
+ "unicode-ident",
1124
+]
1125
+
1126
+[[package]]
1127
+name = "windows-core"
1128
+version = "0.62.2"
1129
+source = "registry+https://github.com/rust-lang/crates.io-index"
1130
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
1131
+dependencies = [
1132
+ "windows-implement",
1133
+ "windows-interface",
1134
+ "windows-link",
1135
+ "windows-result",
1136
+ "windows-strings",
1137
+]
1138
+
1139
+[[package]]
1140
+name = "windows-implement"
1141
+version = "0.60.2"
1142
+source = "registry+https://github.com/rust-lang/crates.io-index"
1143
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
1144
+dependencies = [
1145
+ "proc-macro2",
1146
+ "quote",
1147
+ "syn",
1148
+]
1149
+
1150
+[[package]]
1151
+name = "windows-interface"
1152
+version = "0.59.3"
1153
+source = "registry+https://github.com/rust-lang/crates.io-index"
1154
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
1155
+dependencies = [
1156
+ "proc-macro2",
1157
+ "quote",
1158
+ "syn",
1159
+]
1160
+
1161
+[[package]]
1162
+name = "windows-link"
1163
+version = "0.2.1"
1164
+source = "registry+https://github.com/rust-lang/crates.io-index"
1165
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1166
+
1167
+[[package]]
1168
+name = "windows-result"
1169
+version = "0.4.1"
1170
+source = "registry+https://github.com/rust-lang/crates.io-index"
1171
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
1172
+dependencies = [
1173
+ "windows-link",
1174
+]
1175
+
1176
+[[package]]
1177
+name = "windows-strings"
1178
+version = "0.5.1"
1179
+source = "registry+https://github.com/rust-lang/crates.io-index"
1180
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
1181
+dependencies = [
1182
+ "windows-link",
1183
+]
1184
+
1185
+[[package]]
1186
+name = "windows-sys"
1187
+version = "0.59.0"
1188
+source = "registry+https://github.com/rust-lang/crates.io-index"
1189
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1190
+dependencies = [
1191
+ "windows-targets",
1192
+]
1193
+
1194
+[[package]]
1195
+name = "windows-sys"
1196
+version = "0.61.2"
1197
+source = "registry+https://github.com/rust-lang/crates.io-index"
1198
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1199
+dependencies = [
1200
+ "windows-link",
1201
+]
1202
+
1203
+[[package]]
1204
+name = "windows-targets"
1205
+version = "0.52.6"
1206
+source = "registry+https://github.com/rust-lang/crates.io-index"
1207
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1208
+dependencies = [
1209
+ "windows_aarch64_gnullvm",
1210
+ "windows_aarch64_msvc",
1211
+ "windows_i686_gnu",
1212
+ "windows_i686_gnullvm",
1213
+ "windows_i686_msvc",
1214
+ "windows_x86_64_gnu",
1215
+ "windows_x86_64_gnullvm",
1216
+ "windows_x86_64_msvc",
1217
+]
1218
+
1219
+[[package]]
1220
+name = "windows_aarch64_gnullvm"
1221
+version = "0.52.6"
1222
+source = "registry+https://github.com/rust-lang/crates.io-index"
1223
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1224
+
1225
+[[package]]
1226
+name = "windows_aarch64_msvc"
1227
+version = "0.52.6"
1228
+source = "registry+https://github.com/rust-lang/crates.io-index"
1229
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1230
+
1231
+[[package]]
1232
+name = "windows_i686_gnu"
1233
+version = "0.52.6"
1234
+source = "registry+https://github.com/rust-lang/crates.io-index"
1235
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1236
+
1237
+[[package]]
1238
+name = "windows_i686_gnullvm"
1239
+version = "0.52.6"
1240
+source = "registry+https://github.com/rust-lang/crates.io-index"
1241
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1242
+
1243
+[[package]]
1244
+name = "windows_i686_msvc"
1245
+version = "0.52.6"
1246
+source = "registry+https://github.com/rust-lang/crates.io-index"
1247
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1248
+
1249
+[[package]]
1250
+name = "windows_x86_64_gnu"
1251
+version = "0.52.6"
1252
+source = "registry+https://github.com/rust-lang/crates.io-index"
1253
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1254
+
1255
+[[package]]
1256
+name = "windows_x86_64_gnullvm"
1257
+version = "0.52.6"
1258
+source = "registry+https://github.com/rust-lang/crates.io-index"
1259
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1260
+
1261
+[[package]]
1262
+name = "windows_x86_64_msvc"
1263
+version = "0.52.6"
1264
+source = "registry+https://github.com/rust-lang/crates.io-index"
1265
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1266
+
1267
+[[package]]
1268
+name = "winnow"
1269
+version = "0.7.14"
1270
+source = "registry+https://github.com/rust-lang/crates.io-index"
1271
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
1272
+dependencies = [
1273
+ "memchr",
1274
+]
1275
+
1276
+[[package]]
1277
+name = "x11rb"
1278
+version = "0.13.2"
1279
+source = "registry+https://github.com/rust-lang/crates.io-index"
1280
+checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
1281
+dependencies = [
1282
+ "as-raw-xcb-connection",
1283
+ "gethostname",
1284
+ "libc",
1285
+ "rustix",
1286
+ "x11rb-protocol",
1287
+ "xcursor",
1288
+]
1289
+
1290
+[[package]]
1291
+name = "x11rb-protocol"
1292
+version = "0.13.2"
1293
+source = "registry+https://github.com/rust-lang/crates.io-index"
1294
+checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
1295
+
1296
+[[package]]
1297
+name = "xcursor"
1298
+version = "0.3.10"
1299
+source = "registry+https://github.com/rust-lang/crates.io-index"
1300
+checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
1301
+
1302
+[[package]]
1303
+name = "zmij"
1304
+version = "1.0.16"
1305
+source = "registry+https://github.com/rust-lang/crates.io-index"
1306
+checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
Cargo.tomladded
@@ -0,0 +1,45 @@
1
+[workspace]
2
+resolver = "2"
3
+members = [
4
+    "garfield",
5
+    "garfieldctl",
6
+    "garfield-ipc",
7
+]
8
+
9
+[workspace.package]
10
+version = "0.1.0"
11
+edition = "2024"
12
+authors = ["mfwolffe"]
13
+license = "MIT"
14
+repository = "https://github.com/mfwolffe/gardesk"
15
+
16
+[workspace.dependencies]
17
+# UI toolkit
18
+gartk-core = { path = "../gartk/gartk-core" }
19
+gartk-x11 = { path = "../gartk/gartk-x11" }
20
+gartk-render = { path = "../gartk/gartk-render" }
21
+
22
+# X11
23
+x11rb = { version = "0.13", features = ["allow-unsafe-code"] }
24
+
25
+# Logging
26
+tracing = "0.1"
27
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
28
+
29
+# Error handling
30
+thiserror = "2.0"
31
+anyhow = "1.0"
32
+
33
+# Serialization
34
+serde = { version = "1.0", features = ["derive"] }
35
+serde_json = "1.0"
36
+
37
+# CLI
38
+clap = { version = "4.5", features = ["derive"] }
39
+
40
+# File system
41
+dirs = "6.0"
42
+chrono = "0.4"
43
+
44
+# IPC types (shared between crates)
45
+garfield-ipc = { path = "garfield-ipc" }
garfield-ipc/Cargo.tomladded
@@ -0,0 +1,11 @@
1
+[package]
2
+name = "garfield-ipc"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+
8
+[dependencies]
9
+serde.workspace = true
10
+serde_json.workspace = true
11
+thiserror.workspace = true
garfield-ipc/src/lib.rsadded
@@ -0,0 +1,113 @@
1
+//! Shared IPC protocol types for garfield.
2
+//!
3
+//! This crate defines the request/response types used for communication
4
+//! between garfield and garfieldctl, as well as with other gar components.
5
+
6
+use serde::{Deserialize, Serialize};
7
+
8
+/// IPC request from client to garfield.
9
+#[derive(Debug, Clone, Serialize, Deserialize)]
10
+#[serde(tag = "command", rename_all = "snake_case")]
11
+pub enum Command {
12
+    /// Open a directory in garfield.
13
+    Open {
14
+        path: String,
15
+        /// Open in new tab instead of current view.
16
+        #[serde(default)]
17
+        new_tab: bool,
18
+    },
19
+    /// Query current directory.
20
+    CurrentDir,
21
+    /// Query garfield status.
22
+    Status,
23
+    /// Request file picker dialog.
24
+    FilePicker {
25
+        /// Dialog title.
26
+        title: Option<String>,
27
+        /// Starting directory.
28
+        start_dir: Option<String>,
29
+        /// File type filters (e.g., "*.rs", "*.txt").
30
+        filters: Vec<String>,
31
+        /// Allow selecting multiple files.
32
+        #[serde(default)]
33
+        multiple: bool,
34
+    },
35
+    /// Request folder picker dialog.
36
+    FolderPicker {
37
+        /// Dialog title.
38
+        title: Option<String>,
39
+        /// Starting directory.
40
+        start_dir: Option<String>,
41
+    },
42
+    /// Quit garfield.
43
+    Quit,
44
+}
45
+
46
+/// IPC response from garfield to client.
47
+#[derive(Debug, Clone, Serialize, Deserialize)]
48
+pub struct Response {
49
+    /// Whether the command succeeded.
50
+    pub success: bool,
51
+    /// Response data (command-specific).
52
+    #[serde(skip_serializing_if = "Option::is_none")]
53
+    pub data: Option<serde_json::Value>,
54
+    /// Error message if failed.
55
+    #[serde(skip_serializing_if = "Option::is_none")]
56
+    pub error: Option<String>,
57
+}
58
+
59
+impl Response {
60
+    /// Create a successful response with no data.
61
+    pub fn ok() -> Self {
62
+        Self {
63
+            success: true,
64
+            data: None,
65
+            error: None,
66
+        }
67
+    }
68
+
69
+    /// Create a successful response with data.
70
+    pub fn ok_with_data(data: impl Serialize) -> Self {
71
+        Self {
72
+            success: true,
73
+            data: serde_json::to_value(data).ok(),
74
+            error: None,
75
+        }
76
+    }
77
+
78
+    /// Create an error response.
79
+    pub fn err(message: impl Into<String>) -> Self {
80
+        Self {
81
+            success: false,
82
+            data: None,
83
+            error: Some(message.into()),
84
+        }
85
+    }
86
+}
87
+
88
+/// Status information returned by the Status command.
89
+#[derive(Debug, Clone, Serialize, Deserialize)]
90
+pub struct StatusInfo {
91
+    /// Current working directory.
92
+    pub current_dir: String,
93
+    /// Number of open tabs.
94
+    pub tab_count: usize,
95
+    /// Number of selected files.
96
+    pub selection_count: usize,
97
+}
98
+
99
+/// Result of a file picker dialog.
100
+#[derive(Debug, Clone, Serialize, Deserialize)]
101
+pub struct PickerResult {
102
+    /// Selected file paths (empty if cancelled).
103
+    pub paths: Vec<String>,
104
+    /// Whether the dialog was cancelled.
105
+    pub cancelled: bool,
106
+}
107
+
108
+/// Default socket path for garfield IPC.
109
+pub fn socket_path() -> std::path::PathBuf {
110
+    let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
111
+        .unwrap_or_else(|_| "/tmp".to_string());
112
+    std::path::PathBuf::from(runtime_dir).join("garfield.sock")
113
+}
garfield/Cargo.tomladded
@@ -0,0 +1,38 @@
1
+[package]
2
+name = "garfield"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+
8
+[[bin]]
9
+name = "garfield"
10
+path = "src/main.rs"
11
+
12
+[dependencies]
13
+# UI toolkit
14
+gartk-core.workspace = true
15
+gartk-x11.workspace = true
16
+gartk-render.workspace = true
17
+
18
+# X11
19
+x11rb.workspace = true
20
+
21
+# Logging
22
+tracing.workspace = true
23
+tracing-subscriber.workspace = true
24
+
25
+# Error handling
26
+thiserror.workspace = true
27
+anyhow.workspace = true
28
+
29
+# Serialization
30
+serde.workspace = true
31
+serde_json.workspace = true
32
+
33
+# File system
34
+dirs.workspace = true
35
+chrono.workspace = true
36
+
37
+# IPC
38
+garfield-ipc.workspace = true
garfield/src/app.rsadded
@@ -0,0 +1,284 @@
1
+//! Application state and event loop.
2
+
3
+use garfield::core::{read_directory, sort_entries, SortDirection, SortOrder};
4
+use garfield::ui::ListView;
5
+use anyhow::Result;
6
+use gartk_core::{InputEvent, Key, Rect, Theme};
7
+use gartk_render::{Renderer, Surface, TextStyle};
8
+use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
9
+use std::path::PathBuf;
10
+use x11rb::protocol::xproto::{ConnectionExt, ImageFormat};
11
+
12
+/// Application state.
13
+pub struct App {
14
+    /// X11 window.
15
+    window: Window,
16
+    /// Renderer.
17
+    renderer: Renderer,
18
+    /// Graphics context for blitting.
19
+    gc: u32,
20
+    /// Current directory path.
21
+    current_dir: PathBuf,
22
+    /// List view component.
23
+    list_view: ListView,
24
+    /// Sort order.
25
+    sort_order: SortOrder,
26
+    /// Sort direction.
27
+    sort_direction: SortDirection,
28
+    /// Whether the app should quit.
29
+    should_quit: bool,
30
+}
31
+
32
+impl App {
33
+    /// Create a new application.
34
+    pub fn new(start_dir: Option<PathBuf>) -> Result<Self> {
35
+        // Connect to X11
36
+        let conn = Connection::connect(None)?;
37
+
38
+        // Get primary monitor for window sizing
39
+        let monitor = gartk_x11::primary_monitor(&conn)?;
40
+
41
+        // Calculate window size (70% of screen)
42
+        let width = (monitor.rect.width as f64 * 0.7) as u32;
43
+        let height = (monitor.rect.height as f64 * 0.7) as u32;
44
+        let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2;
45
+        let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2;
46
+
47
+        // Create window
48
+        let window = Window::create(
49
+            conn.clone(),
50
+            WindowConfig::default()
51
+                .title("garfield")
52
+                .class("garfield")
53
+                .position(x, y)
54
+                .size(width, height)
55
+                .transparent(false),
56
+        )?;
57
+
58
+        window.focus()?;
59
+
60
+        // Create graphics context for blitting
61
+        let gc = conn.generate_id()?;
62
+        conn.inner().create_gc(gc, window.id(), &Default::default())?;
63
+        conn.flush()?;
64
+
65
+        // Create renderer with dark theme
66
+        let theme = Theme::dark();
67
+        let renderer = Renderer::with_theme(width, height, theme)?;
68
+
69
+        // Determine starting directory
70
+        let current_dir = start_dir
71
+            .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")));
72
+
73
+        // Create list view
74
+        let list_bounds = Rect::new(0, 40, width, height - 40); // Leave space for path bar
75
+        let mut list_view = ListView::new(list_bounds);
76
+
77
+        // Load initial directory
78
+        let mut entries = read_directory(&current_dir).unwrap_or_default();
79
+        sort_entries(&mut entries, SortOrder::Name, SortDirection::Ascending);
80
+        list_view.set_entries(entries);
81
+
82
+        Ok(Self {
83
+            window,
84
+            renderer,
85
+            gc,
86
+            current_dir,
87
+            list_view,
88
+            sort_order: SortOrder::Name,
89
+            sort_direction: SortDirection::Ascending,
90
+            should_quit: false,
91
+        })
92
+    }
93
+
94
+    /// Run the application event loop.
95
+    pub fn run(&mut self) -> Result<()> {
96
+        let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?;
97
+
98
+        // Initial render
99
+        self.render()?;
100
+
101
+        event_loop.run(|ev, event| {
102
+            match event {
103
+                InputEvent::Key(key_event) if key_event.pressed => {
104
+                    self.handle_key(&key_event.key);
105
+                    ev.request_redraw();
106
+                }
107
+                InputEvent::Resize { width, height } => {
108
+                    let _ = self.renderer.resize(width, height);
109
+                    self.list_view.set_bounds(Rect::new(0, 40, width, height - 40));
110
+                    ev.request_redraw();
111
+                }
112
+                InputEvent::Expose => {
113
+                    ev.request_redraw();
114
+                }
115
+                InputEvent::CloseRequested => {
116
+                    self.should_quit = true;
117
+                }
118
+                _ => {}
119
+            }
120
+
121
+            if ev.needs_redraw() {
122
+                let _ = self.render();
123
+                ev.redraw_done();
124
+            }
125
+
126
+            Ok(!self.should_quit)
127
+        })?;
128
+
129
+        Ok(())
130
+    }
131
+
132
+    /// Handle a key press.
133
+    fn handle_key(&mut self, key: &Key) {
134
+        match key {
135
+            Key::Escape | Key::Char('q') => {
136
+                self.should_quit = true;
137
+            }
138
+            Key::Up | Key::Char('k') => {
139
+                self.list_view.select_prev();
140
+            }
141
+            Key::Down | Key::Char('j') => {
142
+                self.list_view.select_next();
143
+            }
144
+            Key::Home | Key::Char('g') => {
145
+                self.list_view.select_first();
146
+            }
147
+            Key::End | Key::Char('G') => {
148
+                self.list_view.select_last();
149
+            }
150
+            Key::PageUp => {
151
+                self.list_view.page_up();
152
+            }
153
+            Key::PageDown => {
154
+                self.list_view.page_down();
155
+            }
156
+            Key::Return | Key::Right | Key::Char('l') => {
157
+                self.enter_selected();
158
+            }
159
+            Key::Backspace | Key::Left | Key::Char('h') => {
160
+                self.go_up();
161
+            }
162
+            Key::Char('H') => {
163
+                self.list_view.toggle_hidden();
164
+            }
165
+            Key::Char('~') => {
166
+                self.navigate_to(dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")));
167
+            }
168
+            Key::Char('/') => {
169
+                self.navigate_to(PathBuf::from("/"));
170
+            }
171
+            Key::Char('r') => {
172
+                self.refresh();
173
+            }
174
+            _ => {}
175
+        }
176
+    }
177
+
178
+    /// Enter the selected entry (open directory).
179
+    fn enter_selected(&mut self) {
180
+        if let Some(entry) = self.list_view.selected_entry().cloned() {
181
+            if entry.is_dir() {
182
+                self.navigate_to(entry.path);
183
+            }
184
+            // TODO: Open files with default application
185
+        }
186
+    }
187
+
188
+    /// Navigate to parent directory.
189
+    fn go_up(&mut self) {
190
+        if let Some(parent) = self.current_dir.parent() {
191
+            self.navigate_to(parent.to_path_buf());
192
+        }
193
+    }
194
+
195
+    /// Navigate to a new directory.
196
+    fn navigate_to(&mut self, path: PathBuf) {
197
+        if path.is_dir() {
198
+            self.current_dir = path;
199
+            self.refresh();
200
+        }
201
+    }
202
+
203
+    /// Refresh the current directory listing.
204
+    fn refresh(&mut self) {
205
+        let mut entries = read_directory(&self.current_dir).unwrap_or_default();
206
+        sort_entries(&mut entries, self.sort_order, self.sort_direction);
207
+        self.list_view.set_entries(entries);
208
+    }
209
+
210
+    /// Render the application.
211
+    fn render(&mut self) -> Result<()> {
212
+        let theme = self.renderer.theme().clone();
213
+        let size = self.renderer.size();
214
+
215
+        // Clear background
216
+        self.renderer.clear()?;
217
+
218
+        // Draw path bar
219
+        let path_rect = Rect::new(0, 0, size.width, 40);
220
+        self.renderer.fill_rect(path_rect, theme.item_background)?;
221
+
222
+        let path_style = TextStyle::new()
223
+            .font_family(&theme.font_family)
224
+            .font_size(theme.font_size + 2.0)
225
+            .color(theme.item_foreground);
226
+
227
+        let path_text = self.current_dir.to_string_lossy();
228
+        let text_rect = Rect::new(16, 0, size.width - 32, 40);
229
+        self.renderer.text_in_rect(&path_text, text_rect, &path_style)?;
230
+
231
+        // Draw separator line
232
+        self.renderer.line(0.0, 40.0, size.width as f64, 40.0, theme.border, 1.0)?;
233
+
234
+        // Draw list view
235
+        self.list_view.render(&self.renderer)?;
236
+
237
+        // Flush and copy to window
238
+        self.renderer.flush();
239
+        self.blit_surface()?;
240
+
241
+        Ok(())
242
+    }
243
+
244
+    /// Blit the rendered surface to the window.
245
+    fn blit_surface(&mut self) -> Result<()> {
246
+        let size = self.renderer.size();
247
+        let conn = self.window.connection();
248
+
249
+        // Get the surface data by creating a temp surface and copying
250
+        let ctx = self.renderer.context()?;
251
+        ctx.target().flush();
252
+
253
+        let mut temp_surface = Surface::new(size.width, size.height)?;
254
+        let temp_ctx = temp_surface.context()?;
255
+        temp_ctx.set_source_surface(self.renderer.surface().cairo_surface(), 0.0, 0.0)?;
256
+        temp_ctx.paint()?;
257
+        drop(temp_ctx);
258
+
259
+        let data = temp_surface.data()?;
260
+
261
+        conn.inner().put_image(
262
+            ImageFormat::Z_PIXMAP,
263
+            self.window.id(),
264
+            self.gc,
265
+            size.width as u16,
266
+            size.height as u16,
267
+            0,
268
+            0,
269
+            0,
270
+            self.window.depth(),
271
+            &data,
272
+        )?;
273
+
274
+        conn.flush()?;
275
+
276
+        Ok(())
277
+    }
278
+}
279
+
280
+impl Drop for App {
281
+    fn drop(&mut self) {
282
+        let _ = self.window.connection().inner().free_gc(self.gc);
283
+    }
284
+}
garfield/src/core/entry.rsadded
@@ -0,0 +1,214 @@
1
+//! File entry type with metadata.
2
+
3
+use std::cmp::Ordering;
4
+use std::fs::{self, Metadata};
5
+use std::path::{Path, PathBuf};
6
+use std::time::SystemTime;
7
+
8
+/// Type of file system entry.
9
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10
+pub enum EntryType {
11
+    /// Regular file.
12
+    File,
13
+    /// Directory.
14
+    Directory,
15
+    /// Symbolic link.
16
+    Symlink,
17
+    /// Other (device, socket, etc.).
18
+    Other,
19
+}
20
+
21
+impl EntryType {
22
+    /// Determine entry type from metadata.
23
+    pub fn from_metadata(meta: &Metadata) -> Self {
24
+        if meta.is_dir() {
25
+            EntryType::Directory
26
+        } else if meta.is_file() {
27
+            EntryType::File
28
+        } else if meta.is_symlink() {
29
+            EntryType::Symlink
30
+        } else {
31
+            EntryType::Other
32
+        }
33
+    }
34
+}
35
+
36
+/// A file system entry with metadata.
37
+#[derive(Debug, Clone)]
38
+pub struct FileEntry {
39
+    /// File name (not full path).
40
+    pub name: String,
41
+    /// Full path.
42
+    pub path: PathBuf,
43
+    /// Entry type.
44
+    pub entry_type: EntryType,
45
+    /// File size in bytes (0 for directories).
46
+    pub size: u64,
47
+    /// Last modified time.
48
+    pub modified: Option<SystemTime>,
49
+    /// Whether this is a hidden file (starts with '.').
50
+    pub hidden: bool,
51
+    /// Whether this is a symlink.
52
+    pub is_symlink: bool,
53
+    /// Symlink target (if symlink).
54
+    pub symlink_target: Option<PathBuf>,
55
+}
56
+
57
+impl FileEntry {
58
+    /// Create a FileEntry from a path.
59
+    pub fn from_path(path: impl AsRef<Path>) -> std::io::Result<Self> {
60
+        let path = path.as_ref();
61
+        let name = path
62
+            .file_name()
63
+            .map(|s| s.to_string_lossy().to_string())
64
+            .unwrap_or_else(|| path.to_string_lossy().to_string());
65
+
66
+        // Get symlink metadata first (doesn't follow symlinks)
67
+        let symlink_meta = fs::symlink_metadata(path)?;
68
+        let is_symlink = symlink_meta.is_symlink();
69
+
70
+        // For symlinks, also get the target metadata
71
+        let (meta, symlink_target) = if is_symlink {
72
+            let target = fs::read_link(path).ok();
73
+            // Try to get target metadata, fall back to symlink metadata
74
+            let target_meta = fs::metadata(path).unwrap_or(symlink_meta.clone());
75
+            (target_meta, target)
76
+        } else {
77
+            (symlink_meta, None)
78
+        };
79
+
80
+        let entry_type = if is_symlink {
81
+            EntryType::Symlink
82
+        } else {
83
+            EntryType::from_metadata(&meta)
84
+        };
85
+
86
+        let size = if meta.is_file() { meta.len() } else { 0 };
87
+        let modified = meta.modified().ok();
88
+        let hidden = name.starts_with('.');
89
+
90
+        Ok(Self {
91
+            name,
92
+            path: path.to_path_buf(),
93
+            entry_type,
94
+            size,
95
+            modified,
96
+            hidden,
97
+            is_symlink,
98
+            symlink_target,
99
+        })
100
+    }
101
+
102
+    /// Check if this is a directory.
103
+    pub fn is_dir(&self) -> bool {
104
+        self.entry_type == EntryType::Directory
105
+    }
106
+
107
+    /// Check if this is a file.
108
+    pub fn is_file(&self) -> bool {
109
+        self.entry_type == EntryType::File
110
+    }
111
+
112
+    /// Get file extension (lowercase).
113
+    pub fn extension(&self) -> Option<String> {
114
+        self.path
115
+            .extension()
116
+            .map(|ext| ext.to_string_lossy().to_lowercase())
117
+    }
118
+
119
+    /// Format size for display.
120
+    pub fn format_size(&self) -> String {
121
+        if self.is_dir() {
122
+            return String::new();
123
+        }
124
+        format_bytes(self.size)
125
+    }
126
+
127
+    /// Format modified time for display.
128
+    pub fn format_modified(&self) -> String {
129
+        match self.modified {
130
+            Some(time) => {
131
+                let datetime: chrono::DateTime<chrono::Local> = time.into();
132
+                datetime.format("%Y-%m-%d %H:%M").to_string()
133
+            }
134
+            None => String::new(),
135
+        }
136
+    }
137
+}
138
+
139
+/// Sort order for file entries.
140
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141
+pub enum SortOrder {
142
+    #[default]
143
+    Name,
144
+    Size,
145
+    Modified,
146
+    Type,
147
+}
148
+
149
+/// Sort direction.
150
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151
+pub enum SortDirection {
152
+    #[default]
153
+    Ascending,
154
+    Descending,
155
+}
156
+
157
+/// Read directory entries.
158
+pub fn read_directory(path: impl AsRef<Path>) -> std::io::Result<Vec<FileEntry>> {
159
+    let path = path.as_ref();
160
+    let mut entries = Vec::new();
161
+
162
+    for entry in fs::read_dir(path)? {
163
+        let entry = entry?;
164
+        if let Ok(file_entry) = FileEntry::from_path(entry.path()) {
165
+            entries.push(file_entry);
166
+        }
167
+    }
168
+
169
+    Ok(entries)
170
+}
171
+
172
+/// Sort entries with directories first.
173
+pub fn sort_entries(entries: &mut [FileEntry], order: SortOrder, direction: SortDirection) {
174
+    entries.sort_by(|a, b| {
175
+        // Directories always come first
176
+        match (a.is_dir(), b.is_dir()) {
177
+            (true, false) => return Ordering::Less,
178
+            (false, true) => return Ordering::Greater,
179
+            _ => {}
180
+        }
181
+
182
+        let cmp = match order {
183
+            SortOrder::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
184
+            SortOrder::Size => a.size.cmp(&b.size),
185
+            SortOrder::Modified => a.modified.cmp(&b.modified),
186
+            SortOrder::Type => a.extension().cmp(&b.extension()),
187
+        };
188
+
189
+        match direction {
190
+            SortDirection::Ascending => cmp,
191
+            SortDirection::Descending => cmp.reverse(),
192
+        }
193
+    });
194
+}
195
+
196
+/// Format bytes as human-readable string.
197
+fn format_bytes(bytes: u64) -> String {
198
+    const KB: u64 = 1024;
199
+    const MB: u64 = KB * 1024;
200
+    const GB: u64 = MB * 1024;
201
+    const TB: u64 = GB * 1024;
202
+
203
+    if bytes >= TB {
204
+        format!("{:.1} TB", bytes as f64 / TB as f64)
205
+    } else if bytes >= GB {
206
+        format!("{:.1} GB", bytes as f64 / GB as f64)
207
+    } else if bytes >= MB {
208
+        format!("{:.1} MB", bytes as f64 / MB as f64)
209
+    } else if bytes >= KB {
210
+        format!("{:.1} KB", bytes as f64 / KB as f64)
211
+    } else {
212
+        format!("{} B", bytes)
213
+    }
214
+}
garfield/src/core/mod.rsadded
@@ -0,0 +1,7 @@
1
+//! Core file system operations.
2
+
3
+pub mod entry;
4
+
5
+pub use entry::{
6
+    read_directory, sort_entries, EntryType, FileEntry, SortDirection, SortOrder,
7
+};
garfield/src/lib.rsadded
@@ -0,0 +1,8 @@
1
+//! garfield - gar file explorer.
2
+//!
3
+//! A file manager for the gardesk suite, built with gartk.
4
+
5
+pub mod core;
6
+pub mod ui;
7
+
8
+pub use crate::core::entry::FileEntry;
garfield/src/main.rsadded
@@ -0,0 +1,32 @@
1
+//! garfield - gar file explorer.
2
+
3
+mod app;
4
+
5
+use anyhow::Result;
6
+use std::path::PathBuf;
7
+use tracing_subscriber::{fmt, EnvFilter};
8
+
9
+fn main() -> Result<()> {
10
+    // Initialize logging
11
+    let filter = EnvFilter::try_from_default_env()
12
+        .unwrap_or_else(|_| EnvFilter::new("info"));
13
+
14
+    fmt()
15
+        .with_env_filter(filter)
16
+        .with_target(false)
17
+        .init();
18
+
19
+    tracing::info!("Starting garfield");
20
+
21
+    // Parse command line arguments (simple for now)
22
+    let start_dir = std::env::args()
23
+        .nth(1)
24
+        .map(PathBuf::from);
25
+
26
+    // Create and run app
27
+    let mut app = app::App::new(start_dir)?;
28
+    app.run()?;
29
+
30
+    tracing::info!("garfield exiting");
31
+    Ok(())
32
+}
garfield/src/ui/list_view.rsadded
@@ -0,0 +1,264 @@
1
+//! List view component for displaying directory contents.
2
+
3
+use crate::core::{EntryType, FileEntry};
4
+use gartk_core::{Color, Rect};
5
+use gartk_render::{Renderer, TextStyle};
6
+
7
+/// Height of each row in the list view.
8
+pub const ROW_HEIGHT: u32 = 28;
9
+
10
+/// List view for displaying file entries.
11
+pub struct ListView {
12
+    /// Entries to display.
13
+    entries: Vec<FileEntry>,
14
+    /// Currently selected index.
15
+    selected: usize,
16
+    /// Scroll offset (first visible row).
17
+    scroll_offset: usize,
18
+    /// View bounds.
19
+    bounds: Rect,
20
+    /// Show hidden files.
21
+    show_hidden: bool,
22
+}
23
+
24
+impl ListView {
25
+    /// Create a new list view.
26
+    pub fn new(bounds: Rect) -> Self {
27
+        Self {
28
+            entries: Vec::new(),
29
+            selected: 0,
30
+            scroll_offset: 0,
31
+            bounds,
32
+            show_hidden: false,
33
+        }
34
+    }
35
+
36
+    /// Set the entries to display.
37
+    pub fn set_entries(&mut self, entries: Vec<FileEntry>) {
38
+        self.entries = entries;
39
+        self.selected = 0;
40
+        self.scroll_offset = 0;
41
+    }
42
+
43
+    /// Get visible entries (respecting hidden filter).
44
+    pub fn visible_entries(&self) -> Vec<&FileEntry> {
45
+        self.entries
46
+            .iter()
47
+            .filter(|e| self.show_hidden || !e.hidden)
48
+            .collect()
49
+    }
50
+
51
+    /// Get the currently selected entry.
52
+    pub fn selected_entry(&self) -> Option<&FileEntry> {
53
+        let visible = self.visible_entries();
54
+        visible.get(self.selected).copied()
55
+    }
56
+
57
+    /// Get the number of visible rows that fit in the view.
58
+    fn visible_rows(&self) -> usize {
59
+        (self.bounds.height / ROW_HEIGHT).max(1) as usize
60
+    }
61
+
62
+    /// Toggle hidden files visibility.
63
+    pub fn toggle_hidden(&mut self) {
64
+        self.show_hidden = !self.show_hidden;
65
+        // Clamp selection
66
+        let visible_count = self.visible_entries().len();
67
+        if self.selected >= visible_count && visible_count > 0 {
68
+            self.selected = visible_count - 1;
69
+        }
70
+    }
71
+
72
+    /// Move selection up.
73
+    pub fn select_prev(&mut self) {
74
+        if self.selected > 0 {
75
+            self.selected -= 1;
76
+            // Scroll if needed
77
+            if self.selected < self.scroll_offset {
78
+                self.scroll_offset = self.selected;
79
+            }
80
+        }
81
+    }
82
+
83
+    /// Move selection down.
84
+    pub fn select_next(&mut self) {
85
+        let visible_count = self.visible_entries().len();
86
+        if self.selected + 1 < visible_count {
87
+            self.selected += 1;
88
+            // Scroll if needed
89
+            let visible_rows = self.visible_rows();
90
+            if self.selected >= self.scroll_offset + visible_rows {
91
+                self.scroll_offset = self.selected - visible_rows + 1;
92
+            }
93
+        }
94
+    }
95
+
96
+    /// Jump to first entry.
97
+    pub fn select_first(&mut self) {
98
+        self.selected = 0;
99
+        self.scroll_offset = 0;
100
+    }
101
+
102
+    /// Jump to last entry.
103
+    pub fn select_last(&mut self) {
104
+        let visible_count = self.visible_entries().len();
105
+        if visible_count > 0 {
106
+            self.selected = visible_count - 1;
107
+            let visible_rows = self.visible_rows();
108
+            if self.selected >= visible_rows {
109
+                self.scroll_offset = self.selected - visible_rows + 1;
110
+            }
111
+        }
112
+    }
113
+
114
+    /// Page up.
115
+    pub fn page_up(&mut self) {
116
+        let page_size = self.visible_rows();
117
+        if self.selected >= page_size {
118
+            self.selected -= page_size;
119
+        } else {
120
+            self.selected = 0;
121
+        }
122
+        if self.selected < self.scroll_offset {
123
+            self.scroll_offset = self.selected;
124
+        }
125
+    }
126
+
127
+    /// Page down.
128
+    pub fn page_down(&mut self) {
129
+        let visible_count = self.visible_entries().len();
130
+        let page_size = self.visible_rows();
131
+        self.selected = (self.selected + page_size).min(visible_count.saturating_sub(1));
132
+        if self.selected >= self.scroll_offset + page_size {
133
+            self.scroll_offset = self.selected - page_size + 1;
134
+        }
135
+    }
136
+
137
+    /// Update bounds.
138
+    pub fn set_bounds(&mut self, bounds: Rect) {
139
+        self.bounds = bounds;
140
+    }
141
+
142
+    /// Render the list view.
143
+    pub fn render(&self, renderer: &Renderer) -> anyhow::Result<()> {
144
+        let theme = renderer.theme();
145
+        let visible = self.visible_entries();
146
+        let visible_rows = self.visible_rows();
147
+
148
+        // Calculate column widths
149
+        let name_width = (self.bounds.width as f64 * 0.5) as u32;
150
+        let size_width = 100;
151
+        let date_width = self.bounds.width.saturating_sub(name_width + size_width + 32);
152
+
153
+        // Draw header
154
+        let header_rect = Rect::new(
155
+            self.bounds.x,
156
+            self.bounds.y,
157
+            self.bounds.width,
158
+            ROW_HEIGHT,
159
+        );
160
+        renderer.fill_rect(header_rect, theme.item_background.darken(0.1))?;
161
+
162
+        let header_style = TextStyle::new()
163
+            .font_family(&theme.font_family)
164
+            .font_size(theme.font_size)
165
+            .color(theme.item_foreground.with_alpha(0.7));
166
+
167
+        // Header labels
168
+        let name_rect = Rect::new(header_rect.x + 8, header_rect.y, name_width, ROW_HEIGHT);
169
+        renderer.text_in_rect("Name", name_rect, &header_style)?;
170
+
171
+        let size_rect = Rect::new(
172
+            header_rect.x + name_width as i32 + 16,
173
+            header_rect.y,
174
+            size_width,
175
+            ROW_HEIGHT,
176
+        );
177
+        renderer.text_in_rect("Size", size_rect, &header_style)?;
178
+
179
+        let date_rect = Rect::new(
180
+            header_rect.x + name_width as i32 + size_width as i32 + 24,
181
+            header_rect.y,
182
+            date_width,
183
+            ROW_HEIGHT,
184
+        );
185
+        renderer.text_in_rect("Modified", date_rect, &header_style)?;
186
+
187
+        // Draw entries
188
+        for (i, entry) in visible
189
+            .iter()
190
+            .skip(self.scroll_offset)
191
+            .take(visible_rows)
192
+            .enumerate()
193
+        {
194
+            let y = self.bounds.y + ((i + 1) as i32 * ROW_HEIGHT as i32);
195
+            let row_rect = Rect::new(self.bounds.x, y, self.bounds.width, ROW_HEIGHT);
196
+
197
+            let actual_index = self.scroll_offset + i;
198
+            let is_selected = actual_index == self.selected;
199
+
200
+            // Row background
201
+            if is_selected {
202
+                renderer.fill_rounded_rect(row_rect, 4.0, theme.item_selected_background)?;
203
+            } else if i % 2 == 1 {
204
+                renderer.fill_rect(row_rect, theme.item_background.with_alpha(0.3))?;
205
+            }
206
+
207
+            // Determine colors
208
+            let text_color = if is_selected {
209
+                theme.selection_foreground
210
+            } else if entry.hidden {
211
+                theme.item_foreground.with_alpha(0.5)
212
+            } else {
213
+                theme.item_foreground
214
+            };
215
+
216
+            let name_color = match entry.entry_type {
217
+                EntryType::Directory => Color::from_hex("#5c9fd8").unwrap_or(text_color),
218
+                EntryType::Symlink => Color::from_hex("#c678dd").unwrap_or(text_color),
219
+                _ => text_color,
220
+            };
221
+
222
+            let text_style = TextStyle::new()
223
+                .font_family(&theme.font_family)
224
+                .font_size(theme.font_size)
225
+                .color(text_color);
226
+
227
+            let name_style = TextStyle::new()
228
+                .font_family(&theme.font_family)
229
+                .font_size(theme.font_size)
230
+                .color(if is_selected { theme.selection_foreground } else { name_color });
231
+
232
+            // Name with icon prefix
233
+            let icon = match entry.entry_type {
234
+                EntryType::Directory => "\u{1F4C1} ", // folder emoji
235
+                EntryType::Symlink => "\u{1F517} ",   // link emoji
236
+                _ => "\u{1F4C4} ",                     // file emoji
237
+            };
238
+            let display_name = format!("{}{}", icon, entry.name);
239
+
240
+            let name_rect = Rect::new(row_rect.x + 8, row_rect.y, name_width, ROW_HEIGHT);
241
+            renderer.text_in_rect(&display_name, name_rect, &name_style)?;
242
+
243
+            // Size
244
+            let size_rect = Rect::new(
245
+                row_rect.x + name_width as i32 + 16,
246
+                row_rect.y,
247
+                size_width,
248
+                ROW_HEIGHT,
249
+            );
250
+            renderer.text_in_rect(&entry.format_size(), size_rect, &text_style)?;
251
+
252
+            // Modified date
253
+            let date_rect = Rect::new(
254
+                row_rect.x + name_width as i32 + size_width as i32 + 24,
255
+                row_rect.y,
256
+                date_width,
257
+                ROW_HEIGHT,
258
+            );
259
+            renderer.text_in_rect(&entry.format_modified(), date_rect, &text_style)?;
260
+        }
261
+
262
+        Ok(())
263
+    }
264
+}
garfield/src/ui/mod.rsadded
@@ -0,0 +1,5 @@
1
+//! UI components for garfield.
2
+
3
+pub mod list_view;
4
+
5
+pub use list_view::ListView;
garfieldctl/Cargo.tomladded
@@ -0,0 +1,24 @@
1
+[package]
2
+name = "garfieldctl"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+
8
+[[bin]]
9
+name = "garfieldctl"
10
+path = "src/main.rs"
11
+
12
+[dependencies]
13
+# CLI
14
+clap.workspace = true
15
+
16
+# Error handling
17
+anyhow.workspace = true
18
+
19
+# Serialization
20
+serde.workspace = true
21
+serde_json.workspace = true
22
+
23
+# IPC
24
+garfield-ipc.workspace = true
garfieldctl/src/main.rsadded
@@ -0,0 +1,79 @@
1
+//! garfieldctl - CLI control tool for garfield.
2
+
3
+use anyhow::Result;
4
+use clap::{Parser, Subcommand};
5
+use garfield_ipc::{Command, Response};
6
+use std::io::{BufRead, BufReader, Write};
7
+use std::os::unix::net::UnixStream;
8
+use std::path::PathBuf;
9
+
10
+#[derive(Parser)]
11
+#[command(name = "garfieldctl")]
12
+#[command(about = "Control garfield file manager", long_about = None)]
13
+struct Cli {
14
+    #[command(subcommand)]
15
+    command: Commands,
16
+}
17
+
18
+#[derive(Subcommand)]
19
+enum Commands {
20
+    /// Open a directory in garfield.
21
+    Open {
22
+        /// Path to open.
23
+        path: PathBuf,
24
+        /// Open in new tab.
25
+        #[arg(short = 't', long)]
26
+        new_tab: bool,
27
+    },
28
+    /// Get the current directory.
29
+    CurrentDir,
30
+    /// Get garfield status.
31
+    Status,
32
+    /// Quit garfield.
33
+    Quit,
34
+}
35
+
36
+fn main() -> Result<()> {
37
+    let cli = Cli::parse();
38
+
39
+    let cmd = match cli.command {
40
+        Commands::Open { path, new_tab } => Command::Open {
41
+            path: path.to_string_lossy().to_string(),
42
+            new_tab,
43
+        },
44
+        Commands::CurrentDir => Command::CurrentDir,
45
+        Commands::Status => Command::Status,
46
+        Commands::Quit => Command::Quit,
47
+    };
48
+
49
+    let response = send_command(&cmd)?;
50
+
51
+    if response.success {
52
+        if let Some(data) = response.data {
53
+            println!("{}", serde_json::to_string_pretty(&data)?);
54
+        }
55
+    } else {
56
+        eprintln!("Error: {}", response.error.unwrap_or_default());
57
+        std::process::exit(1);
58
+    }
59
+
60
+    Ok(())
61
+}
62
+
63
+fn send_command(cmd: &Command) -> Result<Response> {
64
+    let socket_path = garfield_ipc::socket_path();
65
+    let mut stream = UnixStream::connect(&socket_path)?;
66
+
67
+    // Send command as JSON line
68
+    let json = serde_json::to_string(cmd)?;
69
+    writeln!(stream, "{}", json)?;
70
+    stream.flush()?;
71
+
72
+    // Read response
73
+    let mut reader = BufReader::new(stream);
74
+    let mut line = String::new();
75
+    reader.read_line(&mut line)?;
76
+
77
+    let response: Response = serde_json::from_str(&line)?;
78
+    Ok(response)
79
+}