tenseleyflow/hyprkvm / 6927e93

Browse files

Add project structure with Hyprland IPC client

- Set up Cargo workspace with 3 crates: hyprkvm-daemon, hyprkvm-cli, hyprkvm-common
- Add common types library with Direction, ModifierState, protocol messages
- Implement Hyprland IPC client with socket discovery and JSON queries
- Add event stream handler for Hyprland socket2 events
- Implement monitor layout manager with adjacency computation
- Add keyboard edge detection for workspace boundary detection
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6927e93b138c6715943798eebacfa44b5d503451
Parents
cfb98ec
Tree
857b571

14 changed files

StatusFile+-
A Cargo.lock 1601 0
A Cargo.toml 57 0
A hyprkvm-cli/Cargo.toml 20 0
A hyprkvm-cli/src/main.rs 57 0
A hyprkvm-common/Cargo.toml 12 0
A hyprkvm-common/src/lib.rs 106 0
A hyprkvm-common/src/protocol/mod.rs 204 0
A hyprkvm-daemon/Cargo.toml 52 0
A hyprkvm-daemon/src/hyprland/edge.rs 110 0
A hyprkvm-daemon/src/hyprland/events.rs 244 0
A hyprkvm-daemon/src/hyprland/ipc.rs 191 0
A hyprkvm-daemon/src/hyprland/layout.rs 301 0
A hyprkvm-daemon/src/hyprland/mod.rs 11 0
A hyprkvm-daemon/src/main.rs 251 0
Cargo.lockadded
1601 lines changed — click to load
@@ -0,0 +1,1601 @@
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 = "anstream"
16
+version = "0.6.21"
17
+source = "registry+https://github.com/rust-lang/crates.io-index"
18
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
19
+dependencies = [
20
+ "anstyle",
21
+ "anstyle-parse",
22
+ "anstyle-query",
23
+ "anstyle-wincon",
24
+ "colorchoice",
25
+ "is_terminal_polyfill",
26
+ "utf8parse",
27
+]
28
+
29
+[[package]]
30
+name = "anstyle"
31
+version = "1.0.13"
32
+source = "registry+https://github.com/rust-lang/crates.io-index"
33
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
34
+
35
+[[package]]
36
+name = "anstyle-parse"
37
+version = "0.2.7"
38
+source = "registry+https://github.com/rust-lang/crates.io-index"
39
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
40
+dependencies = [
41
+ "utf8parse",
42
+]
43
+
44
+[[package]]
45
+name = "anstyle-query"
46
+version = "1.1.5"
47
+source = "registry+https://github.com/rust-lang/crates.io-index"
48
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
49
+dependencies = [
50
+ "windows-sys 0.61.2",
51
+]
52
+
53
+[[package]]
54
+name = "anstyle-wincon"
55
+version = "3.0.11"
56
+source = "registry+https://github.com/rust-lang/crates.io-index"
57
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
58
+dependencies = [
59
+ "anstyle",
60
+ "once_cell_polyfill",
61
+ "windows-sys 0.61.2",
62
+]
63
+
64
+[[package]]
65
+name = "anyhow"
66
+version = "1.0.100"
67
+source = "registry+https://github.com/rust-lang/crates.io-index"
68
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
69
+
70
+[[package]]
71
+name = "aws-lc-rs"
72
+version = "1.15.2"
73
+source = "registry+https://github.com/rust-lang/crates.io-index"
74
+checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288"
75
+dependencies = [
76
+ "aws-lc-sys",
77
+ "zeroize",
78
+]
79
+
80
+[[package]]
81
+name = "aws-lc-sys"
82
+version = "0.35.0"
83
+source = "registry+https://github.com/rust-lang/crates.io-index"
84
+checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1"
85
+dependencies = [
86
+ "cc",
87
+ "cmake",
88
+ "dunce",
89
+ "fs_extra",
90
+]
91
+
92
+[[package]]
93
+name = "base64"
94
+version = "0.22.1"
95
+source = "registry+https://github.com/rust-lang/crates.io-index"
96
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
97
+
98
+[[package]]
99
+name = "bitflags"
100
+version = "2.10.0"
101
+source = "registry+https://github.com/rust-lang/crates.io-index"
102
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
103
+
104
+[[package]]
105
+name = "bytemuck"
106
+version = "1.24.0"
107
+source = "registry+https://github.com/rust-lang/crates.io-index"
108
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
109
+dependencies = [
110
+ "bytemuck_derive",
111
+]
112
+
113
+[[package]]
114
+name = "bytemuck_derive"
115
+version = "1.10.2"
116
+source = "registry+https://github.com/rust-lang/crates.io-index"
117
+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
118
+dependencies = [
119
+ "proc-macro2",
120
+ "quote",
121
+ "syn",
122
+]
123
+
124
+[[package]]
125
+name = "bytes"
126
+version = "1.11.0"
127
+source = "registry+https://github.com/rust-lang/crates.io-index"
128
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
129
+
130
+[[package]]
131
+name = "calloop"
132
+version = "0.13.0"
133
+source = "registry+https://github.com/rust-lang/crates.io-index"
134
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
135
+dependencies = [
136
+ "bitflags",
137
+ "log",
138
+ "polling",
139
+ "rustix 0.38.44",
140
+ "slab",
141
+ "thiserror",
142
+]
143
+
144
+[[package]]
145
+name = "calloop-wayland-source"
146
+version = "0.3.0"
147
+source = "registry+https://github.com/rust-lang/crates.io-index"
148
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
149
+dependencies = [
150
+ "calloop",
151
+ "rustix 0.38.44",
152
+ "wayland-backend",
153
+ "wayland-client",
154
+]
155
+
156
+[[package]]
157
+name = "cc"
158
+version = "1.2.51"
159
+source = "registry+https://github.com/rust-lang/crates.io-index"
160
+checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
161
+dependencies = [
162
+ "find-msvc-tools",
163
+ "jobserver",
164
+ "libc",
165
+ "shlex",
166
+]
167
+
168
+[[package]]
169
+name = "cfg-if"
170
+version = "1.0.4"
171
+source = "registry+https://github.com/rust-lang/crates.io-index"
172
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
173
+
174
+[[package]]
175
+name = "clap"
176
+version = "4.5.54"
177
+source = "registry+https://github.com/rust-lang/crates.io-index"
178
+checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
179
+dependencies = [
180
+ "clap_builder",
181
+ "clap_derive",
182
+]
183
+
184
+[[package]]
185
+name = "clap_builder"
186
+version = "4.5.54"
187
+source = "registry+https://github.com/rust-lang/crates.io-index"
188
+checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
189
+dependencies = [
190
+ "anstream",
191
+ "anstyle",
192
+ "clap_lex",
193
+ "strsim",
194
+]
195
+
196
+[[package]]
197
+name = "clap_derive"
198
+version = "4.5.49"
199
+source = "registry+https://github.com/rust-lang/crates.io-index"
200
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
201
+dependencies = [
202
+ "heck",
203
+ "proc-macro2",
204
+ "quote",
205
+ "syn",
206
+]
207
+
208
+[[package]]
209
+name = "clap_lex"
210
+version = "0.7.6"
211
+source = "registry+https://github.com/rust-lang/crates.io-index"
212
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
213
+
214
+[[package]]
215
+name = "cmake"
216
+version = "0.1.57"
217
+source = "registry+https://github.com/rust-lang/crates.io-index"
218
+checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
219
+dependencies = [
220
+ "cc",
221
+]
222
+
223
+[[package]]
224
+name = "colorchoice"
225
+version = "1.0.4"
226
+source = "registry+https://github.com/rust-lang/crates.io-index"
227
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
228
+
229
+[[package]]
230
+name = "concurrent-queue"
231
+version = "2.5.0"
232
+source = "registry+https://github.com/rust-lang/crates.io-index"
233
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
234
+dependencies = [
235
+ "crossbeam-utils",
236
+]
237
+
238
+[[package]]
239
+name = "crossbeam-utils"
240
+version = "0.8.21"
241
+source = "registry+https://github.com/rust-lang/crates.io-index"
242
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
243
+
244
+[[package]]
245
+name = "cursor-icon"
246
+version = "1.2.0"
247
+source = "registry+https://github.com/rust-lang/crates.io-index"
248
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
249
+
250
+[[package]]
251
+name = "deranged"
252
+version = "0.5.5"
253
+source = "registry+https://github.com/rust-lang/crates.io-index"
254
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
255
+dependencies = [
256
+ "powerfmt",
257
+]
258
+
259
+[[package]]
260
+name = "dirs"
261
+version = "5.0.1"
262
+source = "registry+https://github.com/rust-lang/crates.io-index"
263
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
264
+dependencies = [
265
+ "dirs-sys",
266
+]
267
+
268
+[[package]]
269
+name = "dirs-sys"
270
+version = "0.4.1"
271
+source = "registry+https://github.com/rust-lang/crates.io-index"
272
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
273
+dependencies = [
274
+ "libc",
275
+ "option-ext",
276
+ "redox_users",
277
+ "windows-sys 0.48.0",
278
+]
279
+
280
+[[package]]
281
+name = "downcast-rs"
282
+version = "1.2.1"
283
+source = "registry+https://github.com/rust-lang/crates.io-index"
284
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
285
+
286
+[[package]]
287
+name = "dunce"
288
+version = "1.0.5"
289
+source = "registry+https://github.com/rust-lang/crates.io-index"
290
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
291
+
292
+[[package]]
293
+name = "equivalent"
294
+version = "1.0.2"
295
+source = "registry+https://github.com/rust-lang/crates.io-index"
296
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
297
+
298
+[[package]]
299
+name = "errno"
300
+version = "0.3.14"
301
+source = "registry+https://github.com/rust-lang/crates.io-index"
302
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
303
+dependencies = [
304
+ "libc",
305
+ "windows-sys 0.61.2",
306
+]
307
+
308
+[[package]]
309
+name = "find-msvc-tools"
310
+version = "0.1.6"
311
+source = "registry+https://github.com/rust-lang/crates.io-index"
312
+checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
313
+
314
+[[package]]
315
+name = "fs_extra"
316
+version = "1.3.0"
317
+source = "registry+https://github.com/rust-lang/crates.io-index"
318
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
319
+
320
+[[package]]
321
+name = "getrandom"
322
+version = "0.2.16"
323
+source = "registry+https://github.com/rust-lang/crates.io-index"
324
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
325
+dependencies = [
326
+ "cfg-if",
327
+ "libc",
328
+ "wasi",
329
+]
330
+
331
+[[package]]
332
+name = "getrandom"
333
+version = "0.3.4"
334
+source = "registry+https://github.com/rust-lang/crates.io-index"
335
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
336
+dependencies = [
337
+ "cfg-if",
338
+ "libc",
339
+ "r-efi",
340
+ "wasip2",
341
+]
342
+
343
+[[package]]
344
+name = "hashbrown"
345
+version = "0.16.1"
346
+source = "registry+https://github.com/rust-lang/crates.io-index"
347
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
348
+
349
+[[package]]
350
+name = "heck"
351
+version = "0.5.0"
352
+source = "registry+https://github.com/rust-lang/crates.io-index"
353
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
354
+
355
+[[package]]
356
+name = "hermit-abi"
357
+version = "0.5.2"
358
+source = "registry+https://github.com/rust-lang/crates.io-index"
359
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
360
+
361
+[[package]]
362
+name = "hostname"
363
+version = "0.4.2"
364
+source = "registry+https://github.com/rust-lang/crates.io-index"
365
+checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
366
+dependencies = [
367
+ "cfg-if",
368
+ "libc",
369
+ "windows-link",
370
+]
371
+
372
+[[package]]
373
+name = "hyprkvm-cli"
374
+version = "0.1.0"
375
+dependencies = [
376
+ "anyhow",
377
+ "clap",
378
+ "hyprkvm-common",
379
+ "serde",
380
+ "serde_json",
381
+ "tokio",
382
+]
383
+
384
+[[package]]
385
+name = "hyprkvm-common"
386
+version = "0.1.0"
387
+dependencies = [
388
+ "serde",
389
+ "serde_json",
390
+ "thiserror",
391
+]
392
+
393
+[[package]]
394
+name = "hyprkvm-daemon"
395
+version = "0.1.0"
396
+dependencies = [
397
+ "anyhow",
398
+ "bytes",
399
+ "clap",
400
+ "dirs",
401
+ "hostname",
402
+ "hyprkvm-common",
403
+ "rcgen",
404
+ "rustix 0.38.44",
405
+ "rustls",
406
+ "rustls-pemfile",
407
+ "serde",
408
+ "serde_json",
409
+ "smithay-client-toolkit",
410
+ "thiserror",
411
+ "tokio",
412
+ "tokio-rustls",
413
+ "toml",
414
+ "tracing",
415
+ "tracing-subscriber",
416
+ "wayland-client",
417
+ "wayland-protocols 0.31.2",
418
+ "wayland-protocols-misc",
419
+ "wayland-protocols-wlr",
420
+]
421
+
422
+[[package]]
423
+name = "indexmap"
424
+version = "2.12.1"
425
+source = "registry+https://github.com/rust-lang/crates.io-index"
426
+checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
427
+dependencies = [
428
+ "equivalent",
429
+ "hashbrown",
430
+]
431
+
432
+[[package]]
433
+name = "is_terminal_polyfill"
434
+version = "1.70.2"
435
+source = "registry+https://github.com/rust-lang/crates.io-index"
436
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
437
+
438
+[[package]]
439
+name = "itoa"
440
+version = "1.0.17"
441
+source = "registry+https://github.com/rust-lang/crates.io-index"
442
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
443
+
444
+[[package]]
445
+name = "jobserver"
446
+version = "0.1.34"
447
+source = "registry+https://github.com/rust-lang/crates.io-index"
448
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
449
+dependencies = [
450
+ "getrandom 0.3.4",
451
+ "libc",
452
+]
453
+
454
+[[package]]
455
+name = "lazy_static"
456
+version = "1.5.0"
457
+source = "registry+https://github.com/rust-lang/crates.io-index"
458
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
459
+
460
+[[package]]
461
+name = "libc"
462
+version = "0.2.179"
463
+source = "registry+https://github.com/rust-lang/crates.io-index"
464
+checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
465
+
466
+[[package]]
467
+name = "libredox"
468
+version = "0.1.12"
469
+source = "registry+https://github.com/rust-lang/crates.io-index"
470
+checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
471
+dependencies = [
472
+ "bitflags",
473
+ "libc",
474
+]
475
+
476
+[[package]]
477
+name = "linux-raw-sys"
478
+version = "0.4.15"
479
+source = "registry+https://github.com/rust-lang/crates.io-index"
480
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
481
+
482
+[[package]]
483
+name = "linux-raw-sys"
484
+version = "0.11.0"
485
+source = "registry+https://github.com/rust-lang/crates.io-index"
486
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
487
+
488
+[[package]]
489
+name = "lock_api"
490
+version = "0.4.14"
491
+source = "registry+https://github.com/rust-lang/crates.io-index"
492
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
493
+dependencies = [
494
+ "scopeguard",
495
+]
496
+
497
+[[package]]
498
+name = "log"
499
+version = "0.4.29"
500
+source = "registry+https://github.com/rust-lang/crates.io-index"
501
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
502
+
503
+[[package]]
504
+name = "matchers"
505
+version = "0.2.0"
506
+source = "registry+https://github.com/rust-lang/crates.io-index"
507
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
508
+dependencies = [
509
+ "regex-automata",
510
+]
511
+
512
+[[package]]
513
+name = "memchr"
514
+version = "2.7.6"
515
+source = "registry+https://github.com/rust-lang/crates.io-index"
516
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
517
+
518
+[[package]]
519
+name = "memmap2"
520
+version = "0.8.0"
521
+source = "registry+https://github.com/rust-lang/crates.io-index"
522
+checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
523
+dependencies = [
524
+ "libc",
525
+]
526
+
527
+[[package]]
528
+name = "memmap2"
529
+version = "0.9.9"
530
+source = "registry+https://github.com/rust-lang/crates.io-index"
531
+checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490"
532
+dependencies = [
533
+ "libc",
534
+]
535
+
536
+[[package]]
537
+name = "mio"
538
+version = "1.1.1"
539
+source = "registry+https://github.com/rust-lang/crates.io-index"
540
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
541
+dependencies = [
542
+ "libc",
543
+ "wasi",
544
+ "windows-sys 0.61.2",
545
+]
546
+
547
+[[package]]
548
+name = "nu-ansi-term"
549
+version = "0.50.3"
550
+source = "registry+https://github.com/rust-lang/crates.io-index"
551
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
552
+dependencies = [
553
+ "windows-sys 0.61.2",
554
+]
555
+
556
+[[package]]
557
+name = "num-conv"
558
+version = "0.1.0"
559
+source = "registry+https://github.com/rust-lang/crates.io-index"
560
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
561
+
562
+[[package]]
563
+name = "once_cell"
564
+version = "1.21.3"
565
+source = "registry+https://github.com/rust-lang/crates.io-index"
566
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
567
+
568
+[[package]]
569
+name = "once_cell_polyfill"
570
+version = "1.70.2"
571
+source = "registry+https://github.com/rust-lang/crates.io-index"
572
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
573
+
574
+[[package]]
575
+name = "option-ext"
576
+version = "0.2.0"
577
+source = "registry+https://github.com/rust-lang/crates.io-index"
578
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
579
+
580
+[[package]]
581
+name = "parking_lot"
582
+version = "0.12.5"
583
+source = "registry+https://github.com/rust-lang/crates.io-index"
584
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
585
+dependencies = [
586
+ "lock_api",
587
+ "parking_lot_core",
588
+]
589
+
590
+[[package]]
591
+name = "parking_lot_core"
592
+version = "0.9.12"
593
+source = "registry+https://github.com/rust-lang/crates.io-index"
594
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
595
+dependencies = [
596
+ "cfg-if",
597
+ "libc",
598
+ "redox_syscall",
599
+ "smallvec",
600
+ "windows-link",
601
+]
602
+
603
+[[package]]
604
+name = "pem"
605
+version = "3.0.6"
606
+source = "registry+https://github.com/rust-lang/crates.io-index"
607
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
608
+dependencies = [
609
+ "base64",
610
+ "serde_core",
611
+]
612
+
613
+[[package]]
614
+name = "pin-project-lite"
615
+version = "0.2.16"
616
+source = "registry+https://github.com/rust-lang/crates.io-index"
617
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
618
+
619
+[[package]]
620
+name = "pkg-config"
621
+version = "0.3.32"
622
+source = "registry+https://github.com/rust-lang/crates.io-index"
623
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
624
+
625
+[[package]]
626
+name = "polling"
627
+version = "3.11.0"
628
+source = "registry+https://github.com/rust-lang/crates.io-index"
629
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
630
+dependencies = [
631
+ "cfg-if",
632
+ "concurrent-queue",
633
+ "hermit-abi",
634
+ "pin-project-lite",
635
+ "rustix 1.1.3",
636
+ "windows-sys 0.61.2",
637
+]
638
+
639
+[[package]]
640
+name = "powerfmt"
641
+version = "0.2.0"
642
+source = "registry+https://github.com/rust-lang/crates.io-index"
643
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
644
+
645
+[[package]]
646
+name = "proc-macro2"
647
+version = "1.0.104"
648
+source = "registry+https://github.com/rust-lang/crates.io-index"
649
+checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
650
+dependencies = [
651
+ "unicode-ident",
652
+]
653
+
654
+[[package]]
655
+name = "quick-xml"
656
+version = "0.38.4"
657
+source = "registry+https://github.com/rust-lang/crates.io-index"
658
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
659
+dependencies = [
660
+ "memchr",
661
+]
662
+
663
+[[package]]
664
+name = "quote"
665
+version = "1.0.42"
666
+source = "registry+https://github.com/rust-lang/crates.io-index"
667
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
668
+dependencies = [
669
+ "proc-macro2",
670
+]
671
+
672
+[[package]]
673
+name = "r-efi"
674
+version = "5.3.0"
675
+source = "registry+https://github.com/rust-lang/crates.io-index"
676
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
677
+
678
+[[package]]
679
+name = "rcgen"
680
+version = "0.13.2"
681
+source = "registry+https://github.com/rust-lang/crates.io-index"
682
+checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
683
+dependencies = [
684
+ "pem",
685
+ "ring",
686
+ "rustls-pki-types",
687
+ "time",
688
+ "yasna",
689
+]
690
+
691
+[[package]]
692
+name = "redox_syscall"
693
+version = "0.5.18"
694
+source = "registry+https://github.com/rust-lang/crates.io-index"
695
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
696
+dependencies = [
697
+ "bitflags",
698
+]
699
+
700
+[[package]]
701
+name = "redox_users"
702
+version = "0.4.6"
703
+source = "registry+https://github.com/rust-lang/crates.io-index"
704
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
705
+dependencies = [
706
+ "getrandom 0.2.16",
707
+ "libredox",
708
+ "thiserror",
709
+]
710
+
711
+[[package]]
712
+name = "regex-automata"
713
+version = "0.4.13"
714
+source = "registry+https://github.com/rust-lang/crates.io-index"
715
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
716
+dependencies = [
717
+ "aho-corasick",
718
+ "memchr",
719
+ "regex-syntax",
720
+]
721
+
722
+[[package]]
723
+name = "regex-syntax"
724
+version = "0.8.8"
725
+source = "registry+https://github.com/rust-lang/crates.io-index"
726
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
727
+
728
+[[package]]
729
+name = "ring"
730
+version = "0.17.14"
731
+source = "registry+https://github.com/rust-lang/crates.io-index"
732
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
733
+dependencies = [
734
+ "cc",
735
+ "cfg-if",
736
+ "getrandom 0.2.16",
737
+ "libc",
738
+ "untrusted",
739
+ "windows-sys 0.52.0",
740
+]
741
+
742
+[[package]]
743
+name = "rustix"
744
+version = "0.38.44"
745
+source = "registry+https://github.com/rust-lang/crates.io-index"
746
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
747
+dependencies = [
748
+ "bitflags",
749
+ "errno",
750
+ "libc",
751
+ "linux-raw-sys 0.4.15",
752
+ "windows-sys 0.52.0",
753
+]
754
+
755
+[[package]]
756
+name = "rustix"
757
+version = "1.1.3"
758
+source = "registry+https://github.com/rust-lang/crates.io-index"
759
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
760
+dependencies = [
761
+ "bitflags",
762
+ "errno",
763
+ "libc",
764
+ "linux-raw-sys 0.11.0",
765
+ "windows-sys 0.61.2",
766
+]
767
+
768
+[[package]]
769
+name = "rustls"
770
+version = "0.23.35"
771
+source = "registry+https://github.com/rust-lang/crates.io-index"
772
+checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
773
+dependencies = [
774
+ "aws-lc-rs",
775
+ "log",
776
+ "once_cell",
777
+ "rustls-pki-types",
778
+ "rustls-webpki",
779
+ "subtle",
780
+ "zeroize",
781
+]
782
+
783
+[[package]]
784
+name = "rustls-pemfile"
785
+version = "2.2.0"
786
+source = "registry+https://github.com/rust-lang/crates.io-index"
787
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
788
+dependencies = [
789
+ "rustls-pki-types",
790
+]
791
+
792
+[[package]]
793
+name = "rustls-pki-types"
794
+version = "1.13.2"
795
+source = "registry+https://github.com/rust-lang/crates.io-index"
796
+checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
797
+dependencies = [
798
+ "zeroize",
799
+]
800
+
801
+[[package]]
802
+name = "rustls-webpki"
803
+version = "0.103.8"
804
+source = "registry+https://github.com/rust-lang/crates.io-index"
805
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
806
+dependencies = [
807
+ "aws-lc-rs",
808
+ "ring",
809
+ "rustls-pki-types",
810
+ "untrusted",
811
+]
812
+
813
+[[package]]
814
+name = "scopeguard"
815
+version = "1.2.0"
816
+source = "registry+https://github.com/rust-lang/crates.io-index"
817
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
818
+
819
+[[package]]
820
+name = "serde"
821
+version = "1.0.228"
822
+source = "registry+https://github.com/rust-lang/crates.io-index"
823
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
824
+dependencies = [
825
+ "serde_core",
826
+ "serde_derive",
827
+]
828
+
829
+[[package]]
830
+name = "serde_core"
831
+version = "1.0.228"
832
+source = "registry+https://github.com/rust-lang/crates.io-index"
833
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
834
+dependencies = [
835
+ "serde_derive",
836
+]
837
+
838
+[[package]]
839
+name = "serde_derive"
840
+version = "1.0.228"
841
+source = "registry+https://github.com/rust-lang/crates.io-index"
842
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
843
+dependencies = [
844
+ "proc-macro2",
845
+ "quote",
846
+ "syn",
847
+]
848
+
849
+[[package]]
850
+name = "serde_json"
851
+version = "1.0.148"
852
+source = "registry+https://github.com/rust-lang/crates.io-index"
853
+checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
854
+dependencies = [
855
+ "itoa",
856
+ "memchr",
857
+ "serde",
858
+ "serde_core",
859
+ "zmij",
860
+]
861
+
862
+[[package]]
863
+name = "serde_spanned"
864
+version = "0.6.9"
865
+source = "registry+https://github.com/rust-lang/crates.io-index"
866
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
867
+dependencies = [
868
+ "serde",
869
+]
870
+
871
+[[package]]
872
+name = "sharded-slab"
873
+version = "0.1.7"
874
+source = "registry+https://github.com/rust-lang/crates.io-index"
875
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
876
+dependencies = [
877
+ "lazy_static",
878
+]
879
+
880
+[[package]]
881
+name = "shlex"
882
+version = "1.3.0"
883
+source = "registry+https://github.com/rust-lang/crates.io-index"
884
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
885
+
886
+[[package]]
887
+name = "signal-hook-registry"
888
+version = "1.4.8"
889
+source = "registry+https://github.com/rust-lang/crates.io-index"
890
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
891
+dependencies = [
892
+ "errno",
893
+ "libc",
894
+]
895
+
896
+[[package]]
897
+name = "slab"
898
+version = "0.4.11"
899
+source = "registry+https://github.com/rust-lang/crates.io-index"
900
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
901
+
902
+[[package]]
903
+name = "smallvec"
904
+version = "1.15.1"
905
+source = "registry+https://github.com/rust-lang/crates.io-index"
906
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
907
+
908
+[[package]]
909
+name = "smithay-client-toolkit"
910
+version = "0.19.2"
911
+source = "registry+https://github.com/rust-lang/crates.io-index"
912
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
913
+dependencies = [
914
+ "bitflags",
915
+ "bytemuck",
916
+ "calloop",
917
+ "calloop-wayland-source",
918
+ "cursor-icon",
919
+ "libc",
920
+ "log",
921
+ "memmap2 0.9.9",
922
+ "pkg-config",
923
+ "rustix 0.38.44",
924
+ "thiserror",
925
+ "wayland-backend",
926
+ "wayland-client",
927
+ "wayland-csd-frame",
928
+ "wayland-cursor",
929
+ "wayland-protocols 0.32.10",
930
+ "wayland-protocols-wlr",
931
+ "wayland-scanner",
932
+ "xkbcommon",
933
+ "xkeysym",
934
+]
935
+
936
+[[package]]
937
+name = "socket2"
938
+version = "0.6.1"
939
+source = "registry+https://github.com/rust-lang/crates.io-index"
940
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
941
+dependencies = [
942
+ "libc",
943
+ "windows-sys 0.60.2",
944
+]
945
+
946
+[[package]]
947
+name = "strsim"
948
+version = "0.11.1"
949
+source = "registry+https://github.com/rust-lang/crates.io-index"
950
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
951
+
952
+[[package]]
953
+name = "subtle"
954
+version = "2.6.1"
955
+source = "registry+https://github.com/rust-lang/crates.io-index"
956
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
957
+
958
+[[package]]
959
+name = "syn"
960
+version = "2.0.113"
961
+source = "registry+https://github.com/rust-lang/crates.io-index"
962
+checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
963
+dependencies = [
964
+ "proc-macro2",
965
+ "quote",
966
+ "unicode-ident",
967
+]
968
+
969
+[[package]]
970
+name = "thiserror"
971
+version = "1.0.69"
972
+source = "registry+https://github.com/rust-lang/crates.io-index"
973
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
974
+dependencies = [
975
+ "thiserror-impl",
976
+]
977
+
978
+[[package]]
979
+name = "thiserror-impl"
980
+version = "1.0.69"
981
+source = "registry+https://github.com/rust-lang/crates.io-index"
982
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
983
+dependencies = [
984
+ "proc-macro2",
985
+ "quote",
986
+ "syn",
987
+]
988
+
989
+[[package]]
990
+name = "thread_local"
991
+version = "1.1.9"
992
+source = "registry+https://github.com/rust-lang/crates.io-index"
993
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
994
+dependencies = [
995
+ "cfg-if",
996
+]
997
+
998
+[[package]]
999
+name = "time"
1000
+version = "0.3.44"
1001
+source = "registry+https://github.com/rust-lang/crates.io-index"
1002
+checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
1003
+dependencies = [
1004
+ "deranged",
1005
+ "num-conv",
1006
+ "powerfmt",
1007
+ "serde",
1008
+ "time-core",
1009
+]
1010
+
1011
+[[package]]
1012
+name = "time-core"
1013
+version = "0.1.6"
1014
+source = "registry+https://github.com/rust-lang/crates.io-index"
1015
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
1016
+
1017
+[[package]]
1018
+name = "tokio"
1019
+version = "1.49.0"
1020
+source = "registry+https://github.com/rust-lang/crates.io-index"
1021
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
1022
+dependencies = [
1023
+ "bytes",
1024
+ "libc",
1025
+ "mio",
1026
+ "parking_lot",
1027
+ "pin-project-lite",
1028
+ "signal-hook-registry",
1029
+ "socket2",
1030
+ "tokio-macros",
1031
+ "windows-sys 0.61.2",
1032
+]
1033
+
1034
+[[package]]
1035
+name = "tokio-macros"
1036
+version = "2.6.0"
1037
+source = "registry+https://github.com/rust-lang/crates.io-index"
1038
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
1039
+dependencies = [
1040
+ "proc-macro2",
1041
+ "quote",
1042
+ "syn",
1043
+]
1044
+
1045
+[[package]]
1046
+name = "tokio-rustls"
1047
+version = "0.26.4"
1048
+source = "registry+https://github.com/rust-lang/crates.io-index"
1049
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
1050
+dependencies = [
1051
+ "rustls",
1052
+ "tokio",
1053
+]
1054
+
1055
+[[package]]
1056
+name = "toml"
1057
+version = "0.8.23"
1058
+source = "registry+https://github.com/rust-lang/crates.io-index"
1059
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
1060
+dependencies = [
1061
+ "serde",
1062
+ "serde_spanned",
1063
+ "toml_datetime",
1064
+ "toml_edit",
1065
+]
1066
+
1067
+[[package]]
1068
+name = "toml_datetime"
1069
+version = "0.6.11"
1070
+source = "registry+https://github.com/rust-lang/crates.io-index"
1071
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
1072
+dependencies = [
1073
+ "serde",
1074
+]
1075
+
1076
+[[package]]
1077
+name = "toml_edit"
1078
+version = "0.22.27"
1079
+source = "registry+https://github.com/rust-lang/crates.io-index"
1080
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
1081
+dependencies = [
1082
+ "indexmap",
1083
+ "serde",
1084
+ "serde_spanned",
1085
+ "toml_datetime",
1086
+ "toml_write",
1087
+ "winnow",
1088
+]
1089
+
1090
+[[package]]
1091
+name = "toml_write"
1092
+version = "0.1.2"
1093
+source = "registry+https://github.com/rust-lang/crates.io-index"
1094
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
1095
+
1096
+[[package]]
1097
+name = "tracing"
1098
+version = "0.1.44"
1099
+source = "registry+https://github.com/rust-lang/crates.io-index"
1100
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1101
+dependencies = [
1102
+ "pin-project-lite",
1103
+ "tracing-attributes",
1104
+ "tracing-core",
1105
+]
1106
+
1107
+[[package]]
1108
+name = "tracing-attributes"
1109
+version = "0.1.31"
1110
+source = "registry+https://github.com/rust-lang/crates.io-index"
1111
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1112
+dependencies = [
1113
+ "proc-macro2",
1114
+ "quote",
1115
+ "syn",
1116
+]
1117
+
1118
+[[package]]
1119
+name = "tracing-core"
1120
+version = "0.1.36"
1121
+source = "registry+https://github.com/rust-lang/crates.io-index"
1122
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1123
+dependencies = [
1124
+ "once_cell",
1125
+ "valuable",
1126
+]
1127
+
1128
+[[package]]
1129
+name = "tracing-log"
1130
+version = "0.2.0"
1131
+source = "registry+https://github.com/rust-lang/crates.io-index"
1132
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1133
+dependencies = [
1134
+ "log",
1135
+ "once_cell",
1136
+ "tracing-core",
1137
+]
1138
+
1139
+[[package]]
1140
+name = "tracing-subscriber"
1141
+version = "0.3.22"
1142
+source = "registry+https://github.com/rust-lang/crates.io-index"
1143
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
1144
+dependencies = [
1145
+ "matchers",
1146
+ "nu-ansi-term",
1147
+ "once_cell",
1148
+ "regex-automata",
1149
+ "sharded-slab",
1150
+ "smallvec",
1151
+ "thread_local",
1152
+ "tracing",
1153
+ "tracing-core",
1154
+ "tracing-log",
1155
+]
1156
+
1157
+[[package]]
1158
+name = "unicode-ident"
1159
+version = "1.0.22"
1160
+source = "registry+https://github.com/rust-lang/crates.io-index"
1161
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
1162
+
1163
+[[package]]
1164
+name = "untrusted"
1165
+version = "0.9.0"
1166
+source = "registry+https://github.com/rust-lang/crates.io-index"
1167
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1168
+
1169
+[[package]]
1170
+name = "utf8parse"
1171
+version = "0.2.2"
1172
+source = "registry+https://github.com/rust-lang/crates.io-index"
1173
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1174
+
1175
+[[package]]
1176
+name = "valuable"
1177
+version = "0.1.1"
1178
+source = "registry+https://github.com/rust-lang/crates.io-index"
1179
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1180
+
1181
+[[package]]
1182
+name = "wasi"
1183
+version = "0.11.1+wasi-snapshot-preview1"
1184
+source = "registry+https://github.com/rust-lang/crates.io-index"
1185
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1186
+
1187
+[[package]]
1188
+name = "wasip2"
1189
+version = "1.0.1+wasi-0.2.4"
1190
+source = "registry+https://github.com/rust-lang/crates.io-index"
1191
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
1192
+dependencies = [
1193
+ "wit-bindgen",
1194
+]
1195
+
1196
+[[package]]
1197
+name = "wayland-backend"
1198
+version = "0.3.12"
1199
+source = "registry+https://github.com/rust-lang/crates.io-index"
1200
+checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
1201
+dependencies = [
1202
+ "cc",
1203
+ "downcast-rs",
1204
+ "rustix 1.1.3",
1205
+ "smallvec",
1206
+ "wayland-sys",
1207
+]
1208
+
1209
+[[package]]
1210
+name = "wayland-client"
1211
+version = "0.31.12"
1212
+source = "registry+https://github.com/rust-lang/crates.io-index"
1213
+checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
1214
+dependencies = [
1215
+ "bitflags",
1216
+ "rustix 1.1.3",
1217
+ "wayland-backend",
1218
+ "wayland-scanner",
1219
+]
1220
+
1221
+[[package]]
1222
+name = "wayland-csd-frame"
1223
+version = "0.3.0"
1224
+source = "registry+https://github.com/rust-lang/crates.io-index"
1225
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
1226
+dependencies = [
1227
+ "bitflags",
1228
+ "cursor-icon",
1229
+ "wayland-backend",
1230
+]
1231
+
1232
+[[package]]
1233
+name = "wayland-cursor"
1234
+version = "0.31.12"
1235
+source = "registry+https://github.com/rust-lang/crates.io-index"
1236
+checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078"
1237
+dependencies = [
1238
+ "rustix 1.1.3",
1239
+ "wayland-client",
1240
+ "xcursor",
1241
+]
1242
+
1243
+[[package]]
1244
+name = "wayland-protocols"
1245
+version = "0.31.2"
1246
+source = "registry+https://github.com/rust-lang/crates.io-index"
1247
+checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
1248
+dependencies = [
1249
+ "bitflags",
1250
+ "wayland-backend",
1251
+ "wayland-client",
1252
+ "wayland-scanner",
1253
+]
1254
+
1255
+[[package]]
1256
+name = "wayland-protocols"
1257
+version = "0.32.10"
1258
+source = "registry+https://github.com/rust-lang/crates.io-index"
1259
+checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
1260
+dependencies = [
1261
+ "bitflags",
1262
+ "wayland-backend",
1263
+ "wayland-client",
1264
+ "wayland-scanner",
1265
+]
1266
+
1267
+[[package]]
1268
+name = "wayland-protocols-misc"
1269
+version = "0.3.10"
1270
+source = "registry+https://github.com/rust-lang/crates.io-index"
1271
+checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e"
1272
+dependencies = [
1273
+ "bitflags",
1274
+ "wayland-backend",
1275
+ "wayland-client",
1276
+ "wayland-protocols 0.32.10",
1277
+ "wayland-scanner",
1278
+]
1279
+
1280
+[[package]]
1281
+name = "wayland-protocols-wlr"
1282
+version = "0.3.10"
1283
+source = "registry+https://github.com/rust-lang/crates.io-index"
1284
+checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
1285
+dependencies = [
1286
+ "bitflags",
1287
+ "wayland-backend",
1288
+ "wayland-client",
1289
+ "wayland-protocols 0.32.10",
1290
+ "wayland-scanner",
1291
+]
1292
+
1293
+[[package]]
1294
+name = "wayland-scanner"
1295
+version = "0.31.8"
1296
+source = "registry+https://github.com/rust-lang/crates.io-index"
1297
+checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
1298
+dependencies = [
1299
+ "proc-macro2",
1300
+ "quick-xml",
1301
+ "quote",
1302
+]
1303
+
1304
+[[package]]
1305
+name = "wayland-sys"
1306
+version = "0.31.8"
1307
+source = "registry+https://github.com/rust-lang/crates.io-index"
1308
+checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
1309
+dependencies = [
1310
+ "pkg-config",
1311
+]
1312
+
1313
+[[package]]
1314
+name = "windows-link"
1315
+version = "0.2.1"
1316
+source = "registry+https://github.com/rust-lang/crates.io-index"
1317
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1318
+
1319
+[[package]]
1320
+name = "windows-sys"
1321
+version = "0.48.0"
1322
+source = "registry+https://github.com/rust-lang/crates.io-index"
1323
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1324
+dependencies = [
1325
+ "windows-targets 0.48.5",
1326
+]
1327
+
1328
+[[package]]
1329
+name = "windows-sys"
1330
+version = "0.52.0"
1331
+source = "registry+https://github.com/rust-lang/crates.io-index"
1332
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1333
+dependencies = [
1334
+ "windows-targets 0.52.6",
1335
+]
1336
+
1337
+[[package]]
1338
+name = "windows-sys"
1339
+version = "0.60.2"
1340
+source = "registry+https://github.com/rust-lang/crates.io-index"
1341
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
1342
+dependencies = [
1343
+ "windows-targets 0.53.5",
1344
+]
1345
+
1346
+[[package]]
1347
+name = "windows-sys"
1348
+version = "0.61.2"
1349
+source = "registry+https://github.com/rust-lang/crates.io-index"
1350
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1351
+dependencies = [
1352
+ "windows-link",
1353
+]
1354
+
1355
+[[package]]
1356
+name = "windows-targets"
1357
+version = "0.48.5"
1358
+source = "registry+https://github.com/rust-lang/crates.io-index"
1359
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1360
+dependencies = [
1361
+ "windows_aarch64_gnullvm 0.48.5",
1362
+ "windows_aarch64_msvc 0.48.5",
1363
+ "windows_i686_gnu 0.48.5",
1364
+ "windows_i686_msvc 0.48.5",
1365
+ "windows_x86_64_gnu 0.48.5",
1366
+ "windows_x86_64_gnullvm 0.48.5",
1367
+ "windows_x86_64_msvc 0.48.5",
1368
+]
1369
+
1370
+[[package]]
1371
+name = "windows-targets"
1372
+version = "0.52.6"
1373
+source = "registry+https://github.com/rust-lang/crates.io-index"
1374
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1375
+dependencies = [
1376
+ "windows_aarch64_gnullvm 0.52.6",
1377
+ "windows_aarch64_msvc 0.52.6",
1378
+ "windows_i686_gnu 0.52.6",
1379
+ "windows_i686_gnullvm 0.52.6",
1380
+ "windows_i686_msvc 0.52.6",
1381
+ "windows_x86_64_gnu 0.52.6",
1382
+ "windows_x86_64_gnullvm 0.52.6",
1383
+ "windows_x86_64_msvc 0.52.6",
1384
+]
1385
+
1386
+[[package]]
1387
+name = "windows-targets"
1388
+version = "0.53.5"
1389
+source = "registry+https://github.com/rust-lang/crates.io-index"
1390
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
1391
+dependencies = [
1392
+ "windows-link",
1393
+ "windows_aarch64_gnullvm 0.53.1",
1394
+ "windows_aarch64_msvc 0.53.1",
1395
+ "windows_i686_gnu 0.53.1",
1396
+ "windows_i686_gnullvm 0.53.1",
1397
+ "windows_i686_msvc 0.53.1",
1398
+ "windows_x86_64_gnu 0.53.1",
1399
+ "windows_x86_64_gnullvm 0.53.1",
1400
+ "windows_x86_64_msvc 0.53.1",
1401
+]
1402
+
1403
+[[package]]
1404
+name = "windows_aarch64_gnullvm"
1405
+version = "0.48.5"
1406
+source = "registry+https://github.com/rust-lang/crates.io-index"
1407
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1408
+
1409
+[[package]]
1410
+name = "windows_aarch64_gnullvm"
1411
+version = "0.52.6"
1412
+source = "registry+https://github.com/rust-lang/crates.io-index"
1413
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1414
+
1415
+[[package]]
1416
+name = "windows_aarch64_gnullvm"
1417
+version = "0.53.1"
1418
+source = "registry+https://github.com/rust-lang/crates.io-index"
1419
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
1420
+
1421
+[[package]]
1422
+name = "windows_aarch64_msvc"
1423
+version = "0.48.5"
1424
+source = "registry+https://github.com/rust-lang/crates.io-index"
1425
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1426
+
1427
+[[package]]
1428
+name = "windows_aarch64_msvc"
1429
+version = "0.52.6"
1430
+source = "registry+https://github.com/rust-lang/crates.io-index"
1431
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1432
+
1433
+[[package]]
1434
+name = "windows_aarch64_msvc"
1435
+version = "0.53.1"
1436
+source = "registry+https://github.com/rust-lang/crates.io-index"
1437
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
1438
+
1439
+[[package]]
1440
+name = "windows_i686_gnu"
1441
+version = "0.48.5"
1442
+source = "registry+https://github.com/rust-lang/crates.io-index"
1443
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1444
+
1445
+[[package]]
1446
+name = "windows_i686_gnu"
1447
+version = "0.52.6"
1448
+source = "registry+https://github.com/rust-lang/crates.io-index"
1449
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1450
+
1451
+[[package]]
1452
+name = "windows_i686_gnu"
1453
+version = "0.53.1"
1454
+source = "registry+https://github.com/rust-lang/crates.io-index"
1455
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
1456
+
1457
+[[package]]
1458
+name = "windows_i686_gnullvm"
1459
+version = "0.52.6"
1460
+source = "registry+https://github.com/rust-lang/crates.io-index"
1461
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1462
+
1463
+[[package]]
1464
+name = "windows_i686_gnullvm"
1465
+version = "0.53.1"
1466
+source = "registry+https://github.com/rust-lang/crates.io-index"
1467
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
1468
+
1469
+[[package]]
1470
+name = "windows_i686_msvc"
1471
+version = "0.48.5"
1472
+source = "registry+https://github.com/rust-lang/crates.io-index"
1473
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1474
+
1475
+[[package]]
1476
+name = "windows_i686_msvc"
1477
+version = "0.52.6"
1478
+source = "registry+https://github.com/rust-lang/crates.io-index"
1479
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1480
+
1481
+[[package]]
1482
+name = "windows_i686_msvc"
1483
+version = "0.53.1"
1484
+source = "registry+https://github.com/rust-lang/crates.io-index"
1485
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
1486
+
1487
+[[package]]
1488
+name = "windows_x86_64_gnu"
1489
+version = "0.48.5"
1490
+source = "registry+https://github.com/rust-lang/crates.io-index"
1491
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1492
+
1493
+[[package]]
1494
+name = "windows_x86_64_gnu"
1495
+version = "0.52.6"
1496
+source = "registry+https://github.com/rust-lang/crates.io-index"
1497
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1498
+
1499
+[[package]]
1500
+name = "windows_x86_64_gnu"
1501
+version = "0.53.1"
1502
+source = "registry+https://github.com/rust-lang/crates.io-index"
1503
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
1504
+
1505
+[[package]]
1506
+name = "windows_x86_64_gnullvm"
1507
+version = "0.48.5"
1508
+source = "registry+https://github.com/rust-lang/crates.io-index"
1509
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1510
+
1511
+[[package]]
1512
+name = "windows_x86_64_gnullvm"
1513
+version = "0.52.6"
1514
+source = "registry+https://github.com/rust-lang/crates.io-index"
1515
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1516
+
1517
+[[package]]
1518
+name = "windows_x86_64_gnullvm"
1519
+version = "0.53.1"
1520
+source = "registry+https://github.com/rust-lang/crates.io-index"
1521
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
1522
+
1523
+[[package]]
1524
+name = "windows_x86_64_msvc"
1525
+version = "0.48.5"
1526
+source = "registry+https://github.com/rust-lang/crates.io-index"
1527
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1528
+
1529
+[[package]]
1530
+name = "windows_x86_64_msvc"
1531
+version = "0.52.6"
1532
+source = "registry+https://github.com/rust-lang/crates.io-index"
1533
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1534
+
1535
+[[package]]
1536
+name = "windows_x86_64_msvc"
1537
+version = "0.53.1"
1538
+source = "registry+https://github.com/rust-lang/crates.io-index"
1539
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
1540
+
1541
+[[package]]
1542
+name = "winnow"
1543
+version = "0.7.14"
1544
+source = "registry+https://github.com/rust-lang/crates.io-index"
1545
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
1546
+dependencies = [
1547
+ "memchr",
1548
+]
1549
+
1550
+[[package]]
1551
+name = "wit-bindgen"
1552
+version = "0.46.0"
1553
+source = "registry+https://github.com/rust-lang/crates.io-index"
1554
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
1555
+
1556
+[[package]]
1557
+name = "xcursor"
1558
+version = "0.3.10"
1559
+source = "registry+https://github.com/rust-lang/crates.io-index"
1560
+checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
1561
+
1562
+[[package]]
1563
+name = "xkbcommon"
1564
+version = "0.7.0"
1565
+source = "registry+https://github.com/rust-lang/crates.io-index"
1566
+checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e"
1567
+dependencies = [
1568
+ "libc",
1569
+ "memmap2 0.8.0",
1570
+ "xkeysym",
1571
+]
1572
+
1573
+[[package]]
1574
+name = "xkeysym"
1575
+version = "0.2.1"
1576
+source = "registry+https://github.com/rust-lang/crates.io-index"
1577
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
1578
+dependencies = [
1579
+ "bytemuck",
1580
+]
1581
+
1582
+[[package]]
1583
+name = "yasna"
1584
+version = "0.5.2"
1585
+source = "registry+https://github.com/rust-lang/crates.io-index"
1586
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
1587
+dependencies = [
1588
+ "time",
1589
+]
1590
+
1591
+[[package]]
1592
+name = "zeroize"
1593
+version = "1.8.2"
1594
+source = "registry+https://github.com/rust-lang/crates.io-index"
1595
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
1596
+
1597
+[[package]]
1598
+name = "zmij"
1599
+version = "1.0.9"
1600
+source = "registry+https://github.com/rust-lang/crates.io-index"
1601
+checksum = "4ee2a72b10d087f75fb2e1c2c7343e308fe6970527c22a41caf8372e165ff5c1"
Cargo.tomladded
@@ -0,0 +1,57 @@
1
+[workspace]
2
+resolver = "2"
3
+members = [
4
+    "hyprkvm-daemon",
5
+    "hyprkvm-cli",
6
+    "hyprkvm-common",
7
+]
8
+
9
+[workspace.package]
10
+version = "0.1.0"
11
+edition = "2021"
12
+authors = ["tenseleyFlow"]
13
+license = "MIT"
14
+repository = "https://github.com/tenseleyFlow/hyprKVM"
15
+description = "Hyprland-native software KVM switch"
16
+
17
+[workspace.dependencies]
18
+# Async runtime
19
+tokio = { version = "1", features = ["full"] }
20
+
21
+# Serialization
22
+serde = { version = "1", features = ["derive"] }
23
+serde_json = "1"
24
+toml = "0.8"
25
+
26
+# Error handling
27
+thiserror = "1"
28
+anyhow = "1"
29
+
30
+# Logging
31
+tracing = "0.1"
32
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
33
+
34
+# CLI
35
+clap = { version = "4", features = ["derive"] }
36
+
37
+# Wayland
38
+wayland-client = "0.31"
39
+wayland-protocols = { version = "0.31", features = ["client", "unstable"] }
40
+wayland-protocols-wlr = "0.3"
41
+wayland-protocols-misc = { version = "0.3", features = ["client"] }
42
+smithay-client-toolkit = "0.19"
43
+
44
+# TLS
45
+tokio-rustls = "0.26"
46
+rustls = "0.23"
47
+rustls-pemfile = "2"
48
+rcgen = "0.13"
49
+
50
+# Utilities
51
+dirs = "5"
52
+bytes = "1"
53
+hostname = "0.4"
54
+rustix = { version = "0.38", features = ["fs"] }
55
+
56
+# Internal crates
57
+hyprkvm-common = { path = "hyprkvm-common" }
hyprkvm-cli/Cargo.tomladded
@@ -0,0 +1,20 @@
1
+[package]
2
+name = "hyprkvm-cli"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+description = "HyprKVM CLI tool"
8
+
9
+[[bin]]
10
+name = "hyprkvm-ctl"
11
+path = "src/main.rs"
12
+
13
+[dependencies]
14
+hyprkvm-common = { workspace = true }
15
+
16
+tokio = { workspace = true }
17
+serde = { workspace = true }
18
+serde_json = { workspace = true }
19
+clap = { workspace = true }
20
+anyhow = { workspace = true }
hyprkvm-cli/src/main.rsadded
@@ -0,0 +1,57 @@
1
+//! HyprKVM CLI tool
2
+//!
3
+//! Separate CLI for querying daemon status and managing configuration.
4
+
5
+use clap::{Parser, Subcommand};
6
+
7
+#[derive(Parser)]
8
+#[command(name = "hyprkvm-ctl")]
9
+#[command(about = "HyprKVM control utility")]
10
+#[command(version)]
11
+struct Cli {
12
+    #[command(subcommand)]
13
+    command: Commands,
14
+}
15
+
16
+#[derive(Subcommand)]
17
+enum Commands {
18
+    /// Show daemon status
19
+    Status {
20
+        /// Output as JSON
21
+        #[arg(long)]
22
+        json: bool,
23
+    },
24
+
25
+    /// List connected peers
26
+    Peers,
27
+
28
+    /// Ping a peer
29
+    Ping {
30
+        /// Peer name or direction
31
+        peer: String,
32
+    },
33
+}
34
+
35
+#[tokio::main]
36
+async fn main() -> anyhow::Result<()> {
37
+    let cli = Cli::parse();
38
+
39
+    match cli.command {
40
+        Commands::Status { json } => {
41
+            // TODO: Connect to daemon and get status
42
+            if json {
43
+                println!("{{\"status\": \"not_implemented\"}}");
44
+            } else {
45
+                println!("HyprKVM Status: not implemented yet");
46
+            }
47
+        }
48
+        Commands::Peers => {
49
+            println!("Peer listing not implemented yet");
50
+        }
51
+        Commands::Ping { peer } => {
52
+            println!("Ping {} not implemented yet", peer);
53
+        }
54
+    }
55
+
56
+    Ok(())
57
+}
hyprkvm-common/Cargo.tomladded
@@ -0,0 +1,12 @@
1
+[package]
2
+name = "hyprkvm-common"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+description = "Shared types and protocols for HyprKVM"
8
+
9
+[dependencies]
10
+serde = { workspace = true }
11
+serde_json = { workspace = true }
12
+thiserror = { workspace = true }
hyprkvm-common/src/lib.rsadded
@@ -0,0 +1,106 @@
1
+//! HyprKVM Common - Shared types and protocols
2
+//!
3
+//! This crate contains types shared between the daemon, CLI, and GUI.
4
+
5
+pub mod protocol;
6
+
7
+use serde::{Deserialize, Serialize};
8
+
9
+/// Direction relative to the current machine
10
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11
+#[serde(rename_all = "lowercase")]
12
+pub enum Direction {
13
+    Left,
14
+    Right,
15
+    Up,
16
+    Down,
17
+}
18
+
19
+impl Direction {
20
+    /// Get the opposite direction
21
+    pub fn opposite(&self) -> Self {
22
+        match self {
23
+            Direction::Left => Direction::Right,
24
+            Direction::Right => Direction::Left,
25
+            Direction::Up => Direction::Down,
26
+            Direction::Down => Direction::Up,
27
+        }
28
+    }
29
+}
30
+
31
+impl std::fmt::Display for Direction {
32
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33
+        match self {
34
+            Direction::Left => write!(f, "left"),
35
+            Direction::Right => write!(f, "right"),
36
+            Direction::Up => write!(f, "up"),
37
+            Direction::Down => write!(f, "down"),
38
+        }
39
+    }
40
+}
41
+
42
+impl std::str::FromStr for Direction {
43
+    type Err = DirectionParseError;
44
+
45
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
46
+        match s.to_lowercase().as_str() {
47
+            "left" | "l" => Ok(Direction::Left),
48
+            "right" | "r" => Ok(Direction::Right),
49
+            "up" | "u" => Ok(Direction::Up),
50
+            "down" | "d" => Ok(Direction::Down),
51
+            _ => Err(DirectionParseError(s.to_string())),
52
+        }
53
+    }
54
+}
55
+
56
+#[derive(Debug, thiserror::Error)]
57
+#[error("Invalid direction: {0}")]
58
+pub struct DirectionParseError(String);
59
+
60
+/// Modifier key state
61
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
62
+pub struct ModifierState {
63
+    pub shift: bool,
64
+    pub ctrl: bool,
65
+    pub alt: bool,
66
+    pub super_key: bool,
67
+    pub caps_lock: bool,
68
+    pub num_lock: bool,
69
+}
70
+
71
+/// Key state
72
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73
+#[serde(rename_all = "lowercase")]
74
+pub enum KeyState {
75
+    Pressed,
76
+    Released,
77
+}
78
+
79
+/// Mouse button
80
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81
+#[serde(rename_all = "lowercase")]
82
+pub enum MouseButton {
83
+    Left,
84
+    Right,
85
+    Middle,
86
+    Side,
87
+    Extra,
88
+    Forward,
89
+    Back,
90
+}
91
+
92
+/// Button state
93
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94
+#[serde(rename_all = "lowercase")]
95
+pub enum ButtonState {
96
+    Pressed,
97
+    Released,
98
+}
99
+
100
+/// Scroll axis
101
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102
+#[serde(rename_all = "lowercase")]
103
+pub enum ScrollAxis {
104
+    Vertical,
105
+    Horizontal,
106
+}
hyprkvm-common/src/protocol/mod.rsadded
@@ -0,0 +1,204 @@
1
+//! Network protocol definitions for HyprKVM
2
+//!
3
+//! All messages exchanged between HyprKVM daemons are defined here.
4
+
5
+use serde::{Deserialize, Serialize};
6
+
7
+use crate::{Direction, ModifierState};
8
+
9
+/// Protocol version - increment on breaking changes
10
+pub const PROTOCOL_VERSION: u32 = 1;
11
+
12
+/// All possible network messages
13
+#[derive(Debug, Clone, Serialize, Deserialize)]
14
+#[serde(tag = "type", rename_all = "snake_case")]
15
+pub enum Message {
16
+    // Connection lifecycle
17
+    Hello(HelloPayload),
18
+    HelloAck(HelloAckPayload),
19
+    Goodbye,
20
+
21
+    // Topology
22
+    TopologyUpdate(TopologyPayload),
23
+    TopologyAck,
24
+
25
+    // Control transfer
26
+    Enter(EnterPayload),
27
+    EnterAck(EnterAckPayload),
28
+    Leave(LeavePayload),
29
+    LeaveAck,
30
+
31
+    // Input events
32
+    InputEvent(InputEventPayload),
33
+    InputBatch(Vec<InputEventPayload>),
34
+
35
+    // Clipboard
36
+    ClipboardOffer(ClipboardOfferPayload),
37
+    ClipboardRequest(ClipboardRequestPayload),
38
+    ClipboardData(ClipboardDataPayload),
39
+
40
+    // Health
41
+    Ping { timestamp: u64 },
42
+    Pong { timestamp: u64 },
43
+}
44
+
45
+// ============================================================================
46
+// Connection Messages
47
+// ============================================================================
48
+
49
+#[derive(Debug, Clone, Serialize, Deserialize)]
50
+pub struct HelloPayload {
51
+    /// Protocol version
52
+    pub protocol_version: u32,
53
+    /// Machine name
54
+    pub machine_name: String,
55
+    /// Supported capabilities
56
+    pub capabilities: Vec<String>,
57
+}
58
+
59
+#[derive(Debug, Clone, Serialize, Deserialize)]
60
+pub struct HelloAckPayload {
61
+    /// Whether handshake was accepted
62
+    pub accepted: bool,
63
+    /// Protocol version (for negotiation)
64
+    pub protocol_version: u32,
65
+    /// Machine name
66
+    pub machine_name: String,
67
+    /// Error message if not accepted
68
+    pub error: Option<String>,
69
+}
70
+
71
+// ============================================================================
72
+// Topology Messages
73
+// ============================================================================
74
+
75
+#[derive(Debug, Clone, Serialize, Deserialize)]
76
+pub struct TopologyPayload {
77
+    /// This machine's name
78
+    pub machine_name: String,
79
+    /// Known neighbors
80
+    pub neighbors: Vec<NeighborInfo>,
81
+}
82
+
83
+#[derive(Debug, Clone, Serialize, Deserialize)]
84
+pub struct NeighborInfo {
85
+    pub name: String,
86
+    pub direction: Direction,
87
+}
88
+
89
+// ============================================================================
90
+// Control Transfer Messages
91
+// ============================================================================
92
+
93
+#[derive(Debug, Clone, Serialize, Deserialize)]
94
+pub struct EnterPayload {
95
+    /// Direction we're entering from (from sender's perspective)
96
+    pub from_direction: Direction,
97
+    /// Cursor entry position
98
+    pub cursor_pos: CursorEntryPos,
99
+    /// Current modifier state
100
+    pub modifiers: ModifierState,
101
+    /// Unique transfer ID
102
+    pub transfer_id: u64,
103
+}
104
+
105
+#[derive(Debug, Clone, Serialize, Deserialize)]
106
+#[serde(rename_all = "snake_case")]
107
+pub enum CursorEntryPos {
108
+    /// Position along the edge (0.0 = top/left, 1.0 = bottom/right)
109
+    EdgeRelative(f64),
110
+    /// Absolute coordinates
111
+    Absolute { x: i32, y: i32 },
112
+}
113
+
114
+#[derive(Debug, Clone, Serialize, Deserialize)]
115
+pub struct EnterAckPayload {
116
+    /// Whether entry was accepted
117
+    pub success: bool,
118
+    /// Transfer ID for correlation
119
+    pub transfer_id: u64,
120
+    /// Actual cursor position after entry
121
+    pub actual_cursor_pos: Option<(i32, i32)>,
122
+    /// Error message if not success
123
+    pub error: Option<String>,
124
+}
125
+
126
+#[derive(Debug, Clone, Serialize, Deserialize)]
127
+pub struct LeavePayload {
128
+    /// Direction we're leaving towards
129
+    pub to_direction: Direction,
130
+    /// Cursor position for target machine
131
+    pub cursor_pos: CursorEntryPos,
132
+    /// Current modifier state
133
+    pub modifiers: ModifierState,
134
+    /// Transfer ID for correlation
135
+    pub transfer_id: u64,
136
+}
137
+
138
+// ============================================================================
139
+// Input Messages
140
+// ============================================================================
141
+
142
+#[derive(Debug, Clone, Serialize, Deserialize)]
143
+pub struct InputEventPayload {
144
+    /// Sequence number for ordering
145
+    pub sequence: u64,
146
+    /// Timestamp in microseconds
147
+    pub timestamp_us: u64,
148
+    /// The actual event
149
+    pub event: InputEventType,
150
+}
151
+
152
+#[derive(Debug, Clone, Serialize, Deserialize)]
153
+#[serde(tag = "type", rename_all = "snake_case")]
154
+pub enum InputEventType {
155
+    KeyDown { keycode: u32 },
156
+    KeyUp { keycode: u32 },
157
+    ModifierState {
158
+        shift: bool,
159
+        ctrl: bool,
160
+        alt: bool,
161
+        super_key: bool,
162
+    },
163
+    PointerMotion { dx: f64, dy: f64 },
164
+    PointerButton { button: u32, pressed: bool },
165
+    Scroll { horizontal: f64, vertical: f64 },
166
+}
167
+
168
+// ============================================================================
169
+// Clipboard Messages
170
+// ============================================================================
171
+
172
+#[derive(Debug, Clone, Serialize, Deserialize)]
173
+pub struct ClipboardOfferPayload {
174
+    /// Unique ID for this clipboard state
175
+    pub clipboard_id: u64,
176
+    /// Available MIME types
177
+    pub mime_types: Vec<String>,
178
+    /// Size hint in bytes (if known)
179
+    pub size_hint: Option<u64>,
180
+    /// Content hash for deduplication
181
+    pub content_hash: Option<String>,
182
+}
183
+
184
+#[derive(Debug, Clone, Serialize, Deserialize)]
185
+pub struct ClipboardRequestPayload {
186
+    /// Which clipboard to fetch
187
+    pub clipboard_id: u64,
188
+    /// Preferred MIME type
189
+    pub mime_type: String,
190
+}
191
+
192
+#[derive(Debug, Clone, Serialize, Deserialize)]
193
+pub struct ClipboardDataPayload {
194
+    /// Clipboard ID
195
+    pub clipboard_id: u64,
196
+    /// MIME type of data
197
+    pub mime_type: String,
198
+    /// Base64-encoded data
199
+    pub data: String,
200
+    /// Chunk index (if chunked)
201
+    pub chunk_index: Option<u32>,
202
+    /// Total chunks (if chunked)
203
+    pub total_chunks: Option<u32>,
204
+}
hyprkvm-daemon/Cargo.tomladded
@@ -0,0 +1,52 @@
1
+[package]
2
+name = "hyprkvm-daemon"
3
+version.workspace = true
4
+edition.workspace = true
5
+authors.workspace = true
6
+license.workspace = true
7
+description = "HyprKVM daemon - Hyprland-native software KVM switch"
8
+
9
+[[bin]]
10
+name = "hyprkvm"
11
+path = "src/main.rs"
12
+
13
+[dependencies]
14
+hyprkvm-common = { workspace = true }
15
+
16
+# Async runtime
17
+tokio = { workspace = true }
18
+
19
+# Serialization
20
+serde = { workspace = true }
21
+serde_json = { workspace = true }
22
+toml = { workspace = true }
23
+
24
+# Error handling
25
+thiserror = { workspace = true }
26
+anyhow = { workspace = true }
27
+
28
+# Logging
29
+tracing = { workspace = true }
30
+tracing-subscriber = { workspace = true }
31
+
32
+# CLI
33
+clap = { workspace = true }
34
+
35
+# Wayland (for input injection/capture)
36
+wayland-client = { workspace = true }
37
+wayland-protocols = { workspace = true }
38
+wayland-protocols-wlr = { workspace = true }
39
+wayland-protocols-misc = { workspace = true }
40
+smithay-client-toolkit = { workspace = true }
41
+
42
+# TLS
43
+tokio-rustls = { workspace = true }
44
+rustls = { workspace = true }
45
+rustls-pemfile = { workspace = true }
46
+rcgen = { workspace = true }
47
+
48
+# Utilities
49
+dirs = { workspace = true }
50
+bytes = { workspace = true }
51
+hostname = { workspace = true }
52
+rustix = { workspace = true }
hyprkvm-daemon/src/hyprland/edge.rsadded
@@ -0,0 +1,110 @@
1
+//! Edge detection for keyboard navigation
2
+//!
3
+//! Determines when a workspace move would exit local boundaries.
4
+
5
+use hyprkvm_common::Direction;
6
+
7
+use super::layout::MonitorLayout;
8
+
9
+/// Result of checking a keyboard move
10
+#[derive(Debug, Clone)]
11
+pub enum KeyboardEdgeResult {
12
+    /// Move is possible within local Hyprland
13
+    LocalMove,
14
+    /// At edge, should trigger network switch
15
+    NetworkEdge { direction: Direction },
16
+}
17
+
18
+/// Keyboard edge detector
19
+pub struct KeyboardEdgeDetector {
20
+    layout: MonitorLayout,
21
+}
22
+
23
+impl KeyboardEdgeDetector {
24
+    /// Create a new detector with the given layout
25
+    pub fn new(layout: MonitorLayout) -> Self {
26
+        Self { layout }
27
+    }
28
+
29
+    /// Update the layout
30
+    pub fn update_layout(&mut self, layout: MonitorLayout) {
31
+        self.layout = layout;
32
+    }
33
+
34
+    /// Check if a move in the given direction would exit local boundaries
35
+    pub fn check_move(&self, direction: Direction) -> KeyboardEdgeResult {
36
+        // If there's a neighbor monitor in that direction, it's a local move
37
+        if self.layout.neighbor(direction).is_some() {
38
+            return KeyboardEdgeResult::LocalMove;
39
+        }
40
+
41
+        // No local neighbor - this is an edge
42
+        KeyboardEdgeResult::NetworkEdge { direction }
43
+    }
44
+
45
+    /// Get reference to the layout
46
+    pub fn layout(&self) -> &MonitorLayout {
47
+        &self.layout
48
+    }
49
+}
50
+
51
+#[cfg(test)]
52
+mod tests {
53
+    use super::*;
54
+    use crate::hyprland::ipc::{Monitor, WorkspaceRef};
55
+
56
+    fn make_monitor(name: &str, x: i32, y: i32, width: u32, height: u32, focused: bool) -> Monitor {
57
+        Monitor {
58
+            id: 0,
59
+            name: name.to_string(),
60
+            description: String::new(),
61
+            x,
62
+            y,
63
+            width,
64
+            height,
65
+            scale: 1.0,
66
+            active_workspace: WorkspaceRef {
67
+                id: 1,
68
+                name: "1".to_string(),
69
+            },
70
+            focused,
71
+        }
72
+    }
73
+
74
+    #[test]
75
+    fn test_single_monitor_all_edges() {
76
+        let monitors = vec![make_monitor("DP-1", 0, 0, 1920, 1080, true)];
77
+        let layout = MonitorLayout::from_monitors(&monitors);
78
+        let detector = KeyboardEdgeDetector::new(layout);
79
+
80
+        // All directions should be network edges
81
+        assert!(matches!(
82
+            detector.check_move(Direction::Left),
83
+            KeyboardEdgeResult::NetworkEdge { direction: Direction::Left }
84
+        ));
85
+        assert!(matches!(
86
+            detector.check_move(Direction::Right),
87
+            KeyboardEdgeResult::NetworkEdge { direction: Direction::Right }
88
+        ));
89
+    }
90
+
91
+    #[test]
92
+    fn test_dual_monitor_horizontal() {
93
+        let monitors = vec![
94
+            make_monitor("DP-1", 0, 0, 1920, 1080, true),
95
+            make_monitor("DP-2", 1920, 0, 1920, 1080, false),
96
+        ];
97
+        let layout = MonitorLayout::from_monitors(&monitors);
98
+        let detector = KeyboardEdgeDetector::new(layout);
99
+
100
+        // Left should be network edge, right should be local
101
+        assert!(matches!(
102
+            detector.check_move(Direction::Left),
103
+            KeyboardEdgeResult::NetworkEdge { .. }
104
+        ));
105
+        assert!(matches!(
106
+            detector.check_move(Direction::Right),
107
+            KeyboardEdgeResult::LocalMove
108
+        ));
109
+    }
110
+}
hyprkvm-daemon/src/hyprland/events.rsadded
@@ -0,0 +1,244 @@
1
+//! Hyprland event stream
2
+//!
3
+//! Listens to Hyprland's event socket for real-time updates.
4
+
5
+use std::path::PathBuf;
6
+
7
+use tokio::io::{AsyncBufReadExt, BufReader};
8
+use tokio::net::UnixStream;
9
+
10
+use super::ipc::HyprlandError;
11
+
12
+/// Stream of Hyprland events
13
+pub struct HyprlandEventStream {
14
+    reader: BufReader<UnixStream>,
15
+}
16
+
17
+impl HyprlandEventStream {
18
+    /// Connect to Hyprland's event socket
19
+    pub async fn connect() -> Result<Self, HyprlandError> {
20
+        let socket_path = Self::discover_event_socket_path()?;
21
+
22
+        let stream = UnixStream::connect(&socket_path)
23
+            .await
24
+            .map_err(|e| HyprlandError::Connection(e.to_string()))?;
25
+
26
+        Ok(Self {
27
+            reader: BufReader::new(stream),
28
+        })
29
+    }
30
+
31
+    /// Discover the event socket path (.socket2.sock)
32
+    fn discover_event_socket_path() -> Result<PathBuf, HyprlandError> {
33
+        let signature = std::env::var("HYPRLAND_INSTANCE_SIGNATURE")
34
+            .map_err(|_| HyprlandError::NotRunning)?;
35
+
36
+        let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
37
+            .unwrap_or_else(|_| "/tmp".to_string());
38
+
39
+        let socket_path = PathBuf::from(runtime_dir)
40
+            .join("hypr")
41
+            .join(&signature)
42
+            .join(".socket2.sock");
43
+
44
+        if !socket_path.exists() {
45
+            return Err(HyprlandError::SocketNotFound(
46
+                socket_path.to_string_lossy().to_string(),
47
+            ));
48
+        }
49
+
50
+        Ok(socket_path)
51
+    }
52
+
53
+    /// Get the next event
54
+    pub async fn next_event(&mut self) -> Result<HyprlandEvent, HyprlandError> {
55
+        let mut line = String::new();
56
+
57
+        self.reader
58
+            .read_line(&mut line)
59
+            .await
60
+            .map_err(|e| HyprlandError::Io(e.to_string()))?;
61
+
62
+        if line.is_empty() {
63
+            return Err(HyprlandError::Connection("Socket closed".to_string()));
64
+        }
65
+
66
+        // Remove trailing newline
67
+        let line = line.trim_end();
68
+
69
+        // Parse event format: EVENT>>DATA
70
+        if let Some((event_name, data)) = line.split_once(">>") {
71
+            Ok(HyprlandEvent::parse(event_name, data))
72
+        } else {
73
+            Ok(HyprlandEvent::Unknown {
74
+                raw: line.to_string(),
75
+            })
76
+        }
77
+    }
78
+}
79
+
80
+/// Hyprland events
81
+#[derive(Debug, Clone)]
82
+pub enum HyprlandEvent {
83
+    /// Workspace changed
84
+    WorkspaceChanged {
85
+        name: String,
86
+    },
87
+
88
+    /// Focused monitor changed
89
+    FocusedMonitorChanged {
90
+        monitor: String,
91
+        workspace: String,
92
+    },
93
+
94
+    /// Active window changed
95
+    ActiveWindowChanged {
96
+        class: String,
97
+        title: String,
98
+    },
99
+
100
+    /// Active window address changed (v2)
101
+    ActiveWindowV2 {
102
+        address: String,
103
+    },
104
+
105
+    /// Monitor added
106
+    MonitorAdded {
107
+        name: String,
108
+    },
109
+
110
+    /// Monitor removed
111
+    MonitorRemoved {
112
+        name: String,
113
+    },
114
+
115
+    /// Workspace created
116
+    WorkspaceCreated {
117
+        name: String,
118
+    },
119
+
120
+    /// Workspace destroyed
121
+    WorkspaceDestroyed {
122
+        name: String,
123
+    },
124
+
125
+    /// Workspace moved to monitor
126
+    WorkspaceMoved {
127
+        workspace: String,
128
+        monitor: String,
129
+    },
130
+
131
+    /// Fullscreen state changed
132
+    FullscreenChanged {
133
+        fullscreen: bool,
134
+    },
135
+
136
+    /// Window opened
137
+    WindowOpened {
138
+        address: String,
139
+        workspace: String,
140
+        class: String,
141
+        title: String,
142
+    },
143
+
144
+    /// Window closed
145
+    WindowClosed {
146
+        address: String,
147
+    },
148
+
149
+    /// Window moved
150
+    WindowMoved {
151
+        address: String,
152
+        workspace: String,
153
+    },
154
+
155
+    /// Unknown event
156
+    Unknown {
157
+        raw: String,
158
+    },
159
+}
160
+
161
+impl HyprlandEvent {
162
+    /// Parse an event from name and data
163
+    fn parse(name: &str, data: &str) -> Self {
164
+        match name {
165
+            "workspace" => HyprlandEvent::WorkspaceChanged {
166
+                name: data.to_string(),
167
+            },
168
+
169
+            "focusedmon" => {
170
+                let parts: Vec<&str> = data.splitn(2, ',').collect();
171
+                HyprlandEvent::FocusedMonitorChanged {
172
+                    monitor: parts.get(0).unwrap_or(&"").to_string(),
173
+                    workspace: parts.get(1).unwrap_or(&"").to_string(),
174
+                }
175
+            }
176
+
177
+            "activewindow" => {
178
+                let parts: Vec<&str> = data.splitn(2, ',').collect();
179
+                HyprlandEvent::ActiveWindowChanged {
180
+                    class: parts.get(0).unwrap_or(&"").to_string(),
181
+                    title: parts.get(1).unwrap_or(&"").to_string(),
182
+                }
183
+            }
184
+
185
+            "activewindowv2" => HyprlandEvent::ActiveWindowV2 {
186
+                address: data.to_string(),
187
+            },
188
+
189
+            "monitoradded" => HyprlandEvent::MonitorAdded {
190
+                name: data.to_string(),
191
+            },
192
+
193
+            "monitorremoved" => HyprlandEvent::MonitorRemoved {
194
+                name: data.to_string(),
195
+            },
196
+
197
+            "createworkspace" => HyprlandEvent::WorkspaceCreated {
198
+                name: data.to_string(),
199
+            },
200
+
201
+            "destroyworkspace" => HyprlandEvent::WorkspaceDestroyed {
202
+                name: data.to_string(),
203
+            },
204
+
205
+            "moveworkspace" => {
206
+                let parts: Vec<&str> = data.splitn(2, ',').collect();
207
+                HyprlandEvent::WorkspaceMoved {
208
+                    workspace: parts.get(0).unwrap_or(&"").to_string(),
209
+                    monitor: parts.get(1).unwrap_or(&"").to_string(),
210
+                }
211
+            }
212
+
213
+            "fullscreen" => HyprlandEvent::FullscreenChanged {
214
+                fullscreen: data == "1",
215
+            },
216
+
217
+            "openwindow" => {
218
+                let parts: Vec<&str> = data.splitn(4, ',').collect();
219
+                HyprlandEvent::WindowOpened {
220
+                    address: parts.get(0).unwrap_or(&"").to_string(),
221
+                    workspace: parts.get(1).unwrap_or(&"").to_string(),
222
+                    class: parts.get(2).unwrap_or(&"").to_string(),
223
+                    title: parts.get(3).unwrap_or(&"").to_string(),
224
+                }
225
+            }
226
+
227
+            "closewindow" => HyprlandEvent::WindowClosed {
228
+                address: data.to_string(),
229
+            },
230
+
231
+            "movewindow" => {
232
+                let parts: Vec<&str> = data.splitn(2, ',').collect();
233
+                HyprlandEvent::WindowMoved {
234
+                    address: parts.get(0).unwrap_or(&"").to_string(),
235
+                    workspace: parts.get(1).unwrap_or(&"").to_string(),
236
+                }
237
+            }
238
+
239
+            _ => HyprlandEvent::Unknown {
240
+                raw: format!("{}>>{}",name, data),
241
+            },
242
+        }
243
+    }
244
+}
hyprkvm-daemon/src/hyprland/ipc.rsadded
@@ -0,0 +1,191 @@
1
+//! Hyprland IPC client
2
+//!
3
+//! Communicates with Hyprland via its Unix socket.
4
+
5
+use std::path::PathBuf;
6
+
7
+use serde::de::DeserializeOwned;
8
+use serde::{Deserialize, Serialize};
9
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
10
+use tokio::net::UnixStream;
11
+
12
+/// Hyprland IPC client
13
+pub struct HyprlandClient {
14
+    socket_path: PathBuf,
15
+}
16
+
17
+impl HyprlandClient {
18
+    /// Create a new Hyprland client
19
+    ///
20
+    /// Discovers the socket path from environment variables.
21
+    pub async fn new() -> Result<Self, HyprlandError> {
22
+        let socket_path = Self::discover_socket_path()?;
23
+
24
+        // Verify we can connect
25
+        let _ = UnixStream::connect(&socket_path)
26
+            .await
27
+            .map_err(|e| HyprlandError::Connection(e.to_string()))?;
28
+
29
+        Ok(Self { socket_path })
30
+    }
31
+
32
+    /// Discover the Hyprland socket path
33
+    fn discover_socket_path() -> Result<PathBuf, HyprlandError> {
34
+        let signature = std::env::var("HYPRLAND_INSTANCE_SIGNATURE")
35
+            .map_err(|_| HyprlandError::NotRunning)?;
36
+
37
+        let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
38
+            .unwrap_or_else(|_| "/tmp".to_string());
39
+
40
+        let socket_path = PathBuf::from(runtime_dir)
41
+            .join("hypr")
42
+            .join(&signature)
43
+            .join(".socket.sock");
44
+
45
+        if !socket_path.exists() {
46
+            return Err(HyprlandError::SocketNotFound(
47
+                socket_path.to_string_lossy().to_string(),
48
+            ));
49
+        }
50
+
51
+        Ok(socket_path)
52
+    }
53
+
54
+    /// Execute a command and get the response
55
+    async fn execute(&self, command: &str) -> Result<String, HyprlandError> {
56
+        let mut stream = UnixStream::connect(&self.socket_path)
57
+            .await
58
+            .map_err(|e| HyprlandError::Connection(e.to_string()))?;
59
+
60
+        stream
61
+            .write_all(command.as_bytes())
62
+            .await
63
+            .map_err(|e| HyprlandError::Io(e.to_string()))?;
64
+
65
+        stream
66
+            .shutdown()
67
+            .await
68
+            .map_err(|e| HyprlandError::Io(e.to_string()))?;
69
+
70
+        let mut response = String::new();
71
+        stream
72
+            .read_to_string(&mut response)
73
+            .await
74
+            .map_err(|e| HyprlandError::Io(e.to_string()))?;
75
+
76
+        Ok(response)
77
+    }
78
+
79
+    /// Execute a command and parse JSON response
80
+    pub async fn query<T: DeserializeOwned>(&self, command: &str) -> Result<T, HyprlandError> {
81
+        // Prefix with j/ for JSON output
82
+        let json_command = format!("j/{}", command);
83
+        let response = self.execute(&json_command).await?;
84
+
85
+        serde_json::from_str(&response)
86
+            .map_err(|e| HyprlandError::Parse(format!("{}: {}", e, response)))
87
+    }
88
+
89
+    /// Get all monitors
90
+    pub async fn monitors(&self) -> Result<Vec<Monitor>, HyprlandError> {
91
+        self.query("monitors").await
92
+    }
93
+
94
+    /// Get all workspaces
95
+    pub async fn workspaces(&self) -> Result<Vec<Workspace>, HyprlandError> {
96
+        self.query("workspaces").await
97
+    }
98
+
99
+    /// Get active workspace
100
+    pub async fn active_workspace(&self) -> Result<Workspace, HyprlandError> {
101
+        self.query("activeworkspace").await
102
+    }
103
+
104
+    /// Get cursor position
105
+    pub async fn cursor_pos(&self) -> Result<CursorPos, HyprlandError> {
106
+        self.query("cursorpos").await
107
+    }
108
+
109
+    /// Execute a dispatcher command
110
+    pub async fn dispatch(&self, dispatcher: &str, args: &str) -> Result<(), HyprlandError> {
111
+        let command = format!("dispatch {} {}", dispatcher, args);
112
+        let response = self.execute(&command).await?;
113
+
114
+        // Check for error in response
115
+        if response.starts_with("err") || response.contains("error") {
116
+            return Err(HyprlandError::Dispatch(response));
117
+        }
118
+
119
+        Ok(())
120
+    }
121
+}
122
+
123
+// ============================================================================
124
+// Data Types
125
+// ============================================================================
126
+
127
+#[derive(Debug, Clone, Serialize, Deserialize)]
128
+pub struct Monitor {
129
+    pub id: i32,
130
+    pub name: String,
131
+    pub description: String,
132
+    pub x: i32,
133
+    pub y: i32,
134
+    pub width: u32,
135
+    pub height: u32,
136
+    pub scale: f32,
137
+    #[serde(rename = "activeWorkspace")]
138
+    pub active_workspace: WorkspaceRef,
139
+    pub focused: bool,
140
+}
141
+
142
+#[derive(Debug, Clone, Serialize, Deserialize)]
143
+pub struct WorkspaceRef {
144
+    pub id: i32,
145
+    pub name: String,
146
+}
147
+
148
+#[derive(Debug, Clone, Serialize, Deserialize)]
149
+pub struct Workspace {
150
+    pub id: i32,
151
+    pub name: String,
152
+    pub monitor: String,
153
+    pub windows: u32,
154
+    #[serde(rename = "hasfullscreen")]
155
+    pub has_fullscreen: bool,
156
+    #[serde(rename = "lastwindow")]
157
+    pub last_window: String,
158
+    #[serde(rename = "lastwindowtitle")]
159
+    pub last_window_title: String,
160
+}
161
+
162
+#[derive(Debug, Clone, Serialize, Deserialize)]
163
+pub struct CursorPos {
164
+    pub x: i32,
165
+    pub y: i32,
166
+}
167
+
168
+// ============================================================================
169
+// Errors
170
+// ============================================================================
171
+
172
+#[derive(Debug, thiserror::Error)]
173
+pub enum HyprlandError {
174
+    #[error("Hyprland is not running (HYPRLAND_INSTANCE_SIGNATURE not set)")]
175
+    NotRunning,
176
+
177
+    #[error("Hyprland socket not found: {0}")]
178
+    SocketNotFound(String),
179
+
180
+    #[error("Connection error: {0}")]
181
+    Connection(String),
182
+
183
+    #[error("IO error: {0}")]
184
+    Io(String),
185
+
186
+    #[error("Parse error: {0}")]
187
+    Parse(String),
188
+
189
+    #[error("Dispatch error: {0}")]
190
+    Dispatch(String),
191
+}
hyprkvm-daemon/src/hyprland/layout.rsadded
@@ -0,0 +1,301 @@
1
+//! Monitor layout management
2
+//!
3
+//! Tracks monitor geometry and calculates adjacency relationships.
4
+
5
+use std::collections::HashMap;
6
+
7
+use hyprkvm_common::Direction;
8
+
9
+use super::ipc::{HyprlandClient, HyprlandError, Monitor};
10
+
11
+/// Manages the monitor layout
12
+pub struct MonitorLayout {
13
+    monitors: HashMap<String, MonitorInfo>,
14
+}
15
+
16
+/// Monitor information with computed adjacency
17
+#[derive(Debug, Clone)]
18
+pub struct MonitorInfo {
19
+    pub name: String,
20
+    pub x: i32,
21
+    pub y: i32,
22
+    pub width: u32,
23
+    pub height: u32,
24
+    pub scale: f32,
25
+    pub active_workspace_id: i32,
26
+    pub active_workspace_name: String,
27
+    pub focused: bool,
28
+    pub adjacency: Adjacency,
29
+}
30
+
31
+/// Adjacent monitors in each direction
32
+#[derive(Debug, Clone, Default)]
33
+pub struct Adjacency {
34
+    pub left: Option<String>,
35
+    pub right: Option<String>,
36
+    pub up: Option<String>,
37
+    pub down: Option<String>,
38
+}
39
+
40
+impl MonitorLayout {
41
+    /// Build layout from current Hyprland state
42
+    pub async fn from_hyprland(client: &HyprlandClient) -> Result<Self, HyprlandError> {
43
+        let monitors = client.monitors().await?;
44
+        Ok(Self::from_monitors(&monitors))
45
+    }
46
+
47
+    /// Build layout from monitor list
48
+    pub fn from_monitors(monitors: &[Monitor]) -> Self {
49
+        let mut layout = Self {
50
+            monitors: HashMap::new(),
51
+        };
52
+
53
+        // First pass: add all monitors
54
+        for mon in monitors {
55
+            layout.monitors.insert(
56
+                mon.name.clone(),
57
+                MonitorInfo {
58
+                    name: mon.name.clone(),
59
+                    x: mon.x,
60
+                    y: mon.y,
61
+                    width: mon.width,
62
+                    height: mon.height,
63
+                    scale: mon.scale,
64
+                    active_workspace_id: mon.active_workspace.id,
65
+                    active_workspace_name: mon.active_workspace.name.clone(),
66
+                    focused: mon.focused,
67
+                    adjacency: Adjacency::default(),
68
+                },
69
+            );
70
+        }
71
+
72
+        // Second pass: compute adjacency
73
+        let names: Vec<String> = layout.monitors.keys().cloned().collect();
74
+        for name in &names {
75
+            let adjacency = layout.compute_adjacency(name);
76
+            if let Some(mon) = layout.monitors.get_mut(name) {
77
+                mon.adjacency = adjacency;
78
+            }
79
+        }
80
+
81
+        layout
82
+    }
83
+
84
+    /// Compute adjacency for a monitor
85
+    fn compute_adjacency(&self, name: &str) -> Adjacency {
86
+        let Some(current) = self.monitors.get(name) else {
87
+            return Adjacency::default();
88
+        };
89
+
90
+        let mut adjacency = Adjacency::default();
91
+
92
+        for (other_name, other) in &self.monitors {
93
+            if other_name == name {
94
+                continue;
95
+            }
96
+
97
+            // Check if monitors are vertically aligned (overlap in Y axis)
98
+            let v_overlap = current.y < other.y + other.height as i32
99
+                && current.y + current.height as i32 > other.y;
100
+
101
+            // Check if monitors are horizontally aligned (overlap in X axis)
102
+            let h_overlap = current.x < other.x + other.width as i32
103
+                && current.x + current.width as i32 > other.x;
104
+
105
+            // Left neighbor: other is to the left and vertically aligned
106
+            if v_overlap && other.x + other.width as i32 <= current.x {
107
+                if adjacency.left.is_none()
108
+                    || self.is_closer_left(current, other, &adjacency.left)
109
+                {
110
+                    adjacency.left = Some(other_name.clone());
111
+                }
112
+            }
113
+
114
+            // Right neighbor: other is to the right and vertically aligned
115
+            if v_overlap && other.x >= current.x + current.width as i32 {
116
+                if adjacency.right.is_none()
117
+                    || self.is_closer_right(current, other, &adjacency.right)
118
+                {
119
+                    adjacency.right = Some(other_name.clone());
120
+                }
121
+            }
122
+
123
+            // Up neighbor: other is above and horizontally aligned
124
+            if h_overlap && other.y + other.height as i32 <= current.y {
125
+                if adjacency.up.is_none()
126
+                    || self.is_closer_up(current, other, &adjacency.up)
127
+                {
128
+                    adjacency.up = Some(other_name.clone());
129
+                }
130
+            }
131
+
132
+            // Down neighbor: other is below and horizontally aligned
133
+            if h_overlap && other.y >= current.y + current.height as i32 {
134
+                if adjacency.down.is_none()
135
+                    || self.is_closer_down(current, other, &adjacency.down)
136
+                {
137
+                    adjacency.down = Some(other_name.clone());
138
+                }
139
+            }
140
+        }
141
+
142
+        adjacency
143
+    }
144
+
145
+    // Helper functions to find closest neighbor
146
+    fn is_closer_left(&self, current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
147
+        let Some(existing_name) = existing else { return true };
148
+        let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
149
+        other.x + other.width as i32 > existing_mon.x + existing_mon.width as i32
150
+    }
151
+
152
+    fn is_closer_right(&self, current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
153
+        let Some(existing_name) = existing else { return true };
154
+        let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
155
+        other.x < existing_mon.x
156
+    }
157
+
158
+    fn is_closer_up(&self, current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
159
+        let Some(existing_name) = existing else { return true };
160
+        let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
161
+        other.y + other.height as i32 > existing_mon.y + existing_mon.height as i32
162
+    }
163
+
164
+    fn is_closer_down(&self, current: &MonitorInfo, other: &MonitorInfo, existing: &Option<String>) -> bool {
165
+        let Some(existing_name) = existing else { return true };
166
+        let Some(existing_mon) = self.monitors.get(existing_name) else { return true };
167
+        other.y < existing_mon.y
168
+    }
169
+
170
+    /// Get the focused monitor
171
+    pub fn focused(&self) -> Option<&MonitorInfo> {
172
+        self.monitors.values().find(|m| m.focused)
173
+    }
174
+
175
+    /// Get a monitor by name
176
+    pub fn get(&self, name: &str) -> Option<&MonitorInfo> {
177
+        self.monitors.get(name)
178
+    }
179
+
180
+    /// Get monitor in given direction from focused monitor
181
+    pub fn neighbor(&self, direction: Direction) -> Option<&MonitorInfo> {
182
+        let focused = self.focused()?;
183
+        let neighbor_name = match direction {
184
+            Direction::Left => focused.adjacency.left.as_ref()?,
185
+            Direction::Right => focused.adjacency.right.as_ref()?,
186
+            Direction::Up => focused.adjacency.up.as_ref()?,
187
+            Direction::Down => focused.adjacency.down.as_ref()?,
188
+        };
189
+        self.monitors.get(neighbor_name)
190
+    }
191
+
192
+    /// Check if we're at an edge (no local monitor in that direction)
193
+    pub fn is_at_edge(&self, direction: Direction) -> bool {
194
+        self.neighbor(direction).is_none()
195
+    }
196
+
197
+    /// Get screen bounds (bounding box of all monitors)
198
+    pub fn bounds(&self) -> (i32, i32, i32, i32) {
199
+        let mut min_x = i32::MAX;
200
+        let mut min_y = i32::MAX;
201
+        let mut max_x = i32::MIN;
202
+        let mut max_y = i32::MIN;
203
+
204
+        for mon in self.monitors.values() {
205
+            min_x = min_x.min(mon.x);
206
+            min_y = min_y.min(mon.y);
207
+            max_x = max_x.max(mon.x + mon.width as i32);
208
+            max_y = max_y.max(mon.y + mon.height as i32);
209
+        }
210
+
211
+        (min_x, min_y, max_x, max_y)
212
+    }
213
+
214
+    /// Check if a cursor position is at a screen edge
215
+    pub fn cursor_at_edge(&self, x: i32, y: i32, threshold: i32) -> Option<Direction> {
216
+        let (min_x, min_y, max_x, max_y) = self.bounds();
217
+
218
+        if x <= min_x + threshold {
219
+            Some(Direction::Left)
220
+        } else if x >= max_x - threshold {
221
+            Some(Direction::Right)
222
+        } else if y <= min_y + threshold {
223
+            Some(Direction::Up)
224
+        } else if y >= max_y - threshold {
225
+            Some(Direction::Down)
226
+        } else {
227
+            None
228
+        }
229
+    }
230
+
231
+    /// Get all monitor names
232
+    pub fn monitor_names(&self) -> impl Iterator<Item = &String> {
233
+        self.monitors.keys()
234
+    }
235
+
236
+    /// Get number of monitors
237
+    pub fn len(&self) -> usize {
238
+        self.monitors.len()
239
+    }
240
+
241
+    /// Check if empty
242
+    pub fn is_empty(&self) -> bool {
243
+        self.monitors.is_empty()
244
+    }
245
+}
246
+
247
+#[cfg(test)]
248
+mod tests {
249
+    use super::*;
250
+    use crate::hyprland::ipc::WorkspaceRef;
251
+
252
+    fn make_monitor(name: &str, x: i32, y: i32, width: u32, height: u32, focused: bool) -> Monitor {
253
+        Monitor {
254
+            id: 0,
255
+            name: name.to_string(),
256
+            description: String::new(),
257
+            x,
258
+            y,
259
+            width,
260
+            height,
261
+            scale: 1.0,
262
+            active_workspace: WorkspaceRef {
263
+                id: 1,
264
+                name: "1".to_string(),
265
+            },
266
+            focused,
267
+        }
268
+    }
269
+
270
+    #[test]
271
+    fn test_horizontal_layout() {
272
+        let monitors = vec![
273
+            make_monitor("DP-1", 0, 0, 1920, 1080, true),
274
+            make_monitor("DP-2", 1920, 0, 1920, 1080, false),
275
+        ];
276
+
277
+        let layout = MonitorLayout::from_monitors(&monitors);
278
+
279
+        let dp1 = layout.get("DP-1").unwrap();
280
+        assert!(dp1.adjacency.left.is_none());
281
+        assert_eq!(dp1.adjacency.right, Some("DP-2".to_string()));
282
+
283
+        let dp2 = layout.get("DP-2").unwrap();
284
+        assert_eq!(dp2.adjacency.left, Some("DP-1".to_string()));
285
+        assert!(dp2.adjacency.right.is_none());
286
+    }
287
+
288
+    #[test]
289
+    fn test_edge_detection() {
290
+        let monitors = vec![
291
+            make_monitor("DP-1", 0, 0, 1920, 1080, true),
292
+        ];
293
+
294
+        let layout = MonitorLayout::from_monitors(&monitors);
295
+
296
+        assert!(layout.is_at_edge(Direction::Left));
297
+        assert!(layout.is_at_edge(Direction::Right));
298
+        assert!(layout.is_at_edge(Direction::Up));
299
+        assert!(layout.is_at_edge(Direction::Down));
300
+    }
301
+}
hyprkvm-daemon/src/hyprland/mod.rsadded
@@ -0,0 +1,11 @@
1
+//! Hyprland integration module
2
+//!
3
+//! Handles communication with Hyprland via IPC sockets.
4
+
5
+pub mod ipc;
6
+pub mod events;
7
+pub mod layout;
8
+pub mod edge;
9
+
10
+pub use ipc::HyprlandClient;
11
+pub use events::HyprlandEventStream;
hyprkvm-daemon/src/main.rsadded
@@ -0,0 +1,251 @@
1
+//! HyprKVM Daemon - Main entry point
2
+//!
3
+//! The daemon handles:
4
+//! - Hyprland IPC communication
5
+//! - Edge detection (mouse and keyboard)
6
+//! - Network connections to peer machines
7
+//! - Input capture and injection
8
+
9
+use clap::{Parser, Subcommand};
10
+use tracing::{info, Level};
11
+use tracing_subscriber::FmtSubscriber;
12
+
13
+mod config;
14
+mod hyprland;
15
+mod input;
16
+mod network;
17
+mod state;
18
+mod transfer;
19
+
20
+use config::Config;
21
+
22
+#[derive(Parser)]
23
+#[command(name = "hyprkvm")]
24
+#[command(about = "Hyprland-native software KVM switch")]
25
+#[command(version)]
26
+struct Cli {
27
+    /// Config file path
28
+    #[arg(short, long)]
29
+    config: Option<std::path::PathBuf>,
30
+
31
+    /// Increase log verbosity (-v, -vv, -vvv)
32
+    #[arg(short, long, action = clap::ArgAction::Count)]
33
+    verbose: u8,
34
+
35
+    #[command(subcommand)]
36
+    command: Commands,
37
+}
38
+
39
+#[derive(Subcommand)]
40
+enum Commands {
41
+    /// Start the HyprKVM daemon
42
+    Daemon,
43
+
44
+    /// Show daemon status
45
+    Status,
46
+
47
+    /// Handle a move request (called by keybinding script)
48
+    Move {
49
+        /// Direction to move
50
+        direction: String,
51
+    },
52
+
53
+    /// Configuration management
54
+    Config {
55
+        #[command(subcommand)]
56
+        action: ConfigAction,
57
+    },
58
+}
59
+
60
+#[derive(Subcommand)]
61
+enum ConfigAction {
62
+    /// Show current configuration
63
+    Show,
64
+    /// Reload configuration
65
+    Reload,
66
+}
67
+
68
+#[tokio::main]
69
+async fn main() -> anyhow::Result<()> {
70
+    let cli = Cli::parse();
71
+
72
+    // Set up logging
73
+    let log_level = match cli.verbose {
74
+        0 => Level::INFO,
75
+        1 => Level::DEBUG,
76
+        _ => Level::TRACE,
77
+    };
78
+
79
+    let subscriber = FmtSubscriber::builder()
80
+        .with_max_level(log_level)
81
+        .with_target(false)
82
+        .init();
83
+
84
+    // Load configuration
85
+    let config_path = cli.config.unwrap_or_else(|| {
86
+        dirs::config_dir()
87
+            .unwrap_or_else(|| std::path::PathBuf::from("."))
88
+            .join("hyprkvm")
89
+            .join("hyprkvm.toml")
90
+    });
91
+
92
+    match cli.command {
93
+        Commands::Daemon => {
94
+            info!("Starting HyprKVM daemon...");
95
+            run_daemon(&config_path).await
96
+        }
97
+        Commands::Status => {
98
+            show_status().await
99
+        }
100
+        Commands::Move { direction } => {
101
+            handle_move(&direction).await
102
+        }
103
+        Commands::Config { action } => {
104
+            match action {
105
+                ConfigAction::Show => show_config(&config_path),
106
+                ConfigAction::Reload => reload_config().await,
107
+            }
108
+        }
109
+    }
110
+}
111
+
112
+async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
113
+    // Load or create default config
114
+    let config = match Config::load(config_path) {
115
+        Ok(cfg) => cfg,
116
+        Err(e) => {
117
+            tracing::warn!("Failed to load config: {e}, using defaults");
118
+            Config::default()
119
+        }
120
+    };
121
+
122
+    info!("Machine name: {}", config.machines.self_name);
123
+    info!("Listening on port: {}", config.network.listen_port);
124
+
125
+    // Connect to Hyprland
126
+    info!("Connecting to Hyprland...");
127
+    let hypr_client = hyprland::ipc::HyprlandClient::new().await?;
128
+
129
+    // Query monitors to validate connection
130
+    let monitors = hypr_client.monitors().await?;
131
+    info!("Connected to Hyprland. Monitors: {}", monitors.len());
132
+    for mon in &monitors {
133
+        info!("  {} at ({}, {}) {}x{}", mon.name, mon.x, mon.y, mon.width, mon.height);
134
+    }
135
+
136
+    // Determine which edges have network neighbors
137
+    let mut enabled_edges = Vec::new();
138
+    for neighbor in &config.machines.neighbors {
139
+        enabled_edges.push(neighbor.direction);
140
+        info!("  Network neighbor: {} ({})", neighbor.name, neighbor.direction);
141
+    }
142
+
143
+    // If no neighbors configured, enable all edges for testing
144
+    if enabled_edges.is_empty() {
145
+        info!("No neighbors configured, enabling all edges for testing");
146
+        enabled_edges = vec![
147
+            hyprkvm_common::Direction::Left,
148
+            hyprkvm_common::Direction::Right,
149
+        ];
150
+    }
151
+
152
+    // Start edge capture
153
+    info!("Starting edge capture for: {:?}", enabled_edges);
154
+    let edge_capture = input::EdgeCapture::new(input::EdgeCaptureConfig {
155
+        barrier_size: 1,
156
+        enabled_edges,
157
+    })?;
158
+
159
+    // Listen for Hyprland events
160
+    let mut event_stream = hyprland::events::HyprlandEventStream::connect().await?;
161
+
162
+    info!("Daemon running. Move mouse to screen edges to test. Press Ctrl+C to stop.");
163
+
164
+    loop {
165
+        tokio::select! {
166
+            // Check for edge events (non-blocking via channel)
167
+            _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
168
+                while let Some(edge_event) = edge_capture.try_recv() {
169
+                    info!(
170
+                        "EDGE EVENT: {:?} at ({}, {})",
171
+                        edge_event.direction,
172
+                        edge_event.position.0,
173
+                        edge_event.position.1
174
+                    );
175
+
176
+                    // TODO: Sprint 4 - Trigger network switch
177
+                    // For now, just log it
178
+                }
179
+            }
180
+
181
+            // Hyprland events
182
+            event = event_stream.next_event() => {
183
+                match event {
184
+                    Ok(evt) => {
185
+                        tracing::debug!("Hyprland event: {:?}", evt);
186
+                    }
187
+                    Err(e) => {
188
+                        tracing::error!("Event error: {e}");
189
+                        break;
190
+                    }
191
+                }
192
+            }
193
+
194
+            // Shutdown
195
+            _ = tokio::signal::ctrl_c() => {
196
+                info!("Shutting down...");
197
+                break;
198
+            }
199
+        }
200
+    }
201
+
202
+    Ok(())
203
+}
204
+
205
+async fn show_status() -> anyhow::Result<()> {
206
+    // TODO: Connect to running daemon via IPC and get status
207
+    println!("HyprKVM Status");
208
+    println!("==============");
209
+    println!("Daemon: not implemented yet");
210
+    Ok(())
211
+}
212
+
213
+async fn handle_move(direction: &str) -> anyhow::Result<()> {
214
+    use hyprkvm_common::Direction;
215
+
216
+    let dir: Direction = direction.parse()?;
217
+    tracing::debug!("Move request: {}", dir);
218
+
219
+    // TODO: Connect to daemon, check if network switch needed
220
+    // For now, just execute local hyprctl move
221
+    let output = tokio::process::Command::new("hyprctl")
222
+        .args(["dispatch", "movefocus", &dir.to_string()])
223
+        .output()
224
+        .await?;
225
+
226
+    if !output.status.success() {
227
+        let stderr = String::from_utf8_lossy(&output.stderr);
228
+        tracing::error!("hyprctl failed: {}", stderr);
229
+    }
230
+
231
+    Ok(())
232
+}
233
+
234
+fn show_config(config_path: &std::path::Path) -> anyhow::Result<()> {
235
+    if config_path.exists() {
236
+        let content = std::fs::read_to_string(config_path)?;
237
+        println!("{}", content);
238
+    } else {
239
+        println!("No config file at {:?}", config_path);
240
+        println!("\nDefault configuration:");
241
+        let default = Config::default();
242
+        println!("{}", toml::to_string_pretty(&default)?);
243
+    }
244
+    Ok(())
245
+}
246
+
247
+async fn reload_config() -> anyhow::Result<()> {
248
+    // TODO: Send reload signal to daemon
249
+    println!("Config reload not implemented yet");
250
+    Ok(())
251
+}