- Host: Iroh endpoint + xcap screen capture + JPEG encode over BiStream - Client: connect to host, receive frames, emit Tauri events to frontend - Frontend: canvas-based frame viewer with live FPS stats - Add xcap, image, base64 deps to Tauri backend - Add core:event:default permission for event emission - Add SETUP.md with laptop setup instructions - Clean up unused imports
- commit
- 933139c93e35300c66c07875f445a9211676e606
- parent
- 9b6a0dfaee672152b647e3e35cdb33faa24dad56
- author
- dami etoile <dami.etoile@gmail.com>
| A | docs/ | 63 | ++ |
| M | src-tauri/ | 1184 | +++++++++++++++++++++++++++++- |
| M | src-tauri/ | 5 | +- |
| M | src-tauri/ | 3 | +- |
| M | src-tauri/ | 280 | ++++++- |
| M | src/ | 210 | +++-- |
@@ -0,0 +1,63 @@
1+# Keyhome Setup2+3+## Prerequisites (Ubuntu/Debian)4+5+```bash6+# Tauri v2 system dependencies7+sudo apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file \8+ libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev9+10+# FIDO2 + screen capture dependencies11+sudo apt install -y libudev-dev libusb-1.0-0-dev pkg-config12+13+# xcap wayland support (even on X11, xcap pulls these in)14+sudo apt install -y libwayland-dev libpipewire-0.3-dev libgbm-dev15+16+# Rust toolchain17+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh18+source "$HOME/.cargo/env"19+20+# Clone21+git clone https://github.com/FelineStateMachine/keyhome.git22+cd keyhome23+```24+25+## Run26+27+```bash28+# Development mode (opens the Tauri window)29+cargo tauri dev30+31+# Or build a debug binary32+cargo tauri build --debug33+```34+35+## Usage36+37+### As Host (Server)38+1. Click "Start host" — the app creates an Iroh endpoint and waits for connections39+2. Copy the address JSON (click "Copy address")40+3. Share the address with the client machine41+42+### As Client43+1. Paste the host's address JSON into the "Connect to Host" input44+2. Click "Connect" — the app connects over Iroh and streams screen frames45+3. The remote screen appears in the viewer canvas46+47+## Architecture48+49+- **Transport**: Iroh (peer-to-peer, relay-assisted)50+- **Screen capture**: xcap (X11/PipeWire)51+- **FIDO2**: ctap-hid-fido2 (CTAP 2.0/2.1 over HID)52+- **Frame encoding**: JPEG (MVP — upgrade to H.264 later)53+- **Protocol**: BiStream with frame headers `[width:u32][height:u32][jpeg_len:u32][jpeg_data]`54+55+## Spikes (evidence)56+57+| Spike | Status | What it proves |58+|-------|--------|----------------|59+| 001-iroh-native-ping | ✅ PASS | Iroh endpoints + ALPN routing work in native Rust |60+| 002-yubikey-hmac | ⚠️ PARTIAL | challenge_response crate works but only for YubiKey (not Titan) |61+| 003-fido2-hid | ✅ PASS | ctap-hid-fido2 communicates with Google Titan v2 |62+| 004-derive-iroh-identity | ✅ PASS | Titan hmac-secret → Iroh SecretKey → working endpoint (6.5ms RTT) |63+| 005-frame-stream | ✅ PASS | Screen capture → JPEG → Iroh stream → client receives frames |@@ -110,6 +110,22 @@ dependencies = [
110110 "libc",111111 ]112112 113+[[package]]114+name = "annotate-snippets"115+version = "0.11.5"116+source = "registry+https://github.com/rust-lang/crates.io-index"117+checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"118+dependencies = [119+ "anstyle",120+ "unicode-width 0.2.2",121+]122+123+[[package]]124+name = "anstyle"125+version = "1.0.14"126+source = "registry+https://github.com/rust-lang/crates.io-index"127+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"128+113129 [[package]]114130 name = "anyhow"115131 version = "1.0.102"@@ -121,7 +137,9 @@ name = "app"
121137 version = "0.1.0"122138 dependencies = [123139 "anyhow",140+ "base64 0.22.1",124141 "ctap-hid-fido2",142+ "image",125143 "iroh",126144 "log",127145 "serde",@@ -130,6 +148,7 @@ dependencies = [
130148 "tauri-build",131149 "tauri-plugin-log",132150 "tokio",151+ "xcap",133152 ]134153 135154 [[package]]@@ -162,7 +181,7 @@ dependencies = [
162181 "asn1-rs-derive",163182 "asn1-rs-impl",164183 "displaydoc",165- "nom",184+ "nom 7.1.3",166185 "num-traits",167186 "rusticata-macros",168187 "thiserror 2.0.18",@@ -192,6 +211,126 @@ dependencies = [
192211 "syn 2.0.118",193212 ]194213 214+[[package]]215+name = "async-broadcast"216+version = "0.7.2"217+source = "registry+https://github.com/rust-lang/crates.io-index"218+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"219+dependencies = [220+ "event-listener",221+ "event-listener-strategy",222+ "futures-core",223+ "pin-project-lite",224+]225+226+[[package]]227+name = "async-channel"228+version = "2.5.0"229+source = "registry+https://github.com/rust-lang/crates.io-index"230+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"231+dependencies = [232+ "concurrent-queue",233+ "event-listener-strategy",234+ "futures-core",235+ "pin-project-lite",236+]237+238+[[package]]239+name = "async-executor"240+version = "1.14.0"241+source = "registry+https://github.com/rust-lang/crates.io-index"242+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"243+dependencies = [244+ "async-task",245+ "concurrent-queue",246+ "fastrand",247+ "futures-lite",248+ "pin-project-lite",249+ "slab",250+]251+252+[[package]]253+name = "async-io"254+version = "2.6.0"255+source = "registry+https://github.com/rust-lang/crates.io-index"256+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"257+dependencies = [258+ "autocfg",259+ "cfg-if",260+ "concurrent-queue",261+ "futures-io",262+ "futures-lite",263+ "parking",264+ "polling",265+ "rustix 1.1.4",266+ "slab",267+ "windows-sys 0.61.2",268+]269+270+[[package]]271+name = "async-lock"272+version = "3.4.2"273+source = "registry+https://github.com/rust-lang/crates.io-index"274+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"275+dependencies = [276+ "event-listener",277+ "event-listener-strategy",278+ "pin-project-lite",279+]280+281+[[package]]282+name = "async-process"283+version = "2.5.0"284+source = "registry+https://github.com/rust-lang/crates.io-index"285+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"286+dependencies = [287+ "async-channel",288+ "async-io",289+ "async-lock",290+ "async-signal",291+ "async-task",292+ "blocking",293+ "cfg-if",294+ "event-listener",295+ "futures-lite",296+ "rustix 1.1.4",297+]298+299+[[package]]300+name = "async-recursion"301+version = "1.1.1"302+source = "registry+https://github.com/rust-lang/crates.io-index"303+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"304+dependencies = [305+ "proc-macro2",306+ "quote",307+ "syn 2.0.118",308+]309+310+[[package]]311+name = "async-signal"312+version = "0.2.14"313+source = "registry+https://github.com/rust-lang/crates.io-index"314+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"315+dependencies = [316+ "async-io",317+ "async-lock",318+ "atomic-waker",319+ "cfg-if",320+ "futures-core",321+ "futures-io",322+ "rustix 1.1.4",323+ "signal-hook-registry",324+ "slab",325+ "windows-sys 0.61.2",326+]327+328+[[package]]329+name = "async-task"330+version = "4.7.1"331+source = "registry+https://github.com/rust-lang/crates.io-index"332+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"333+195334 [[package]]196335 name = "async-trait"197336 version = "0.1.89"@@ -234,7 +373,7 @@ dependencies = [
234373 "glib-sys",235374 "gobject-sys",236375 "libc",237- "system-deps",376+ "system-deps 6.2.2",238377 ]239378 240379 [[package]]@@ -296,6 +435,25 @@ version = "1.8.3"
296435 source = "registry+https://github.com/rust-lang/crates.io-index"297436 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"298437 438+[[package]]439+name = "bindgen"440+version = "0.72.1"441+source = "registry+https://github.com/rust-lang/crates.io-index"442+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"443+dependencies = [444+ "annotate-snippets",445+ "bitflags 2.13.0",446+ "cexpr",447+ "clang-sys",448+ "itertools",449+ "proc-macro2",450+ "quote",451+ "regex",452+ "rustc-hash",453+ "shlex 1.3.0",454+ "syn 2.0.118",455+]456+299457 [[package]]300458 name = "bit-set"301459 version = "0.8.0"@@ -388,6 +546,19 @@ dependencies = [
388546 "objc2",389547 ]390548 549+[[package]]550+name = "blocking"551+version = "1.6.2"552+source = "registry+https://github.com/rust-lang/crates.io-index"553+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"554+dependencies = [555+ "async-channel",556+ "async-task",557+ "futures-io",558+ "futures-lite",559+ "piper",560+]561+391562 [[package]]392563 name = "borsh"393564 version = "1.7.0"@@ -478,6 +649,20 @@ name = "bytemuck"
478649 version = "1.25.0"479650 source = "registry+https://github.com/rust-lang/crates.io-index"480651 checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"652+dependencies = [653+ "bytemuck_derive",654+]655+656+[[package]]657+name = "bytemuck_derive"658+version = "1.10.2"659+source = "registry+https://github.com/rust-lang/crates.io-index"660+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"661+dependencies = [662+ "proc-macro2",663+ "quote",664+ "syn 2.0.118",665+]481666 482667 [[package]]483668 name = "byteorder"@@ -485,6 +670,12 @@ version = "1.5.0"
485670 source = "registry+https://github.com/rust-lang/crates.io-index"486671 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"487672 673+[[package]]674+name = "byteorder-lite"675+version = "0.1.0"676+source = "registry+https://github.com/rust-lang/crates.io-index"677+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"678+488679 [[package]]489680 name = "bytes"490681 version = "1.12.0"@@ -516,7 +707,7 @@ checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
516707 dependencies = [517708 "glib-sys",518709 "libc",519- "system-deps",710+ "system-deps 6.2.2",520711 ]521712 522713 [[package]]@@ -577,7 +768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
577768 checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"578769 dependencies = [579770 "find-msvc-tools",580- "shlex",771+ "shlex 2.0.1",581772 ]582773 583774 [[package]]@@ -586,6 +777,15 @@ version = "1.1.0"
586777 source = "registry+https://github.com/rust-lang/crates.io-index"587778 checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"588779 780+[[package]]781+name = "cexpr"782+version = "0.6.0"783+source = "registry+https://github.com/rust-lang/crates.io-index"784+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"785+dependencies = [786+ "nom 7.1.3",787+]788+589789 [[package]]590790 name = "cfb"591791 version = "0.7.3"@@ -604,7 +804,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
604804 checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"605805 dependencies = [606806 "smallvec",607- "target-lexicon",807+ "target-lexicon 0.12.16",808+]809+810+[[package]]811+name = "cfg-expr"812+version = "0.20.8"813+source = "registry+https://github.com/rust-lang/crates.io-index"814+checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"815+dependencies = [816+ "smallvec",817+ "target-lexicon 0.13.5",608818 ]609819 610820 [[package]]@@ -679,6 +889,17 @@ dependencies = [
679889 "inout",680890 ]681891 892+[[package]]893+name = "clang-sys"894+version = "1.8.1"895+source = "registry+https://github.com/rust-lang/crates.io-index"896+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"897+dependencies = [898+ "glob",899+ "libc",900+ "libloading 0.8.9",901+]902+682903 [[package]]683904 name = "cmov"684905 version = "0.5.4"@@ -704,6 +925,15 @@ dependencies = [
704925 "memchr",705926 ]706927 928+[[package]]929+name = "concurrent-queue"930+version = "2.5.0"931+source = "registry+https://github.com/rust-lang/crates.io-index"932+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"933+dependencies = [934+ "crossbeam-utils",935+]936+707937 [[package]]708938 name = "const-oid"709939 version = "0.10.2"@@ -716,6 +946,15 @@ version = "0.4.2"
716946 source = "registry+https://github.com/rust-lang/crates.io-index"717947 checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"718948 949+[[package]]950+name = "convert_case"951+version = "0.8.0"952+source = "registry+https://github.com/rust-lang/crates.io-index"953+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"954+dependencies = [955+ "unicode-segmentation",956+]957+719958 [[package]]720959 name = "convert_case"721960 version = "0.10.0"@@ -735,6 +974,12 @@ dependencies = [
735974 "version_check",736975 ]737976 977+[[package]]978+name = "cookie-factory"979+version = "0.3.3"980+source = "registry+https://github.com/rust-lang/crates.io-index"981+checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"982+738983 [[package]]739984 name = "cordyceps"740985 version = "0.3.4"@@ -1111,7 +1356,7 @@ checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
11111356 dependencies = [11121357 "asn1-rs",11131358 "displaydoc",1114- "nom",1359+ "nom 7.1.3",11151360 "num-bigint",11161361 "num-traits",11171362 "rusticata-macros",@@ -1173,7 +1418,7 @@ version = "2.1.1"
11731418 source = "registry+https://github.com/rust-lang/crates.io-index"11741419 checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"11751420 dependencies = [1176- "convert_case",1421+ "convert_case 0.10.0",11771422 "proc-macro2",11781423 "quote",11791424 "rustc_version",@@ -1251,6 +1496,15 @@ dependencies = [
12511496 "syn 2.0.118",12521497 ]12531498 1499+[[package]]1500+name = "dlib"1501+version = "0.5.3"1502+source = "registry+https://github.com/rust-lang/crates.io-index"1503+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"1504+dependencies = [1505+ "libloading 0.7.4",1506+]1507+12541508 [[package]]12551509 name = "dlopen2"12561510 version = "0.8.2"@@ -1289,6 +1543,12 @@ dependencies = [
12891543 "tendril",12901544 ]12911545 1546+[[package]]1547+name = "downcast-rs"1548+version = "1.2.1"1549+source = "registry+https://github.com/rust-lang/crates.io-index"1550+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"1551+12921552 [[package]]12931553 name = "dpi"12941554 version = "0.1.2"@@ -1298,6 +1558,46 @@ dependencies = [
12981558 "serde",12991559 ]13001560 1561+[[package]]1562+name = "drm"1563+version = "0.14.1"1564+source = "registry+https://github.com/rust-lang/crates.io-index"1565+checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f"1566+dependencies = [1567+ "bitflags 2.13.0",1568+ "bytemuck",1569+ "drm-ffi",1570+ "drm-fourcc",1571+ "libc",1572+ "rustix 0.38.44",1573+]1574+1575+[[package]]1576+name = "drm-ffi"1577+version = "0.9.1"1578+source = "registry+https://github.com/rust-lang/crates.io-index"1579+checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690"1580+dependencies = [1581+ "drm-sys",1582+ "rustix 1.1.4",1583+]1584+1585+[[package]]1586+name = "drm-fourcc"1587+version = "2.2.0"1588+source = "registry+https://github.com/rust-lang/crates.io-index"1589+checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"1590+1591+[[package]]1592+name = "drm-sys"1593+version = "0.8.1"1594+source = "registry+https://github.com/rust-lang/crates.io-index"1595+checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8"1596+dependencies = [1597+ "libc",1598+ "linux-raw-sys 0.9.4",1599+]1600+13011601 [[package]]13021602 name = "dtoa"13031603 version = "1.0.11"@@ -1405,6 +1705,12 @@ version = "0.6.1"
14051705 source = "registry+https://github.com/rust-lang/crates.io-index"14061706 checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"14071707 1708+[[package]]1709+name = "endi"1710+version = "1.1.1"1711+source = "registry+https://github.com/rust-lang/crates.io-index"1712+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"1713+14081714 [[package]]14091715 name = "enum-assoc"14101716 version = "1.3.0"@@ -1416,6 +1722,27 @@ dependencies = [
14161722 "syn 2.0.118",14171723 ]14181724 1725+[[package]]1726+name = "enumflags2"1727+version = "0.7.12"1728+source = "registry+https://github.com/rust-lang/crates.io-index"1729+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"1730+dependencies = [1731+ "enumflags2_derive",1732+ "serde",1733+]1734+1735+[[package]]1736+name = "enumflags2_derive"1737+version = "0.7.12"1738+source = "registry+https://github.com/rust-lang/crates.io-index"1739+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"1740+dependencies = [1741+ "proc-macro2",1742+ "quote",1743+ "syn 2.0.118",1744+]1745+14191746 [[package]]14201747 name = "env_filter"14211748 version = "0.1.4"@@ -1453,6 +1780,27 @@ dependencies = [
14531780 "windows-sys 0.61.2",14541781 ]14551782 1783+[[package]]1784+name = "event-listener"1785+version = "5.4.1"1786+source = "registry+https://github.com/rust-lang/crates.io-index"1787+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"1788+dependencies = [1789+ "concurrent-queue",1790+ "parking",1791+ "pin-project-lite",1792+]1793+1794+[[package]]1795+name = "event-listener-strategy"1796+version = "0.5.4"1797+source = "registry+https://github.com/rust-lang/crates.io-index"1798+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"1799+dependencies = [1800+ "event-listener",1801+ "pin-project-lite",1802+]1803+14561804 [[package]]14571805 name = "fastrand"14581806 version = "2.4.1"@@ -1677,6 +2025,30 @@ dependencies = [
16772025 "slab",16782026 ]16792027 2028+[[package]]2029+name = "gbm"2030+version = "0.18.0"2031+source = "registry+https://github.com/rust-lang/crates.io-index"2032+checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a"2033+dependencies = [2034+ "bitflags 2.13.0",2035+ "drm",2036+ "drm-fourcc",2037+ "gbm-sys",2038+ "libc",2039+ "wayland-backend",2040+ "wayland-server",2041+]2042+2043+[[package]]2044+name = "gbm-sys"2045+version = "0.4.0"2046+source = "registry+https://github.com/rust-lang/crates.io-index"2047+checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f"2048+dependencies = [2049+ "libc",2050+]2051+16802052 [[package]]16812053 name = "gdk"16822054 version = "0.18.2"@@ -1715,7 +2087,7 @@ dependencies = [
17152087 "glib-sys",17162088 "gobject-sys",17172089 "libc",1718- "system-deps",2090+ "system-deps 6.2.2",17192091 ]17202092 17212093 [[package]]@@ -1732,7 +2104,7 @@ dependencies = [
17322104 "libc",17332105 "pango-sys",17342106 "pkg-config",1735- "system-deps",2107+ "system-deps 6.2.2",17362108 ]17372109 17382110 [[package]]@@ -1746,7 +2118,7 @@ dependencies = [
17462118 "gobject-sys",17472119 "libc",17482120 "pkg-config",1749- "system-deps",2121+ "system-deps 6.2.2",17502122 ]17512123 17522124 [[package]]@@ -1772,7 +2144,7 @@ dependencies = [
17722144 "gdk-sys",17732145 "glib-sys",17742146 "libc",1775- "system-deps",2147+ "system-deps 6.2.2",17762148 "x11",17772149 ]17782150 @@ -1878,10 +2250,30 @@ dependencies = [
18782250 "glib-sys",18792251 "gobject-sys",18802252 "libc",1881- "system-deps",2253+ "system-deps 6.2.2",18822254 "winapi",18832255 ]18842256 2257+[[package]]2258+name = "gl"2259+version = "0.14.0"2260+source = "registry+https://github.com/rust-lang/crates.io-index"2261+checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"2262+dependencies = [2263+ "gl_generator",2264+]2265+2266+[[package]]2267+name = "gl_generator"2268+version = "0.14.0"2269+source = "registry+https://github.com/rust-lang/crates.io-index"2270+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"2271+dependencies = [2272+ "khronos_api",2273+ "log",2274+ "xml-rs",2275+]2276+18852277 [[package]]18862278 name = "glib"18872279 version = "0.18.5"@@ -1926,7 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19262318 checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"19272319 dependencies = [19282320 "libc",1929- "system-deps",2321+ "system-deps 6.2.2",19302322 ]19312323 19322324 [[package]]@@ -1955,7 +2347,7 @@ checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
19552347 dependencies = [19562348 "glib-sys",19572349 "libc",1958- "system-deps",2350+ "system-deps 6.2.2",19592351 ]19602352 19612353 [[package]]@@ -1994,7 +2386,7 @@ dependencies = [
19942386 "gobject-sys",19952387 "libc",19962388 "pango-sys",1997- "system-deps",2389+ "system-deps 6.2.2",19982390 ]19992391 20002392 [[package]]@@ -2072,6 +2464,12 @@ version = "0.5.0"
20722464 source = "registry+https://github.com/rust-lang/crates.io-index"20732465 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"20742466 2467+[[package]]2468+name = "hermit-abi"2469+version = "0.5.2"2470+source = "registry+https://github.com/rust-lang/crates.io-index"2471+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"2472+20752473 [[package]]20762474 name = "hex"20772475 version = "0.4.3"@@ -2460,6 +2858,21 @@ dependencies = [
24602858 "xmltree",24612859 ]24622860 2861+[[package]]2862+name = "image"2863+version = "0.25.9"2864+source = "registry+https://github.com/rust-lang/crates.io-index"2865+checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"2866+dependencies = [2867+ "bytemuck",2868+ "byteorder-lite",2869+ "moxcms",2870+ "num-traits",2871+ "png 0.18.1",2872+ "zune-core",2873+ "zune-jpeg",2874+]2875+24632876 [[package]]24642877 name = "indexmap"24652878 version = "1.9.3"@@ -2691,6 +3104,15 @@ dependencies = [
26913104 "ws_stream_wasm",26923105 ]26933106 3107+[[package]]3108+name = "itertools"3109+version = "0.13.0"3110+source = "registry+https://github.com/rust-lang/crates.io-index"3111+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"3112+dependencies = [3113+ "either",3114+]3115+26943116 [[package]]26953117 name = "itoa"26963118 version = "1.0.18"@@ -2717,7 +3139,7 @@ dependencies = [
27173139 "glib-sys",27183140 "gobject-sys",27193141 "libc",2720- "system-deps",3142+ "system-deps 6.2.2",27213143 ]27223144 27233145 [[package]]@@ -2838,6 +3260,22 @@ dependencies = [
28383260 "unicode-segmentation",28393261 ]28403262 3263+[[package]]3264+name = "khronos-egl"3265+version = "6.0.0"3266+source = "registry+https://github.com/rust-lang/crates.io-index"3267+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"3268+dependencies = [3269+ "libc",3270+ "pkg-config",3271+]3272+3273+[[package]]3274+name = "khronos_api"3275+version = "3.1.0"3276+source = "registry+https://github.com/rust-lang/crates.io-index"3277+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"3278+28413279 [[package]]28423280 name = "lazy_static"28433281 version = "1.5.0"@@ -2864,7 +3302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
28643302 checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"28653303 dependencies = [28663304 "gtk-sys",2867- "libloading",3305+ "libloading 0.7.4",28683306 "once_cell",28693307 ]28703308 @@ -2893,6 +3331,16 @@ dependencies = [
28933331 "winapi",28943332 ]28953333 3334+[[package]]3335+name = "libloading"3336+version = "0.8.9"3337+source = "registry+https://github.com/rust-lang/crates.io-index"3338+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"3339+dependencies = [3340+ "cfg-if",3341+ "windows-link 0.2.1",3342+]3343+28963344 [[package]]28973345 name = "libredox"28983346 version = "0.1.17"@@ -2902,6 +3350,73 @@ dependencies = [
29023350 "libc",29033351 ]29043352 3353+[[package]]3354+name = "libspa"3355+version = "0.9.2"3356+source = "registry+https://github.com/rust-lang/crates.io-index"3357+checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4"3358+dependencies = [3359+ "bitflags 2.13.0",3360+ "cc",3361+ "convert_case 0.8.0",3362+ "cookie-factory",3363+ "libc",3364+ "libspa-sys",3365+ "nix",3366+ "nom 8.0.0",3367+ "system-deps 7.0.8",3368+]3369+3370+[[package]]3371+name = "libspa-sys"3372+version = "0.9.2"3373+source = "registry+https://github.com/rust-lang/crates.io-index"3374+checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0"3375+dependencies = [3376+ "bindgen",3377+ "cc",3378+ "system-deps 7.0.8",3379+]3380+3381+[[package]]3382+name = "libwayshot-xcap"3383+version = "0.3.2"3384+source = "registry+https://github.com/rust-lang/crates.io-index"3385+checksum = "558a3a7ca16a17a14adf8f051b3adcd7766d397532f5f6d6a48034db11e54c22"3386+dependencies = [3387+ "drm",3388+ "gbm",3389+ "gl",3390+ "image",3391+ "khronos-egl",3392+ "memmap2",3393+ "rustix 1.1.4",3394+ "thiserror 2.0.18",3395+ "tracing",3396+ "wayland-backend",3397+ "wayland-client",3398+ "wayland-protocols",3399+ "wayland-protocols-wlr",3400+]3401+3402+[[package]]3403+name = "linux-raw-sys"3404+version = "0.4.15"3405+source = "registry+https://github.com/rust-lang/crates.io-index"3406+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"3407+3408+[[package]]3409+name = "linux-raw-sys"3410+version = "0.9.4"3411+source = "registry+https://github.com/rust-lang/crates.io-index"3412+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"3413+3414+[[package]]3415+name = "linux-raw-sys"3416+version = "0.12.1"3417+source = "registry+https://github.com/rust-lang/crates.io-index"3418+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"3419+29053420 [[package]]29063421 name = "litemap"29073422 version = "0.8.2"@@ -2986,6 +3501,15 @@ version = "2.8.2"
29863501 source = "registry+https://github.com/rust-lang/crates.io-index"29873502 checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"29883503 3504+[[package]]3505+name = "memmap2"3506+version = "0.9.11"3507+source = "registry+https://github.com/rust-lang/crates.io-index"3508+checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"3509+dependencies = [3510+ "libc",3511+]3512+29893513 [[package]]29903514 name = "memoffset"29913515 version = "0.9.1"@@ -3045,6 +3569,16 @@ dependencies = [
30453569 "uuid",30463570 ]30473571 3572+[[package]]3573+name = "moxcms"3574+version = "0.7.11"3575+source = "registry+https://github.com/rust-lang/crates.io-index"3576+checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"3577+dependencies = [3578+ "num-traits",3579+ "pxfm",3580+]3581+30483582 [[package]]30493583 name = "muda"30503584 version = "0.19.3"@@ -3279,6 +3813,18 @@ version = "1.0.6"
32793813 source = "registry+https://github.com/rust-lang/crates.io-index"32803814 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"32813815 3816+[[package]]3817+name = "nix"3818+version = "0.30.1"3819+source = "registry+https://github.com/rust-lang/crates.io-index"3820+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"3821+dependencies = [3822+ "bitflags 2.13.0",3823+ "cfg-if",3824+ "cfg_aliases",3825+ "libc",3826+]3827+32823828 [[package]]32833829 name = "nom"32843830 version = "7.1.3"@@ -3289,6 +3835,15 @@ dependencies = [
32893835 "minimal-lexical",32903836 ]32913837 3838+[[package]]3839+name = "nom"3840+version = "8.0.0"3841+source = "registry+https://github.com/rust-lang/crates.io-index"3842+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"3843+dependencies = [3844+ "memchr",3845+]3846+32923847 [[package]]32933848 name = "noq"32943849 version = "1.0.0"@@ -3488,8 +4043,48 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
34884043 dependencies = [34894044 "bitflags 2.13.0",34904045 "block2",4046+ "libc",4047+ "objc2",4048+ "objc2-cloud-kit",4049+ "objc2-core-data",4050+ "objc2-core-foundation",4051+ "objc2-core-graphics",4052+ "objc2-core-image",4053+ "objc2-core-text",4054+ "objc2-core-video",4055+ "objc2-foundation",4056+ "objc2-quartz-core",4057+]4058+4059+[[package]]4060+name = "objc2-av-foundation"4061+version = "0.3.2"4062+source = "registry+https://github.com/rust-lang/crates.io-index"4063+checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0"4064+dependencies = [4065+ "bitflags 2.13.0",4066+ "block2",4067+ "dispatch2",34914068 "objc2",4069+ "objc2-avf-audio",4070+ "objc2-core-audio-types",34924071 "objc2-core-foundation",4072+ "objc2-core-graphics",4073+ "objc2-core-image",4074+ "objc2-core-video",4075+ "objc2-foundation",4076+ "objc2-image-io",4077+ "objc2-media-toolbox",4078+ "objc2-quartz-core",4079+]4080+4081+[[package]]4082+name = "objc2-avf-audio"4083+version = "0.3.2"4084+source = "registry+https://github.com/rust-lang/crates.io-index"4085+checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"4086+dependencies = [4087+ "objc2",34934088 "objc2-foundation",34944089 ]34954090 @@ -3504,12 +4099,35 @@ dependencies = [
35044099 "objc2-foundation",35054100 ]35064101 4102+[[package]]4103+name = "objc2-core-audio"4104+version = "0.3.2"4105+source = "registry+https://github.com/rust-lang/crates.io-index"4106+checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"4107+dependencies = [4108+ "dispatch2",4109+ "objc2",4110+ "objc2-core-audio-types",4111+ "objc2-core-foundation",4112+]4113+4114+[[package]]4115+name = "objc2-core-audio-types"4116+version = "0.3.2"4117+source = "registry+https://github.com/rust-lang/crates.io-index"4118+checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"4119+dependencies = [4120+ "bitflags 2.13.0",4121+ "objc2",4122+]4123+35074124 [[package]]35084125 name = "objc2-core-data"35094126 version = "0.3.2"35104127 source = "registry+https://github.com/rust-lang/crates.io-index"35114128 checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"35124129 dependencies = [4130+ "bitflags 2.13.0",35134131 "objc2",35144132 "objc2-foundation",35154133 ]@@ -3534,10 +4152,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
35344152 checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"35354153 dependencies = [35364154 "bitflags 2.13.0",4155+ "block2",35374156 "dispatch2",4157+ "libc",35384158 "objc2",35394159 "objc2-core-foundation",35404160 "objc2-io-surface",4161+ "objc2-metal",35414162 ]35424163 35434164 [[package]]@@ -3560,6 +4181,22 @@ dependencies = [
35604181 "objc2-foundation",35614182 ]35624183 4184+[[package]]4185+name = "objc2-core-media"4186+version = "0.3.2"4187+source = "registry+https://github.com/rust-lang/crates.io-index"4188+checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"4189+dependencies = [4190+ "bitflags 2.13.0",4191+ "block2",4192+ "dispatch2",4193+ "objc2",4194+ "objc2-core-audio",4195+ "objc2-core-audio-types",4196+ "objc2-core-foundation",4197+ "objc2-core-video",4198+]4199+35634200 [[package]]35644201 name = "objc2-core-text"35654202 version = "0.3.2"@@ -3572,6 +4209,21 @@ dependencies = [
35724209 "objc2-core-graphics",35734210 ]35744211 4212+[[package]]4213+name = "objc2-core-video"4214+version = "0.3.2"4215+source = "registry+https://github.com/rust-lang/crates.io-index"4216+checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"4217+dependencies = [4218+ "bitflags 2.13.0",4219+ "block2",4220+ "objc2",4221+ "objc2-core-foundation",4222+ "objc2-core-graphics",4223+ "objc2-io-surface",4224+ "objc2-metal",4225+]4226+35754227 [[package]]35764228 name = "objc2-core-wlan"35774229 version = "0.3.2"@@ -3614,6 +4266,17 @@ dependencies = [
36144266 "objc2-core-foundation",36154267 ]36164268 4269+[[package]]4270+name = "objc2-image-io"4271+version = "0.3.2"4272+source = "registry+https://github.com/rust-lang/crates.io-index"4273+checksum = "32b0446e98cf4a784cc7a0177715ff317eeaa8463841c616cfc78aa4f953c4ea"4274+dependencies = [4275+ "objc2",4276+ "objc2-core-foundation",4277+ "objc2-core-graphics",4278+]4279+36174280 [[package]]36184281 name = "objc2-io-surface"36194282 version = "0.3.2"@@ -3625,6 +4288,29 @@ dependencies = [
36254288 "objc2-core-foundation",36264289 ]36274290 4291+[[package]]4292+name = "objc2-media-toolbox"4293+version = "0.3.2"4294+source = "registry+https://github.com/rust-lang/crates.io-index"4295+checksum = "edd9fdde720df3da7046bb9097811000c1e7ab5cd579fa89d96b27d56781fb30"4296+dependencies = [4297+ "objc2",4298+ "objc2-core-audio-types",4299+ "objc2-core-foundation",4300+ "objc2-core-media",4301+]4302+4303+[[package]]4304+name = "objc2-metal"4305+version = "0.3.2"4306+source = "registry+https://github.com/rust-lang/crates.io-index"4307+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"4308+dependencies = [4309+ "bitflags 2.13.0",4310+ "objc2",4311+ "objc2-foundation",4312+]4313+36284314 [[package]]36294315 name = "objc2-quartz-core"36304316 version = "0.3.2"@@ -3754,13 +4440,23 @@ version = "0.2.0"
37544440 source = "registry+https://github.com/rust-lang/crates.io-index"37554441 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"37564442 4443+[[package]]4444+name = "ordered-stream"4445+version = "0.2.0"4446+source = "registry+https://github.com/rust-lang/crates.io-index"4447+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"4448+dependencies = [4449+ "futures-core",4450+ "pin-project-lite",4451+]4452+37574453 [[package]]37584454 name = "pad"37594455 version = "0.1.6"37604456 source = "registry+https://github.com/rust-lang/crates.io-index"37614457 checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3"37624458 dependencies = [3763- "unicode-width",4459+ "unicode-width 0.1.14",37644460 ]37654461 37664462 [[package]]@@ -3785,7 +4481,7 @@ dependencies = [
37854481 "glib-sys",37864482 "gobject-sys",37874483 "libc",3788- "system-deps",4484+ "system-deps 6.2.2",37894485 ]37904486 37914487 [[package]]@@ -3937,6 +4633,45 @@ version = "0.2.17"
39374633 source = "registry+https://github.com/rust-lang/crates.io-index"39384634 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"39394635 4636+[[package]]4637+name = "piper"4638+version = "0.2.5"4639+source = "registry+https://github.com/rust-lang/crates.io-index"4640+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"4641+dependencies = [4642+ "atomic-waker",4643+ "fastrand",4644+ "futures-io",4645+]4646+4647+[[package]]4648+name = "pipewire"4649+version = "0.9.2"4650+source = "registry+https://github.com/rust-lang/crates.io-index"4651+checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44"4652+dependencies = [4653+ "anyhow",4654+ "bitflags 2.13.0",4655+ "libc",4656+ "libspa",4657+ "libspa-sys",4658+ "nix",4659+ "once_cell",4660+ "pipewire-sys",4661+ "thiserror 2.0.18",4662+]4663+4664+[[package]]4665+name = "pipewire-sys"4666+version = "0.9.2"4667+source = "registry+https://github.com/rust-lang/crates.io-index"4668+checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36"4669+dependencies = [4670+ "bindgen",4671+ "libspa-sys",4672+ "system-deps 7.0.8",4673+]4674+39404675 [[package]]39414676 name = "pkcs8"39424677 version = "0.11.0"@@ -3961,7 +4696,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
39614696 dependencies = [39624697 "base64 0.22.1",39634698 "indexmap 2.14.0",3964- "quick-xml",4699+ "quick-xml 0.38.4",39654700 "serde",39664701 "time",39674702 ]@@ -3992,6 +4727,20 @@ dependencies = [
39924727 "miniz_oxide",39934728 ]39944729 4730+[[package]]4731+name = "polling"4732+version = "3.11.0"4733+source = "registry+https://github.com/rust-lang/crates.io-index"4734+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"4735+dependencies = [4736+ "cfg-if",4737+ "concurrent-queue",4738+ "hermit-abi",4739+ "pin-project-lite",4740+ "rustix 1.1.4",4741+ "windows-sys 0.61.2",4742+]4743+39954744 [[package]]39964745 name = "polyval"39974746 version = "0.6.2"@@ -4189,6 +4938,21 @@ dependencies = [
41894938 "syn 1.0.109",41904939 ]41914940 4941+[[package]]4942+name = "pxfm"4943+version = "0.1.29"4944+source = "registry+https://github.com/rust-lang/crates.io-index"4945+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"4946+4947+[[package]]4948+name = "quick-xml"4949+version = "0.30.0"4950+source = "registry+https://github.com/rust-lang/crates.io-index"4951+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"4952+dependencies = [4953+ "memchr",4954+]4955+41924956 [[package]]41934957 name = "quick-xml"41944958 version = "0.38.4"@@ -4198,6 +4962,15 @@ dependencies = [
41984962 "memchr",41994963 ]42004964 4965+[[package]]4966+name = "quick-xml"4967+version = "0.39.4"4968+source = "registry+https://github.com/rust-lang/crates.io-index"4969+checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"4970+dependencies = [4971+ "memchr",4972+]4973+42014974 [[package]]42024975 name = "quote"42034976 version = "1.0.46"@@ -4232,10 +5005,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42325005 checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"42335006 dependencies = [42345007 "libc",4235- "rand_chacha",5008+ "rand_chacha 0.3.1",42365009 "rand_core 0.6.4",42375010 ]42385011 5012+[[package]]5013+name = "rand"5014+version = "0.9.4"5015+source = "registry+https://github.com/rust-lang/crates.io-index"5016+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"5017+dependencies = [5018+ "rand_chacha 0.9.0",5019+ "rand_core 0.9.5",5020+]5021+42395022 [[package]]42405023 name = "rand"42415024 version = "0.10.1"@@ -4257,6 +5040,16 @@ dependencies = [
42575040 "rand_core 0.6.4",42585041 ]42595042 5043+[[package]]5044+name = "rand_chacha"5045+version = "0.9.0"5046+source = "registry+https://github.com/rust-lang/crates.io-index"5047+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"5048+dependencies = [5049+ "ppv-lite86",5050+ "rand_core 0.9.5",5051+]5052+42605053 [[package]]42615054 name = "rand_core"42625055 version = "0.6.4"@@ -4266,6 +5059,15 @@ dependencies = [
42665059 "getrandom 0.2.17",42675060 ]42685061 5062+[[package]]5063+name = "rand_core"5064+version = "0.9.5"5065+source = "registry+https://github.com/rust-lang/crates.io-index"5066+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"5067+dependencies = [5068+ "getrandom 0.3.4",5069+]5070+42695071 [[package]]42705072 name = "rand_core"42715073 version = "0.10.1"@@ -4491,7 +5293,33 @@ version = "4.1.0"
44915293 source = "registry+https://github.com/rust-lang/crates.io-index"44925294 checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"44935295 dependencies = [4494- "nom",5296+ "nom 7.1.3",5297+]5298+5299+[[package]]5300+name = "rustix"5301+version = "0.38.44"5302+source = "registry+https://github.com/rust-lang/crates.io-index"5303+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"5304+dependencies = [5305+ "bitflags 2.13.0",5306+ "errno",5307+ "libc",5308+ "linux-raw-sys 0.4.15",5309+ "windows-sys 0.59.0",5310+]5311+5312+[[package]]5313+name = "rustix"5314+version = "1.1.4"5315+source = "registry+https://github.com/rust-lang/crates.io-index"5316+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"5317+dependencies = [5318+ "bitflags 2.13.0",5319+ "errno",5320+ "libc",5321+ "linux-raw-sys 0.12.1",5322+ "windows-sys 0.61.2",44955323 ]44965324 44975325 [[package]]@@ -4950,6 +5778,12 @@ dependencies = [
49505778 "lazy_static",49515779 ]49525780 5781+[[package]]5782+name = "shlex"5783+version = "1.3.0"5784+source = "registry+https://github.com/rust-lang/crates.io-index"5785+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"5786+49535787 [[package]]49545788 name = "shlex"49555789 version = "2.0.1"@@ -5082,7 +5916,7 @@ dependencies = [
50825916 "glib-sys",50835917 "gobject-sys",50845918 "libc",5085- "system-deps",5919+ "system-deps 6.2.2",50865920 ]50875921 50885922 [[package]]@@ -5255,13 +6089,26 @@ version = "6.2.2"
52556089 source = "registry+https://github.com/rust-lang/crates.io-index"52566090 checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"52576091 dependencies = [5258- "cfg-expr",6092+ "cfg-expr 0.15.8",52596093 "heck 0.5.0",52606094 "pkg-config",52616095 "toml 0.8.2",52626096 "version-compare",52636097 ]52646098 6099+[[package]]6100+name = "system-deps"6101+version = "7.0.8"6102+source = "registry+https://github.com/rust-lang/crates.io-index"6103+checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"6104+dependencies = [6105+ "cfg-expr 0.20.8",6106+ "heck 0.5.0",6107+ "pkg-config",6108+ "toml 1.1.2+spec-1.1.0",6109+ "version-compare",6110+]6111+52656112 [[package]]52666113 name = "tagptr"52676114 version = "0.2.0"@@ -5331,6 +6178,12 @@ version = "0.12.16"
53316178 source = "registry+https://github.com/rust-lang/crates.io-index"53326179 checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"53336180 6181+[[package]]6182+name = "target-lexicon"6183+version = "0.13.5"6184+source = "registry+https://github.com/rust-lang/crates.io-index"6185+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"6186+53346187 [[package]]53356188 name = "tauri"53366189 version = "2.11.3"@@ -5582,6 +6435,19 @@ dependencies = [
55826435 "toml 1.1.2+spec-1.1.0",55836436 ]55846437 6438+[[package]]6439+name = "tempfile"6440+version = "3.27.0"6441+source = "registry+https://github.com/rust-lang/crates.io-index"6442+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"6443+dependencies = [6444+ "fastrand",6445+ "getrandom 0.4.3",6446+ "once_cell",6447+ "rustix 1.1.4",6448+ "windows-sys 0.61.2",6449+]6450+55856451 [[package]]55866452 name = "tendril"55876453 version = "0.5.0"@@ -6053,6 +6919,17 @@ version = "1.20.1"
60536919 source = "registry+https://github.com/rust-lang/crates.io-index"60546920 checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"60556921 6922+[[package]]6923+name = "uds_windows"6924+version = "1.2.1"6925+source = "registry+https://github.com/rust-lang/crates.io-index"6926+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"6927+dependencies = [6928+ "memoffset",6929+ "tempfile",6930+ "windows-sys 0.61.2",6931+]6932+60566933 [[package]]60576934 name = "unic-char-property"60586935 version = "0.9.0"@@ -6112,6 +6989,12 @@ version = "0.1.14"
61126989 source = "registry+https://github.com/rust-lang/crates.io-index"61136990 checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"61146991 6992+[[package]]6993+name = "unicode-width"6994+version = "0.2.2"6995+source = "registry+https://github.com/rust-lang/crates.io-index"6996+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"6997+61156998 [[package]]61166999 name = "unicode-xid"61177000 version = "0.2.6"@@ -6372,6 +7255,94 @@ dependencies = [
63727255 "web-sys",63737256 ]63747257 7258+[[package]]7259+name = "wayland-backend"7260+version = "0.3.15"7261+source = "registry+https://github.com/rust-lang/crates.io-index"7262+checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"7263+dependencies = [7264+ "cc",7265+ "downcast-rs",7266+ "rustix 1.1.4",7267+ "scoped-tls",7268+ "smallvec",7269+ "wayland-sys",7270+]7271+7272+[[package]]7273+name = "wayland-client"7274+version = "0.31.14"7275+source = "registry+https://github.com/rust-lang/crates.io-index"7276+checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"7277+dependencies = [7278+ "bitflags 2.13.0",7279+ "rustix 1.1.4",7280+ "wayland-backend",7281+ "wayland-scanner",7282+]7283+7284+[[package]]7285+name = "wayland-protocols"7286+version = "0.32.12"7287+source = "registry+https://github.com/rust-lang/crates.io-index"7288+checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"7289+dependencies = [7290+ "bitflags 2.13.0",7291+ "wayland-backend",7292+ "wayland-client",7293+ "wayland-scanner",7294+]7295+7296+[[package]]7297+name = "wayland-protocols-wlr"7298+version = "0.3.12"7299+source = "registry+https://github.com/rust-lang/crates.io-index"7300+checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"7301+dependencies = [7302+ "bitflags 2.13.0",7303+ "wayland-backend",7304+ "wayland-client",7305+ "wayland-protocols",7306+ "wayland-scanner",7307+]7308+7309+[[package]]7310+name = "wayland-scanner"7311+version = "0.31.10"7312+source = "registry+https://github.com/rust-lang/crates.io-index"7313+checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"7314+dependencies = [7315+ "proc-macro2",7316+ "quick-xml 0.39.4",7317+ "quote",7318+]7319+7320+[[package]]7321+name = "wayland-server"7322+version = "0.31.13"7323+source = "registry+https://github.com/rust-lang/crates.io-index"7324+checksum = "cc1846eb04c49182e04f4a099e2a830a2b745610bbc1d61246e206f29c7000a0"7325+dependencies = [7326+ "bitflags 2.13.0",7327+ "downcast-rs",7328+ "rustix 1.1.4",7329+ "wayland-backend",7330+ "wayland-scanner",7331+]7332+7333+[[package]]7334+name = "wayland-sys"7335+version = "0.31.11"7336+source = "registry+https://github.com/rust-lang/crates.io-index"7337+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"7338+dependencies = [7339+ "dlib",7340+ "libc",7341+ "log",7342+ "memoffset",7343+ "pkg-config",7344+]7345+63757346 [[package]]63767347 name = "web-sys"63777348 version = "0.3.102"@@ -6445,7 +7416,7 @@ dependencies = [
64457416 "libc",64467417 "pkg-config",64477418 "soup3-sys",6448- "system-deps",7419+ "system-deps 6.2.2",64497420 ]64507421 64517422 [[package]]@@ -6944,6 +7915,9 @@ name = "winnow"
69447915 version = "0.7.15"69457916 source = "registry+https://github.com/rust-lang/crates.io-index"69467917 checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"7918+dependencies = [7919+ "memchr",7920+]69477921 69487922 [[package]]69497923 name = "winnow"@@ -7094,13 +8068,55 @@ dependencies = [
70948068 "data-encoding",70958069 "der-parser",70968070 "lazy_static",7097- "nom",8071+ "nom 7.1.3",70988072 "oid-registry",70998073 "rusticata-macros",71008074 "thiserror 2.0.18",71018075 "time",71028076 ]71038077 8078+[[package]]8079+name = "xcap"8080+version = "0.9.6"8081+source = "registry+https://github.com/rust-lang/crates.io-index"8082+checksum = "b6ad471d5ba232bc276382d26a9d3b837d6853b7df389058b5bb1e94dcdd248c"8083+dependencies = [8084+ "dispatch2",8085+ "image",8086+ "libwayshot-xcap",8087+ "log",8088+ "objc2",8089+ "objc2-app-kit",8090+ "objc2-av-foundation",8091+ "objc2-core-foundation",8092+ "objc2-core-graphics",8093+ "objc2-core-media",8094+ "objc2-core-video",8095+ "objc2-foundation",8096+ "percent-encoding",8097+ "pipewire",8098+ "rand 0.9.4",8099+ "scopeguard",8100+ "serde",8101+ "thiserror 2.0.18",8102+ "url",8103+ "widestring",8104+ "windows 0.62.2",8105+ "xcb",8106+ "zbus",8107+]8108+8109+[[package]]8110+name = "xcb"8111+version = "1.7.0"8112+source = "registry+https://github.com/rust-lang/crates.io-index"8113+checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6"8114+dependencies = [8115+ "bitflags 1.3.2",8116+ "libc",8117+ "quick-xml 0.30.0",8118+]8119+71048120 [[package]]71058121 name = "xml-rs"71068122 version = "0.8.28"@@ -7139,6 +8155,67 @@ dependencies = [
71398155 "synstructure",71408156 ]71418157 8158+[[package]]8159+name = "zbus"8160+version = "5.13.2"8161+source = "registry+https://github.com/rust-lang/crates.io-index"8162+checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1"8163+dependencies = [8164+ "async-broadcast",8165+ "async-executor",8166+ "async-io",8167+ "async-lock",8168+ "async-process",8169+ "async-recursion",8170+ "async-task",8171+ "async-trait",8172+ "blocking",8173+ "enumflags2",8174+ "event-listener",8175+ "futures-core",8176+ "futures-lite",8177+ "hex",8178+ "libc",8179+ "ordered-stream",8180+ "rustix 1.1.4",8181+ "serde",8182+ "serde_repr",8183+ "tracing",8184+ "uds_windows",8185+ "uuid",8186+ "windows-sys 0.61.2",8187+ "winnow 0.7.15",8188+ "zbus_macros",8189+ "zbus_names",8190+ "zvariant",8191+]8192+8193+[[package]]8194+name = "zbus_macros"8195+version = "5.13.2"8196+source = "registry+https://github.com/rust-lang/crates.io-index"8197+checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1"8198+dependencies = [8199+ "proc-macro-crate 3.5.0",8200+ "proc-macro2",8201+ "quote",8202+ "syn 2.0.118",8203+ "zbus_names",8204+ "zvariant",8205+ "zvariant_utils",8206+]8207+8208+[[package]]8209+name = "zbus_names"8210+version = "4.3.1"8211+source = "registry+https://github.com/rust-lang/crates.io-index"8212+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"8213+dependencies = [8214+ "serde",8215+ "winnow 0.7.15",8216+ "zvariant",8217+]8218+71428219 [[package]]71438220 name = "zerocopy"71448221 version = "0.8.52"@@ -7238,3 +8315,58 @@ name = "zmij"
72388315 version = "1.0.21"72398316 source = "registry+https://github.com/rust-lang/crates.io-index"72408317 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"8318+8319+[[package]]8320+name = "zune-core"8321+version = "0.5.1"8322+source = "registry+https://github.com/rust-lang/crates.io-index"8323+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"8324+8325+[[package]]8326+name = "zune-jpeg"8327+version = "0.5.15"8328+source = "registry+https://github.com/rust-lang/crates.io-index"8329+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"8330+dependencies = [8331+ "zune-core",8332+]8333+8334+[[package]]8335+name = "zvariant"8336+version = "5.9.2"8337+source = "registry+https://github.com/rust-lang/crates.io-index"8338+checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4"8339+dependencies = [8340+ "endi",8341+ "enumflags2",8342+ "serde",8343+ "winnow 0.7.15",8344+ "zvariant_derive",8345+ "zvariant_utils",8346+]8347+8348+[[package]]8349+name = "zvariant_derive"8350+version = "5.9.2"8351+source = "registry+https://github.com/rust-lang/crates.io-index"8352+checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c"8353+dependencies = [8354+ "proc-macro-crate 3.5.0",8355+ "proc-macro2",8356+ "quote",8357+ "syn 2.0.118",8358+ "zvariant_utils",8359+]8360+8361+[[package]]8362+name = "zvariant_utils"8363+version = "3.3.0"8364+source = "registry+https://github.com/rust-lang/crates.io-index"8365+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"8366+dependencies = [8367+ "proc-macro2",8368+ "quote",8369+ "serde",8370+ "syn 2.0.118",8371+ "winnow 0.7.15",8372+]@@ -24,4 +24,7 @@ tauri-plugin-log = "2"
2424 ctap-hid-fido2 = "3.5"2525 iroh = "1.0"2626 anyhow = "1"27-tokio = { version = "1", features = ["macros", "rt-multi-thread"] }27+tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-util", "time"] }28+xcap = "0.9"29+image = { version = "0.25", default-features = false, features = ["jpeg"] }30+base64 = "0.22"@@ -3,20 +3,45 @@
33 //! Commands:44 //! - fido_device_info: enumerate FIDO2 devices and get info55 //! - fido_pin_retries: check PIN retry count6-//! - iroh_host_start: start Iroh endpoint, return addr JSON6+//! - iroh_host_start: start Iroh endpoint, stream screen frames to clients77 //! - iroh_host_status: check if host endpoint is running8-//! - iroh_client_connect: connect to a host addr (placeholder for now)8+//! - iroh_client_connect: connect to a host, receive frames, emit "frame" events99 10+use anyhow::Context as _;11+use base64::Engine;1012 use ctap_hid_fido2::{Cfg, FidoKeyHidFactory};13+use iroh::endpoint::{presets, Connection};14+use iroh::protocol::{ProtocolHandler, Router};15+use iroh::{Endpoint, EndpointAddr, SecretKey};1116 use serde::Serialize;12-use std::sync::Mutex;13-use tauri::State;17+use std::io::Cursor;18+use std::sync::{Arc, Mutex};19+use std::time::{Duration, Instant};20+use tauri::{AppHandle, Emitter, State};21+use tokio::io::{AsyncReadExt, AsyncWriteExt};22+23+const ALPN: &[u8] = b"keyhome/frame-stream/0";24+25+// ─── AppState ────────────────────────────────────────────────────────────────1426 15-1627 pub struct AppState {17- pub host_addr: Mutex<Option<String>>,28+ pub host: Mutex<Option<HostState>>,29+}30+31+pub struct HostState {32+ pub addr_json: String,1833 }1934 35+impl Default for AppState {36+ fn default() -> Self {37+ Self {38+ host: Mutex::new(None),39+ }40+ }41+}42+43+// ─── FIDO2 ───────────────────────────────────────────────────────────────────44+2045 2146 pub struct FidoDeviceInfo {2247 pub found: bool,@@ -31,6 +56,23 @@ pub struct FidoDeviceInfo {
3156 pub error: Option<String>,3257 }3358 59+impl Default for FidoDeviceInfo {60+ fn default() -> Self {61+ Self {62+ found: false,63+ vid: 0,64+ pid: 0,65+ product: String::new(),66+ versions: vec![],67+ extensions: vec![],68+ options: vec![],69+ max_msg_size: 0,70+ pin_retries: 0,71+ error: None,72+ }73+ }74+}75+3476 3577 pub fn fido_device_info() -> FidoDeviceInfo {3678 let devices = ctap_hid_fido2::get_fidokey_devices();@@ -89,23 +131,6 @@ pub fn fido_device_info() -> FidoDeviceInfo {
89131 }90132 }91133 92-impl Default for FidoDeviceInfo {93- fn default() -> Self {94- Self {95- found: false,96- vid: 0,97- pid: 0,98- product: String::new(),99- versions: vec![],100- extensions: vec![],101- options: vec![],102- max_msg_size: 0,103- pin_retries: 0,104- error: None,105- }106- }107-}108-109134 110135 pub struct PinRetries {111136 pub retries: u32,@@ -133,6 +158,8 @@ pub fn fido_pin_retries() -> PinRetries {
133158 }134159 }135160 161+// ─── Iroh Host ───────────────────────────────────────────────────────────────162+136163 137164 pub struct HostStatus {138165 pub running: bool,@@ -142,22 +169,32 @@ pub struct HostStatus {
142169 143170 144171 pub async fn iroh_host_start(state: State<'_, AppState>) -> Result<HostStatus, String> {145- let secret = iroh::SecretKey::generate();146- let endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0)172+ let secret = SecretKey::generate();173+ let endpoint = Endpoint::builder(presets::N0)147174 .secret_key(secret)148175 .bind()149176 .await150177 .map_err(|e| format!("Failed to bind endpoint: {}", e))?;151178 179+ let _ = tokio::time::timeout(Duration::from_secs(5), endpoint.online()).await;180+152181 let addr = endpoint.addr();153- let addr_json = serde_json::to_string(&addr).map_err(|e| format!("Failed to serialize addr: {}", e))?;182+ let addr_json =183+ serde_json::to_string(&addr).map_err(|e| format!("Failed to serialize addr: {}", e))?;154184 155- let mut host_addr = state.host_addr.lock().map_err(|e| format!("Lock error: {}", e))?;156- *host_addr = Some(addr_json.clone());185+ let handler = Arc::new(FrameStreamHandler);186+ let _router = Router::builder(endpoint.clone())187+ .accept(ALPN, handler)188+ .spawn();157189 158- // Keep endpoint alive by leaking it — for MVP this is fine159- // In production, store in AppState190+ // Keep endpoint alive by leaking — MVP tradeoff160191 std::mem::forget(endpoint);192+ std::mem::forget(_router);193+194+ let mut host = state.host.lock().map_err(|e| format!("Lock error: {}", e))?;195+ *host = Some(HostState {196+ addr_json: addr_json.clone(),197+ });161198 162199 Ok(HostStatus {163200 running: true,@@ -168,12 +205,19 @@ pub async fn iroh_host_start(state: State<'_, AppState>) -> Result<HostStatus, S
168205 169206 170207 pub fn iroh_host_status(state: State<'_, AppState>) -> HostStatus {171- let host_addr = state.host_addr.lock().ok();172- match host_addr {173- Some(addr) => HostStatus {174- running: addr.is_some(),175- addr_json: addr.clone(),176- error: None,208+ let host = state.host.lock().ok();209+ match host {210+ Some(h) => match &*h {211+ Some(hs) => HostStatus {212+ running: true,213+ addr_json: Some(hs.addr_json.clone()),214+ error: None,215+ },216+ None => HostStatus {217+ running: false,218+ addr_json: None,219+ error: None,220+ },177221 },178222 None => HostStatus {179223 running: false,@@ -183,6 +227,89 @@ pub fn iroh_host_status(state: State<'_, AppState>) -> HostStatus {
183227 }184228 }185229 230+231+struct FrameStreamHandler;232+233+impl ProtocolHandler for FrameStreamHandler {234+ async fn accept(&self, conn: Connection) -> Result<(), iroh::protocol::AcceptError> {235+ if let Err(e) = stream_frames(conn).await {236+ eprintln!("[host] stream error: {}", e);237+ }238+ Ok(())239+ }240+}241+242+async fn stream_frames(conn: Connection) -> anyhow::Result<()> {243+ let (mut send, mut recv) = conn.accept_bi().await?;244+245+ // Wait for client start signal246+ let mut start_buf = [0u8; 1];247+ recv.read_exact(&mut start_buf).await?;248+ if start_buf[0] != 1 {249+ return Ok(());250+ }251+252+ // Capture screen253+ let monitors = xcap::Monitor::all().context("failed to enumerate monitors")?;254+ let monitor = monitors255+ .into_iter()256+ .next()257+ .ok_or_else(|| anyhow::anyhow!("no monitors found"))?;258+259+ let mut frame_count = 0u32;260+ let start = Instant::now();261+262+ loop {263+ let image = monitor.capture_image()?;264+ let rgb_image = image::DynamicImage::ImageRgba8(image).to_rgb8();265+ let (w, h) = (rgb_image.width(), rgb_image.height());266+267+ let mut jpeg_buf = Vec::with_capacity(50_000);268+ rgb_image.write_to(&mut Cursor::new(&mut jpeg_buf), image::ImageFormat::Jpeg)?;269+ let jpeg_size = jpeg_buf.len();270+271+ let header = [272+ (w as u32).to_be_bytes(),273+ (h as u32).to_be_bytes(),274+ (jpeg_size as u32).to_be_bytes(),275+ ]276+ .concat();277+278+ send.write_all(&header).await?;279+ send.write_all(&jpeg_buf).await?;280+281+ frame_count += 1;282+ let elapsed = start.elapsed();283+ let fps = frame_count as f64 / elapsed.as_secs_f64().max(0.001);284+ eprintln!(285+ "[host] frame={} {}x{} jpeg={}B fps={:.1}",286+ frame_count, w, h, jpeg_size, fps287+ );288+289+ // Check for client disconnect290+ match tokio::time::timeout(Duration::from_millis(1), recv.read(&mut [0u8; 1])).await {291+ Ok(Ok(Some(_))) => {292+ eprintln!("[host] client disconnected");293+ break;294+ }295+ _ => {}296+ }297+298+ tokio::time::sleep(Duration::from_millis(33)).await;299+ }300+301+ Ok(())302+}303+304+// ─── Iroh Client ─────────────────────────────────────────────────────────────305+306+307+pub struct FramePayload {308+ pub width: u32,309+ pub height: u32,310+ pub data: String, // base64 JPEG311+}312+186313 187314 pub struct ConnectResult {188315 pub connected: bool,@@ -190,10 +317,83 @@ pub struct ConnectResult {
190317 }191318 192319 193-pub async fn iroh_client_connect(_addr_json: String) -> Result<ConnectResult, String> {194- // Placeholder — full client connection will be implemented in next iteration320+pub async fn iroh_client_connect(321+ app: AppHandle,322+ _state: State<'_, AppState>,323+ addr_json: String,324+) -> Result<ConnectResult, String> {325+ let addr: EndpointAddr = serde_json::from_str(&addr_json)326+ .map_err(|e| format!("Invalid address: {}", e))?;327+328+ let secret = SecretKey::generate();329+ let endpoint = Endpoint::builder(presets::N0)330+ .secret_key(secret)331+ .bind()332+ .await333+ .map_err(|e| format!("Failed to bind endpoint: {}", e))?;334+335+ let _ = tokio::time::timeout(Duration::from_secs(10), endpoint.online()).await;336+337+ let conn = endpoint338+ .connect(addr, ALPN)339+ .await340+ .map_err(|e| format!("Failed to connect: {}", e))?;341+342+ let (mut send, mut recv) = conn343+ .open_bi()344+ .await345+ .map_err(|e| format!("Failed to open stream: {}", e))?;346+347+ // Send start signal348+ send.write_all(&[1u8])349+ .await350+ .map_err(|e| format!("Failed to send start: {}", e))?;351+352+ // Spawn background task to read frames and emit events353+ tokio::spawn(async move {354+ let mut frame_count = 0u32;355+ let start = Instant::now();356+357+ loop {358+ let mut header = [0u8; 12];359+ if recv.read_exact(&mut header).await.is_err() {360+ let _ = app.emit("frame-error", "Connection lost");361+ break;362+ }363+364+ let w = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);365+ let h = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);366+ let jpeg_len =367+ u32::from_be_bytes([header[8], header[9], header[10], header[11]]) as usize;368+369+ let mut jpeg_buf = vec![0u8; jpeg_len];370+ if recv.read_exact(&mut jpeg_buf).await.is_err() {371+ let _ = app.emit("frame-error", "Connection lost");372+ break;373+ }374+375+ frame_count += 1;376+ let elapsed = start.elapsed();377+ let fps = frame_count as f64 / elapsed.as_secs_f64().max(0.001);378+379+ let b64 = base64::engine::general_purpose::STANDARD.encode(&jpeg_buf);380+ let _ = app.emit(381+ "frame",382+ FramePayload {383+ width: w,384+ height: h,385+ data: b64,386+ },387+ );388+ let _ = app.emit("frame-stats", serde_json::json!({ "fps": fps, "count": frame_count }));389+ }390+391+ // Keep endpoint alive until task ends392+ drop(endpoint);393+ });394+195395 Ok(ConnectResult {196- connected: false,197- error: Some("Client connection not yet implemented in Tauri UI".into()),396+ connected: true,397+ error: None,198398 })199399 }@@ -25,22 +25,9 @@
2525 font-size: 14px;2626 padding: 24px;2727 }28- h1 {29- font-size: 22px;30- font-weight: 600;31- margin-bottom: 4px;32- }33- .subtitle {34- color: var(--text-dim);35- font-size: 13px;36- margin-bottom: 24px;37- }38- .grid {39- display: grid;40- grid-template-columns: 1fr 1fr;41- gap: 16px;42- max-width: 900px;43- }28+ h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; }29+ .subtitle { color: var(--text-dim); font-size: 13px; margin-bottom: 24px; }30+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 1100px; }4431 .card {4532 background: var(--surface);4633 border: 1px solid var(--border);@@ -48,95 +35,80 @@
4835 padding: 16px;4936 }5037 .card h2 {51- font-size: 14px;52- font-weight: 600;53- color: var(--text-dim);54- text-transform: uppercase;55- letter-spacing: 0.5px;56- margin-bottom: 12px;38+ font-size: 14px; font-weight: 600; color: var(--text-dim);39+ text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px;5740 }5841 .row {59- display: flex;60- justify-content: space-between;61- align-items: center;62- padding: 6px 0;63- border-bottom: 1px solid var(--border);42+ display: flex; justify-content: space-between; align-items: center;43+ padding: 6px 0; border-bottom: 1px solid var(--border);6444 }6545 .row:last-child { border-bottom: none; }6646 .row .label { color: var(--text-dim); }6747 .row .value { font-family: var(--mono); font-size: 13px; }6848 .status-badge {69- display: inline-block;70- padding: 2px 8px;71- border-radius: 12px;72- font-size: 12px;73- font-weight: 500;49+ display: inline-block; padding: 2px 8px; border-radius: 12px;50+ font-size: 12px; font-weight: 500;7451 }7552 .status-ok { background: rgba(63,185,80,0.15); color: var(--green); }7653 .status-err { background: rgba(248,81,73,0.15); color: var(--red); }7754 .status-pending { background: rgba(210,153,34,0.15); color: var(--yellow); }7855 .btn {79- background: var(--surface);80- border: 1px solid var(--border);81- color: var(--text);82- padding: 8px 16px;83- border-radius: 6px;84- font-size: 13px;85- cursor: pointer;86- transition: border-color 0.2s;56+ background: var(--surface); border: 1px solid var(--border);57+ color: var(--text); padding: 8px 16px; border-radius: 6px;58+ font-size: 13px; cursor: pointer; transition: border-color 0.2s;8759 }8860 .btn:hover { border-color: var(--accent); }8961 .btn-primary {90- background: rgba(88,166,255,0.1);91- border-color: rgba(88,166,255,0.3);62+ background: rgba(88,166,255,0.1); border-color: rgba(88,166,255,0.3);9263 color: var(--accent);9364 }9465 .btn-bar { display: flex; gap: 8px; margin-top: 12px; }9566 .addr-box {96- margin-top: 12px;97- padding: 8px 12px;98- background: rgba(88,166,255,0.05);99- border: 1px solid rgba(88,166,255,0.2);100- border-radius: 6px;101- font-family: var(--mono);102- font-size: 11px;103- word-break: break-all;104- color: var(--accent);105- display: none;67+ margin-top: 12px; padding: 8px 12px;68+ background: rgba(88,166,255,0.05); border: 1px solid rgba(88,166,255,0.2);69+ border-radius: 6px; font-family: var(--mono); font-size: 11px;70+ word-break: break-all; color: var(--accent); display: none;10671 }10772 .addr-box.visible { display: block; }108- .connect-section {109- margin-top: 16px;110- }73+ .connect-section { margin-top: 16px; }11174 .input {112- width: 100%;113- background: var(--bg);114- border: 1px solid var(--border);115- color: var(--text);116- padding: 8px 12px;117- border-radius: 6px;118- font-family: var(--mono);119- font-size: 12px;120- margin-top: 8px;75+ width: 100%; background: var(--bg); border: 1px solid var(--border);76+ color: var(--text); padding: 8px 12px; border-radius: 6px;77+ font-family: var(--mono); font-size: 12px; margin-top: 8px;12178 }12279 .input:focus { outline: none; border-color: var(--accent); }123- .error-msg {124- color: var(--red);125- font-size: 12px;126- margin-top: 8px;127- display: none;128- }80+ .error-msg { color: var(--red); font-size: 12px; margin-top: 8px; display: none; }12981 .error-msg.visible { display: block; }13082 .spinner {131- display: inline-block;132- width: 14px;133- height: 14px;134- border: 2px solid var(--border);135- border-top-color: var(--accent);136- border-radius: 50%;137- animation: spin 0.8s linear infinite;83+ display: inline-block; width: 14px; height: 14px;84+ border: 2px solid var(--border); border-top-color: var(--accent);85+ border-radius: 50%; animation: spin 0.8s linear infinite;13886 }13987 @keyframes spin { to { transform: rotate(360deg); } }88+89+ /* Frame viewer */90+ .viewer-card { grid-column: 1 / -1; }91+ .viewer-wrap {92+ margin-top: 12px; border: 1px solid var(--border); border-radius: 6px;93+ overflow: hidden; background: #000; position: relative;94+ min-height: 300px; display: flex; align-items: center; justify-content: center;95+ }96+ #frame-canvas { max-width: 100%; max-height: 500px; display: none; }97+ .viewer-placeholder {98+ color: var(--text-dim); font-size: 13px; padding: 60px 20px; text-align: center;99+ }100+ .viewer-stats {101+ position: absolute; top: 8px; right: 8px;102+ background: rgba(0,0,0,0.7); color: var(--green);103+ font-family: var(--mono); font-size: 11px; padding: 4px 8px;104+ border-radius: 4px; display: none;105+ }106+ .viewer-stats.visible { display: block; }107+ .copy-btn {108+ background: none; border: none; color: var(--text-dim); cursor: pointer;109+ font-size: 12px; margin-left: 8px;110+ }111+ .copy-btn:hover { color: var(--accent); }140112 </style>141113 </head>142114 <body>@@ -191,13 +163,16 @@
191163 <button class="btn btn-primary" onclick="startHost()">Start host</button>192164 </div>193165 <div id="host-addr-box" class="addr-box"></div>166+ <div class="btn-bar" id="copy-bar" style="display:none;">167+ <button class="btn" onclick="copyAddr()">Copy address</button>168+ </div>194169 </div>195170 196171 <!-- Iroh Client Card -->197172 <div class="card" style="grid-column: 1 / -1;">198173 <h2>Connect to Host (Client)</h2>199174 <p style="color: var(--text-dim); font-size: 12px; margin-bottom: 8px;">200- Paste the host's address JSON below to connect.175+ Paste the host's address JSON below to connect and view the remote screen.201176 </p>202177 <input class="input" id="connect-addr" placeholder='{"id":"...","addrs":[...]}' />203178 <div class="btn-bar">@@ -205,16 +180,31 @@
205180 </div>206181 <div id="connect-error" class="error-msg"></div>207182 </div>183+184+ <!-- Frame Viewer -->185+ <div class="card viewer-card">186+ <h2>Remote Screen <span id="viewer-stats" class="viewer-stats"></span></h2>187+ <div class="viewer-wrap">188+ <canvas id="frame-canvas"></canvas>189+ <div id="viewer-placeholder" class="viewer-placeholder">190+ No stream connected. Start a host on one machine, copy the address,191+ and connect from another.192+ </div>193+ </div>194+ </div>208195 </div>209196 210197 <script>211198 const { invoke } = window.__TAURI__.core;199+ const { listen } = window.__TAURI__.event;200+201+ let hostAddrJson = '';212202 203+ // ─── FIDO2 ─────────────────────────────────────────────────────────213204 async function scanFido() {214205 const status = document.getElementById('fido-status');215206 status.className = 'status-badge status-pending';216207 status.innerHTML = '<span class="spinner"></span> Scanning...';217-218208 try {219209 const info = await invoke('fido_device_info');220210 if (info.found && !info.error) {@@ -241,22 +231,21 @@
241231 }242232 }243233 234+ // ─── Host ──────────────────────────────────────────────────────────244235 async function startHost() {245236 const status = document.getElementById('host-status');246237 status.className = 'status-badge status-pending';247238 status.innerHTML = '<span class="spinner"></span> Starting...';248-249239 try {250240 const result = await invoke('iroh_host_start');251241 if (result.running) {252242 status.className = 'status-badge status-ok';253243 status.textContent = 'Running';254-244+ hostAddrJson = result.addr_json;255245 const addrBox = document.getElementById('host-addr-box');256246 addrBox.textContent = result.addr_json;257247 addrBox.classList.add('visible');258-259- // Extract endpoint ID from the JSON248+ document.getElementById('copy-bar').style.display = 'flex';260249 const addr = JSON.parse(result.addr_json);261250 document.getElementById('host-id').textContent = addr.id.substring(0, 16) + '...';262251 } else {@@ -269,6 +258,16 @@
269258 }270259 }271260 261+ function copyAddr() {262+ navigator.clipboard.writeText(hostAddrJson).then(() => {263+ const btn = document.querySelector('#copy-bar .btn');264+ const orig = btn.textContent;265+ btn.textContent = 'Copied!';266+ setTimeout(() => { btn.textContent = orig; }, 1500);267+ });268+ }269+270+ // ─── Client ────────────────────────────────────────────────────────272271 async function connectHost() {273272 const addr = document.getElementById('connect-addr').value.trim();274273 const errBox = document.getElementById('connect-error');@@ -280,12 +279,18 @@
280279 return;281280 }282281 282+ errBox.style.color = 'var(--yellow)';283+ errBox.textContent = 'Connecting...';284+ errBox.classList.add('visible');285+283286 try {284287 const result = await invoke('iroh_client_connect', { addrJson: addr });285288 if (result.connected) {286289 errBox.style.color = 'var(--green)';287- errBox.textContent = 'Connected!';290+ errBox.textContent = 'Connected! Streaming frames...';288291 errBox.classList.add('visible');292+ document.getElementById('frame-canvas').style.display = 'block';293+ document.getElementById('viewer-placeholder').style.display = 'none';289294 } else {290295 errBox.style.color = 'var(--red)';291296 errBox.textContent = result.error || 'Connection failed';@@ -297,6 +302,43 @@
297302 errBox.classList.add('visible');298303 }299304 }305+306+ // ─── Frame rendering ───────────────────────────────────────────────307+ const canvas = document.getElementById('frame-canvas');308+ const ctx = canvas.getContext('2d');309+ let lastFrameTime = 0;310+311+ listen('frame', (event) => {312+ const { width, height, data } = event.payload;313+ if (canvas.width !== width) canvas.width = width;314+ if (canvas.height !== height) canvas.height = height;315+316+ const bytes = Uint8Array.from(atob(data), c => c.charCodeAt(0));317+ const blob = new Blob([bytes], { type: 'image/jpeg' });318+ const url = URL.createObjectURL(blob);319+ const img = new Image();320+ img.onload = () => {321+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);322+ URL.revokeObjectURL(url);323+ };324+ img.src = url;325+ });326+327+ listen('frame-stats', (event) => {328+ const stats = document.getElementById('viewer-stats');329+ stats.textContent = `${event.payload.fps.toFixed(1)} fps | frame ${event.payload.count}`;330+ stats.classList.add('visible');331+ });332+333+ listen('frame-error', (event) => {334+ const errBox = document.getElementById('connect-error');335+ errBox.style.color = 'var(--red)';336+ errBox.textContent = event.payload;337+ errBox.classList.add('visible');338+ document.getElementById('frame-canvas').style.display = 'none';339+ document.getElementById('viewer-placeholder').style.display = 'block';340+ document.getElementById('viewer-placeholder').textContent = 'Disconnected: ' + event.payload;341+ });300342 </script>301343 </body>302344 </html>