cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

Wire input injection into Tauri app

dami etoile committed 8a2aaf0
- Add enigo dep with serde feature
- Dual ALPN handlers: frame-stream + input-stream on host
- InputEvent enum with mouse move/click/down/up/scroll, key down/up/click, text
- Host injects received events via enigo (same as spike 006)
- Client opens both frame and input BiStreams on connect
- iroh_client_send_input command sends events through channel to Iroh stream
- Frontend: control mode toggle, canvas mouse capture with coordinate scaling,
  keyboard capture on window when controlling, scroll wheel support
- Register all commands in lib.rs invoke_handler
- lib.rs: add mod commands declaration
commit
8a2aaf0e38d41463390130fe958dabb7f78e5039
parent
087b4d054c4761c999be7e9b3b44508aac33faf4
author
dami etoile <dami.etoile@gmail.com>
5 files changed, +731 -85
Msrc-tauri/Cargo.lock295+++++++++++++++++++++------
Msrc-tauri/Cargo.toml1+
Msrc-tauri/src/commands.rs323++++++++++++++++++++++++++++--
Msrc-tauri/src/lib.rs11+
Msrc/ui/index.html186+++++++++++++++++-
M src-tauri/Cargo.lock +231 -64
@@ -139,6 +139,7 @@ dependencies = [
139139 "anyhow",140140 "base64 0.22.1",141141 "ctap-hid-fido2",142+ "enigo",142143 "image",143144 "iroh",144145 "log",
@@ -537,13 +538,32 @@ dependencies = [
537538 "generic-array",538539 ]539540 541+[[package]]542+name = "block-sys"543+version = "0.2.1"544+source = "registry+https://github.com/rust-lang/crates.io-index"545+checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"546+dependencies = [547+ "objc-sys",548+]549+550+[[package]]551+name = "block2"552+version = "0.4.0"553+source = "registry+https://github.com/rust-lang/crates.io-index"554+checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"555+dependencies = [556+ "block-sys",557+ "objc2 0.5.2",558+]559+540560 [[package]]541561 name = "block2"542562 version = "0.6.2"543563 source = "registry+https://github.com/rust-lang/crates.io-index"544564 checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"545565 dependencies = [546- "objc2",566+ "objc2 0.6.4",547567 ]548568 549569 [[package]]
@@ -1016,6 +1036,19 @@ version = "0.8.7"
10161036 source = "registry+https://github.com/rust-lang/crates.io-index"10171037 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"10181038 1039+[[package]]1040+name = "core-graphics"1041+version = "0.23.2"1042+source = "registry+https://github.com/rust-lang/crates.io-index"1043+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"1044+dependencies = [1045+ "bitflags 1.3.2",1046+ "core-foundation 0.9.4",1047+ "core-graphics-types 0.1.3",1048+ "foreign-types",1049+ "libc",1050+]1051+10191052 [[package]]10201053 name = "core-graphics"10211054 version = "0.25.0"
@@ -1024,11 +1057,22 @@ checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
10241057 dependencies = [10251058 "bitflags 2.13.0",10261059 "core-foundation 0.10.1",1027- "core-graphics-types",1060+ "core-graphics-types 0.2.0",10281061 "foreign-types",10291062 "libc",10301063 ]10311064 1065+[[package]]1066+name = "core-graphics-types"1067+version = "0.1.3"1068+source = "registry+https://github.com/rust-lang/crates.io-index"1069+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"1070+dependencies = [1071+ "bitflags 1.3.2",1072+ "core-foundation 0.9.4",1073+ "libc",1074+]1075+10321076 [[package]]10331077 name = "core-graphics-types"10341078 version = "0.2.0"
@@ -1480,9 +1524,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
14801524 checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"14811525 dependencies = [14821526 "bitflags 2.13.0",1483- "block2",1527+ "block2 0.6.2",14841528 "libc",1485- "objc2",1529+ "objc2 0.6.4",14861530 ]14871531 14881532 [[package]]
@@ -1502,7 +1546,7 @@ version = "0.5.3"
15021546 source = "registry+https://github.com/rust-lang/crates.io-index"15031547 checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"15041548 dependencies = [1505- "libloading 0.7.4",1549+ "libloading 0.8.9",15061550 ]15071551 15081552 [[package]]
@@ -1711,6 +1755,24 @@ version = "1.1.1"
17111755 source = "registry+https://github.com/rust-lang/crates.io-index"17121756 checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"17131757 1758+[[package]]1759+name = "enigo"1760+version = "0.2.1"1761+source = "registry+https://github.com/rust-lang/crates.io-index"1762+checksum = "0087a01fc8591217447d28005379fb5a183683cc83f0a4707af28cc6603f70fb"1763+dependencies = [1764+ "core-graphics 0.23.2",1765+ "foreign-types-shared",1766+ "icrate",1767+ "libc",1768+ "log",1769+ "objc2 0.5.2",1770+ "serde",1771+ "windows 0.56.0",1772+ "xkbcommon",1773+ "xkeysym",1774+]1775+17141776 [[package]]17151777 name = "enum-assoc"17161778 version = "1.3.0"
@@ -2724,6 +2786,16 @@ dependencies = [
27242786 "png 0.17.16",27252787 ]27262788 2789+[[package]]2790+name = "icrate"2791+version = "0.1.2"2792+source = "registry+https://github.com/rust-lang/crates.io-index"2793+checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"2794+dependencies = [2795+ "block2 0.4.0",2796+ "objc2 0.5.2",2797+]2798+27272799 [[package]]27282800 name = "icu_collections"27292801 version = "2.1.1"
@@ -3389,7 +3461,7 @@ dependencies = [
33893461 "gl",33903462 "image",33913463 "khronos-egl",3392- "memmap2",3464+ "memmap2 0.9.11",33933465 "rustix 1.1.4",33943466 "thiserror 2.0.18",33953467 "tracing",
@@ -3501,6 +3573,15 @@ version = "2.8.2"
35013573 source = "registry+https://github.com/rust-lang/crates.io-index"35023574 checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"35033575 3576+[[package]]3577+name = "memmap2"3578+version = "0.8.0"3579+source = "registry+https://github.com/rust-lang/crates.io-index"3580+checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"3581+dependencies = [3582+ "libc",3583+]3584+35043585 [[package]]35053586 name = "memmap2"35063587 version = "0.9.11"
@@ -3589,7 +3670,7 @@ dependencies = [
35893670 "dpi",35903671 "gtk",35913672 "keyboard-types",3592- "objc2",3673+ "objc2 0.6.4",35933674 "objc2-app-kit",35943675 "objc2-core-foundation",35953676 "objc2-foundation",
@@ -3689,7 +3770,7 @@ version = "0.44.0"
36893770 source = "registry+https://github.com/rust-lang/crates.io-index"36903771 checksum = "9d31e7286c21ceaf0ddb1d881964011214555ea0b317cc2eb1a1d68d861386fc"36913772 dependencies = [3692- "block2",3773+ "block2 0.6.2",36933774 "dispatch2",36943775 "dlopen2",36953776 "ipnet",
@@ -3700,7 +3781,7 @@ dependencies = [
37003781 "netlink-packet-core",37013782 "netlink-packet-route 0.29.0",37023783 "netlink-sys",3703- "objc2",3784+ "objc2 0.6.4",37043785 "objc2-core-foundation",37053786 "objc2-core-wlan",37063787 "objc2-foundation",
@@ -4025,6 +4106,22 @@ dependencies = [
40254106 "libc",40264107 ]40274108 4109+[[package]]4110+name = "objc-sys"4111+version = "0.3.5"4112+source = "registry+https://github.com/rust-lang/crates.io-index"4113+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"4114+4115+[[package]]4116+name = "objc2"4117+version = "0.5.2"4118+source = "registry+https://github.com/rust-lang/crates.io-index"4119+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"4120+dependencies = [4121+ "objc-sys",4122+ "objc2-encode",4123+]4124+40284125 [[package]]40294126 name = "objc2"40304127 version = "0.6.4"
@@ -4042,9 +4139,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
40424139 checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"40434140 dependencies = [40444141 "bitflags 2.13.0",4045- "block2",4142+ "block2 0.6.2",40464143 "libc",4047- "objc2",4144+ "objc2 0.6.4",40484145 "objc2-cloud-kit",40494146 "objc2-core-data",40504147 "objc2-core-foundation",
@@ -4063,9 +4160,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
40634160 checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0"40644161 dependencies = [40654162 "bitflags 2.13.0",4066- "block2",4163+ "block2 0.6.2",40674164 "dispatch2",4068- "objc2",4165+ "objc2 0.6.4",40694166 "objc2-avf-audio",40704167 "objc2-core-audio-types",40714168 "objc2-core-foundation",
@@ -4084,7 +4181,7 @@ version = "0.3.2"
40844181 source = "registry+https://github.com/rust-lang/crates.io-index"40854182 checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"40864183 dependencies = [4087- "objc2",4184+ "objc2 0.6.4",40884185 "objc2-foundation",40894186 ]40904187
@@ -4095,7 +4192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
40954192 checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"40964193 dependencies = [40974194 "bitflags 2.13.0",4098- "objc2",4195+ "objc2 0.6.4",40994196 "objc2-foundation",41004197 ]41014198
@@ -4106,7 +4203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41064203 checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"41074204 dependencies = [41084205 "dispatch2",4109- "objc2",4206+ "objc2 0.6.4",41104207 "objc2-core-audio-types",41114208 "objc2-core-foundation",41124209 ]
@@ -4118,7 +4215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41184215 checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"41194216 dependencies = [41204217 "bitflags 2.13.0",4121- "objc2",4218+ "objc2 0.6.4",41224219 ]41234220 41244221 [[package]]
@@ -4128,7 +4225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41284225 checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"41294226 dependencies = [41304227 "bitflags 2.13.0",4131- "objc2",4228+ "objc2 0.6.4",41324229 "objc2-foundation",41334230 ]41344231
@@ -4139,10 +4236,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41394236 checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"41404237 dependencies = [41414238 "bitflags 2.13.0",4142- "block2",4239+ "block2 0.6.2",41434240 "dispatch2",41444241 "libc",4145- "objc2",4242+ "objc2 0.6.4",41464243 ]41474244 41484245 [[package]]
@@ -4152,10 +4249,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41524249 checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"41534250 dependencies = [41544251 "bitflags 2.13.0",4155- "block2",4252+ "block2 0.6.2",41564253 "dispatch2",41574254 "libc",4158- "objc2",4255+ "objc2 0.6.4",41594256 "objc2-core-foundation",41604257 "objc2-io-surface",41614258 "objc2-metal",
@@ -4167,7 +4264,7 @@ version = "0.3.2"
41674264 source = "registry+https://github.com/rust-lang/crates.io-index"41684265 checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"41694266 dependencies = [4170- "objc2",4267+ "objc2 0.6.4",41714268 "objc2-foundation",41724269 ]41734270
@@ -4177,7 +4274,7 @@ version = "0.3.2"
41774274 source = "registry+https://github.com/rust-lang/crates.io-index"41784275 checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"41794276 dependencies = [4180- "objc2",4277+ "objc2 0.6.4",41814278 "objc2-foundation",41824279 ]41834280
@@ -4188,9 +4285,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
41884285 checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"41894286 dependencies = [41904287 "bitflags 2.13.0",4191- "block2",4288+ "block2 0.6.2",41924289 "dispatch2",4193- "objc2",4290+ "objc2 0.6.4",41944291 "objc2-core-audio",41954292 "objc2-core-audio-types",41964293 "objc2-core-foundation",
@@ -4204,7 +4301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42044301 checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"42054302 dependencies = [42064303 "bitflags 2.13.0",4207- "objc2",4304+ "objc2 0.6.4",42084305 "objc2-core-foundation",42094306 "objc2-core-graphics",42104307 ]
@@ -4216,8 +4313,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42164313 checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"42174314 dependencies = [42184315 "bitflags 2.13.0",4219- "block2",4220- "objc2",4316+ "block2 0.6.2",4317+ "objc2 0.6.4",42214318 "objc2-core-foundation",42224319 "objc2-core-graphics",42234320 "objc2-io-surface",
@@ -4231,7 +4328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42314328 checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48"42324329 dependencies = [42334330 "bitflags 2.13.0",4234- "objc2",4331+ "objc2 0.6.4",42354332 "objc2-core-foundation",42364333 "objc2-foundation",42374334 "objc2-security",
@@ -4260,9 +4357,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42604357 checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"42614358 dependencies = [42624359 "bitflags 2.13.0",4263- "block2",4360+ "block2 0.6.2",42644361 "libc",4265- "objc2",4362+ "objc2 0.6.4",42664363 "objc2-core-foundation",42674364 ]42684365
@@ -4272,7 +4369,7 @@ version = "0.3.2"
42724369 source = "registry+https://github.com/rust-lang/crates.io-index"42734370 checksum = "32b0446e98cf4a784cc7a0177715ff317eeaa8463841c616cfc78aa4f953c4ea"42744371 dependencies = [4275- "objc2",4372+ "objc2 0.6.4",42764373 "objc2-core-foundation",42774374 "objc2-core-graphics",42784375 ]
@@ -4284,7 +4381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42844381 checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"42854382 dependencies = [42864383 "bitflags 2.13.0",4287- "objc2",4384+ "objc2 0.6.4",42884385 "objc2-core-foundation",42894386 ]42904387
@@ -4294,7 +4391,7 @@ version = "0.3.2"
42944391 source = "registry+https://github.com/rust-lang/crates.io-index"42954392 checksum = "edd9fdde720df3da7046bb9097811000c1e7ab5cd579fa89d96b27d56781fb30"42964393 dependencies = [4297- "objc2",4394+ "objc2 0.6.4",42984395 "objc2-core-audio-types",42994396 "objc2-core-foundation",43004397 "objc2-core-media",
@@ -4307,7 +4404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43074404 checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"43084405 dependencies = [43094406 "bitflags 2.13.0",4310- "objc2",4407+ "objc2 0.6.4",43114408 "objc2-foundation",43124409 ]43134410
@@ -4318,7 +4415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43184415 checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"43194416 dependencies = [43204417 "bitflags 2.13.0",4321- "objc2",4418+ "objc2 0.6.4",43224419 "objc2-core-foundation",43234420 "objc2-foundation",43244421 ]
@@ -4330,7 +4427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43304427 checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"43314428 dependencies = [43324429 "bitflags 2.13.0",4333- "objc2",4430+ "objc2 0.6.4",43344431 "objc2-core-foundation",43354432 ]43364433
@@ -4340,7 +4437,7 @@ version = "0.3.2"
43404437 source = "registry+https://github.com/rust-lang/crates.io-index"43414438 checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5"43424439 dependencies = [4343- "objc2",4440+ "objc2 0.6.4",43444441 "objc2-foundation",43454442 ]43464443
@@ -4353,7 +4450,7 @@ dependencies = [
43534450 "bitflags 2.13.0",43544451 "dispatch2",43554452 "libc",4356- "objc2",4453+ "objc2 0.6.4",43574454 "objc2-core-foundation",43584455 "objc2-security",43594456 ]
@@ -4365,8 +4462,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43654462 checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"43664463 dependencies = [43674464 "bitflags 2.13.0",4368- "block2",4369- "objc2",4465+ "block2 0.6.2",4466+ "objc2 0.6.4",43704467 "objc2-cloud-kit",43714468 "objc2-core-data",43724469 "objc2-core-foundation",
@@ -4385,7 +4482,7 @@ version = "0.3.2"
43854482 source = "registry+https://github.com/rust-lang/crates.io-index"43864483 checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"43874484 dependencies = [4388- "objc2",4485+ "objc2 0.6.4",43894486 "objc2-foundation",43904487 ]43914488
@@ -4396,8 +4493,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
43964493 checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"43974494 dependencies = [43984495 "bitflags 2.13.0",4399- "block2",4400- "objc2",4496+ "block2 0.6.2",4497+ "objc2 0.6.4",44014498 "objc2-app-kit",44024499 "objc2-core-foundation",44034500 "objc2-foundation",
@@ -5874,7 +5971,7 @@ dependencies = [
58745971 "bytemuck",58755972 "js-sys",58765973 "ndk",5877- "objc2",5974+ "objc2 0.6.4",58785975 "objc2-core-foundation",58795976 "objc2-core-graphics",58805977 "objc2-foundation",
@@ -6122,9 +6219,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
61226219 checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"61236220 dependencies = [61246221 "bitflags 2.13.0",6125- "block2",6222+ "block2 0.6.2",61266223 "core-foundation 0.10.1",6127- "core-graphics",6224+ "core-graphics 0.25.0",61286225 "crossbeam-channel",61296226 "dbus",61306227 "dispatch2",
@@ -6138,7 +6235,7 @@ dependencies = [
61386235 "log",61396236 "ndk",61406237 "ndk-sys",6141- "objc2",6238+ "objc2 0.6.4",61426239 "objc2-app-kit",61436240 "objc2-foundation",61446241 "objc2-ui-kit",
@@ -6206,7 +6303,7 @@ dependencies = [
62066303 "log",62076304 "mime",62086305 "muda",6209- "objc2",6306+ "objc2 0.6.4",62106307 "objc2-app-kit",62116308 "objc2-foundation",62126309 "objc2-ui-kit",
@@ -6323,7 +6420,7 @@ dependencies = [
63236420 "byte-unit",63246421 "fern",63256422 "log",6326- "objc2",6423+ "objc2 0.6.4",63276424 "objc2-foundation",63286425 "serde",63296426 "serde_json",
@@ -6346,7 +6443,7 @@ dependencies = [
63466443 "gtk",63476444 "http",63486445 "jni 0.21.1",6349- "objc2",6446+ "objc2 0.6.4",63506447 "objc2-ui-kit",63516448 "objc2-web-kit",63526449 "raw-window-handle",
@@ -6370,7 +6467,7 @@ dependencies = [
63706467 "http",63716468 "jni 0.21.1",63726469 "log",6373- "objc2",6470+ "objc2 0.6.4",63746471 "objc2-app-kit",63756472 "once_cell",63766473 "percent-encoding",
@@ -6889,7 +6986,7 @@ dependencies = [
68896986 "dirs",68906987 "libappindicator",68916988 "muda",6892- "objc2",6989+ "objc2 0.6.4",68936990 "objc2-app-kit",68946991 "objc2-core-foundation",68956992 "objc2-core-graphics",
@@ -7447,8 +7544,8 @@ dependencies = [
74477544 "webview2-com-sys",74487545 "windows 0.61.3",74497546 "windows-core 0.61.2",7450- "windows-implement",7451- "windows-interface",7547+ "windows-implement 0.60.2",7548+ "windows-interface 0.59.3",74527549 ]74537550 74547551 [[package]]
@@ -7516,7 +7613,7 @@ version = "0.6.0"
75167613 source = "registry+https://github.com/rust-lang/crates.io-index"75177614 checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"75187615 dependencies = [7519- "objc2",7616+ "objc2 0.6.4",75207617 "objc2-app-kit",75217618 "objc2-core-foundation",75227619 "objc2-foundation",
@@ -7525,6 +7622,16 @@ dependencies = [
75257622 "windows-version",75267623 ]75277624 7625+[[package]]7626+name = "windows"7627+version = "0.56.0"7628+source = "registry+https://github.com/rust-lang/crates.io-index"7629+checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"7630+dependencies = [7631+ "windows-core 0.56.0",7632+ "windows-targets 0.52.6",7633+]7634+75287635 [[package]]75297636 name = "windows"75307637 version = "0.61.3"
@@ -7568,14 +7675,26 @@ dependencies = [
75687675 "windows-core 0.62.2",75697676 ]75707677 7678+[[package]]7679+name = "windows-core"7680+version = "0.56.0"7681+source = "registry+https://github.com/rust-lang/crates.io-index"7682+checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"7683+dependencies = [7684+ "windows-implement 0.56.0",7685+ "windows-interface 0.56.0",7686+ "windows-result 0.1.2",7687+ "windows-targets 0.52.6",7688+]7689+75717690 [[package]]75727691 name = "windows-core"75737692 version = "0.61.2"75747693 source = "registry+https://github.com/rust-lang/crates.io-index"75757694 checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"75767695 dependencies = [7577- "windows-implement",7578- "windows-interface",7696+ "windows-implement 0.60.2",7697+ "windows-interface 0.59.3",75797698 "windows-link 0.1.3",75807699 "windows-result 0.3.4",75817700 "windows-strings 0.4.2",
@@ -7587,8 +7706,8 @@ version = "0.62.2"
75877706 source = "registry+https://github.com/rust-lang/crates.io-index"75887707 checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"75897708 dependencies = [7590- "windows-implement",7591- "windows-interface",7709+ "windows-implement 0.60.2",7710+ "windows-interface 0.59.3",75927711 "windows-link 0.2.1",75937712 "windows-result 0.4.1",75947713 "windows-strings 0.5.1",
@@ -7616,6 +7735,17 @@ dependencies = [
76167735 "windows-threading 0.2.1",76177736 ]76187737 7738+[[package]]7739+name = "windows-implement"7740+version = "0.56.0"7741+source = "registry+https://github.com/rust-lang/crates.io-index"7742+checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"7743+dependencies = [7744+ "proc-macro2",7745+ "quote",7746+ "syn 2.0.118",7747+]7748+76197749 [[package]]76207750 name = "windows-implement"76217751 version = "0.60.2"
@@ -7627,6 +7757,17 @@ dependencies = [
76277757 "syn 2.0.118",76287758 ]76297759 7760+[[package]]7761+name = "windows-interface"7762+version = "0.56.0"7763+source = "registry+https://github.com/rust-lang/crates.io-index"7764+checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"7765+dependencies = [7766+ "proc-macro2",7767+ "quote",7768+ "syn 2.0.118",7769+]7770+76307771 [[package]]76317772 name = "windows-interface"76327773 version = "0.59.3"
@@ -7681,6 +7822,15 @@ dependencies = [
76817822 "windows-strings 0.5.1",76827823 ]76837824 7825+[[package]]7826+name = "windows-result"7827+version = "0.1.2"7828+source = "registry+https://github.com/rust-lang/crates.io-index"7829+checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"7830+dependencies = [7831+ "windows-targets 0.52.6",7832+]7833+76847834 [[package]]76857835 name = "windows-result"76867836 version = "0.3.4"
@@ -7972,7 +8122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
79728122 checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"79738123 dependencies = [79748124 "base64 0.22.1",7975- "block2",8125+ "block2 0.6.2",79768126 "cookie",79778127 "crossbeam-channel",79788128 "dirs",
@@ -7986,7 +8136,7 @@ dependencies = [
79868136 "jni 0.21.1",79878137 "libc",79888138 "ndk",7989- "objc2",8139+ "objc2 0.6.4",79908140 "objc2-app-kit",79918141 "objc2-core-foundation",79928142 "objc2-foundation",
@@ -8085,7 +8235,7 @@ dependencies = [
80858235 "image",80868236 "libwayshot-xcap",80878237 "log",8088- "objc2",8238+ "objc2 0.6.4",80898239 "objc2-app-kit",80908240 "objc2-av-foundation",80918241 "objc2-core-foundation",
@@ -8117,6 +8267,23 @@ dependencies = [
81178267 "quick-xml 0.30.0",81188268 ]81198269 8270+[[package]]8271+name = "xkbcommon"8272+version = "0.7.0"8273+source = "registry+https://github.com/rust-lang/crates.io-index"8274+checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e"8275+dependencies = [8276+ "libc",8277+ "memmap2 0.8.0",8278+ "xkeysym",8279+]8280+8281+[[package]]8282+name = "xkeysym"8283+version = "0.2.1"8284+source = "registry+https://github.com/rust-lang/crates.io-index"8285+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"8286+81208287 [[package]]81218288 name = "xml-rs"81228289 version = "0.8.28"
M src-tauri/Cargo.toml +1 -0
M src-tauri/src/commands.rs +305 -18
@@ -3,29 +3,168 @@
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, stream screen frames to clients6+//! - iroh_host_start: start Iroh endpoint, stream screen frames + accept input77 //! - iroh_host_status: check if host endpoint is running8-//! - iroh_client_connect: connect to a host, receive frames, emit "frame" events8+//! - iroh_client_connect: connect to host, receive frames, emit events9+//! - iroh_client_send_input: send input event to host for injection910 1011 use anyhow::Context as _;1112 use base64::Engine;1213 use ctap_hid_fido2::{Cfg, FidoKeyHidFactory};14+use enigo::{Button, Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings};1315 use iroh::endpoint::{presets, Connection};1416 use iroh::protocol::{ProtocolHandler, Router};1517 use iroh::{Endpoint, EndpointAddr, SecretKey};16-use serde::Serialize;18+use serde::{Deserialize, Serialize};1719 use std::io::Cursor;1820 use std::sync::{Arc, Mutex};1921 use std::time::{Duration, Instant};2022 use tauri::{AppHandle, Emitter, State};21-use tokio::io::{AsyncReadExt, AsyncWriteExt};23+use tokio::sync::Mutex as TokioMutex;24+25+const FRAME_ALPN: &[u8] = b"keyhome/frame-stream/0";26+const INPUT_ALPN: &[u8] = b"keyhome/input-stream/0";27+28+// ─── Input Event Protocol ───────────────────────────────────────────────────29+30+#[derive(Serialize, Deserialize, Debug, Clone)]31+#[serde(tag = "t")]32+pub enum InputEvent {33+ #[serde(rename = "mm")]34+ MouseMove { x: i32, y: i32 },35+ #[serde(rename = "mc")]36+ MouseClick { b: u8 },37+ #[serde(rename = "md")]38+ MouseDown { b: u8 },39+ #[serde(rename = "mu")]40+ MouseUp { b: u8 },41+ #[serde(rename = "ms")]42+ MouseScroll { dx: i32, dy: i32 },43+ #[serde(rename = "kd")]44+ KeyDown { k: String },45+ #[serde(rename = "ku")]46+ KeyUp { k: String },47+ #[serde(rename = "kt")]48+ KeyClick { k: String },49+ #[serde(rename = "tx")]50+ Text { s: String },51+}52+53+impl InputEvent {54+ fn apply(&self, enigo: &mut Enigo) -> anyhow::Result<()> {55+ match self {56+ InputEvent::MouseMove { x, y } => {57+ enigo58+ .move_mouse(*x, *y, Coordinate::Abs)59+ .map_err(|e| anyhow::anyhow!("mouse move: {:?}", e))?;60+ }61+ InputEvent::MouseClick { b } => {62+ let btn = button_from_code(*b);63+ enigo64+ .button(btn, Direction::Click)65+ .map_err(|e| anyhow::anyhow!("mouse click: {:?}", e))?;66+ }67+ InputEvent::MouseDown { b } => {68+ let btn = button_from_code(*b);69+ enigo70+ .button(btn, Direction::Press)71+ .map_err(|e| anyhow::anyhow!("mouse down: {:?}", e))?;72+ }73+ InputEvent::MouseUp { b } => {74+ let btn = button_from_code(*b);75+ enigo76+ .button(btn, Direction::Release)77+ .map_err(|e| anyhow::anyhow!("mouse up: {:?}", e))?;78+ }79+ InputEvent::MouseScroll { dx, dy } => {80+ if *dy != 0 {81+ enigo82+ .scroll(*dy, enigo::Axis::Vertical)83+ .map_err(|e| anyhow::anyhow!("scroll: {:?}", e))?;84+ }85+ if *dx != 0 {86+ enigo87+ .scroll(*dx, enigo::Axis::Horizontal)88+ .map_err(|e| anyhow::anyhow!("scroll: {:?}", e))?;89+ }90+ }91+ InputEvent::KeyDown { k } => {92+ if let Some(key) = key_from_str(k) {93+ enigo94+ .key(key, Direction::Press)95+ .map_err(|e| anyhow::anyhow!("key down: {:?}", e))?;96+ }97+ }98+ InputEvent::KeyUp { k } => {99+ if let Some(key) = key_from_str(k) {100+ enigo101+ .key(key, Direction::Release)102+ .map_err(|e| anyhow::anyhow!("key up: {:?}", e))?;103+ }104+ }105+ InputEvent::KeyClick { k } => {106+ if let Some(key) = key_from_str(k) {107+ enigo108+ .key(key, Direction::Click)109+ .map_err(|e| anyhow::anyhow!("key click: {:?}", e))?;110+ }111+ }112+ InputEvent::Text { s } => {113+ enigo114+ .text(s)115+ .map_err(|e| anyhow::anyhow!("text: {:?}", e))?;116+ }117+ }118+ Ok(())119+ }120+}121+122+fn button_from_code(b: u8) -> Button {123+ match b {124+ 2 => Button::Right,125+ 3 => Button::Middle,126+ 4 => Button::Back,127+ 5 => Button::Forward,128+ _ => Button::Left,129+ }130+}22131 23-const ALPN: &[u8] = b"keyhome/frame-stream/0";132+fn key_from_str(s: &str) -> Option<Key> {133+ match s {134+ "Enter" => Some(Key::Return),135+ "Tab" => Some(Key::Tab),136+ "Space" => Some(Key::Space),137+ "Backspace" => Some(Key::Backspace),138+ "Escape" => Some(Key::Escape),139+ "Shift" => Some(Key::Shift),140+ "Control" => Some(Key::Control),141+ "Alt" => Some(Key::Alt),142+ "Meta" => Some(Key::Meta),143+ "Up" => Some(Key::UpArrow),144+ "Down" => Some(Key::DownArrow),145+ "Left" => Some(Key::LeftArrow),146+ "Right" => Some(Key::RightArrow),147+ "Home" => Some(Key::Home),148+ "End" => Some(Key::End),149+ "PageUp" => Some(Key::PageUp),150+ "PageDown" => Some(Key::PageDown),151+ "Delete" => Some(Key::Delete),152+ _ => {153+ let c = s.chars().next()?;154+ if c.is_ascii() {155+ Some(Key::Unicode(c))156+ } else {157+ None158+ }159+ }160+ }161+}24162 25163 // ─── AppState ────────────────────────────────────────────────────────────────26164 27165 pub struct AppState {28166 pub host: Mutex<Option<HostState>>,167+ pub input_send: TokioMutex<Option<tokio::sync::mpsc::UnboundedSender<InputEvent>>>,29168 }30169 31170 pub struct HostState {
@@ -36,6 +175,7 @@ impl Default for AppState {
36175 fn default() -> Self {37176 Self {38177 host: Mutex::new(None),178+ input_send: TokioMutex::new(None),39179 }40180 }41181 }
@@ -182,9 +322,14 @@ pub async fn iroh_host_start(state: State<'_, AppState>) -> Result<HostStatus, S
182322 let addr_json =183323 serde_json::to_string(&addr).map_err(|e| format!("Failed to serialize addr: {}", e))?;184324 185- let handler = Arc::new(FrameStreamHandler);325+ // Frame stream handler (existing)326+ let frame_handler = Arc::new(FrameStreamHandler);327+ // Input handler (new)328+ let input_handler = Arc::new(InputStreamHandler);329+186330 let router = Router::builder(endpoint.clone())187- .accept(ALPN, handler)331+ .accept(FRAME_ALPN, frame_handler)332+ .accept(INPUT_ALPN, input_handler)188333 .spawn();189334 190335 // Keep endpoint and router alive by leaking — MVP tradeoff
@@ -227,6 +372,8 @@ pub fn iroh_host_status(state: State<'_, AppState>) -> HostStatus {
227372 }228373 }229374 375+// ─── Frame Stream Handler (host side) ────────────────────────────────────────376+230377 #[derive(Debug)]231378 struct FrameStreamHandler;232379
@@ -301,6 +448,82 @@ async fn stream_frames(conn: Connection) -> anyhow::Result<()> {
301448 Ok(())302449 }303450 451+// ─── Input Stream Handler (host side) ─────────────────────────────────────────452+453+#[derive(Debug)]454+struct InputStreamHandler;455+456+impl ProtocolHandler for InputStreamHandler {457+ async fn accept(&self, conn: Connection) -> Result<(), iroh::protocol::AcceptError> {458+ if let Err(e) = handle_input(conn).await {459+ eprintln!("[host] input error: {}", e);460+ }461+ Ok(())462+ }463+}464+465+async fn handle_input(conn: Connection) -> anyhow::Result<()> {466+ let (mut send, mut recv) = conn.accept_bi().await?;467+ eprintln!("[host] input client connected: {}", conn.remote_id());468+469+ // Wait for start signal470+ let mut start_buf = [0u8; 1];471+ recv.read_exact(&mut start_buf).await?;472+ if start_buf[0] != 1 {473+ return Ok(());474+ }475+476+ let mut enigo = Enigo::new(&Settings::default())477+ .map_err(|e| anyhow::anyhow!("enigo init: {:?}", e))?;478+479+ let mut buf = Vec::new();480+ let mut chunk = [0u8; 1024];481+482+ loop {483+ let n = match recv.read(&mut chunk).await {484+ Ok(Some(n)) => n,485+ Ok(None) => {486+ eprintln!("[host] input client disconnected");487+ break;488+ }489+ Err(e) => {490+ eprintln!("[host] input read error: {}", e);491+ break;492+ }493+ };494+ if n == 0 {495+ break;496+ }497+498+ buf.extend_from_slice(&chunk[..n]);499+500+ // Process complete newline-delimited JSON events501+ while let Some(pos) = buf.iter().position(|&b| b == b'\n') {502+ let line: Vec<u8> = buf.drain(..=pos).collect();503+ let line_str = String::from_utf8_lossy(&line[..line.len() - 1]);504+505+ if line_str.trim().is_empty() {506+ continue;507+ }508+509+ match serde_json::from_str::<InputEvent>(&line_str) {510+ Ok(event) => {511+ eprintln!("[host] input: {:?}", event);512+ if let Err(e) = event.apply(&mut enigo) {513+ eprintln!("[host] inject error: {}", e);514+ }515+ }516+ Err(e) => {517+ eprintln!("[host] parse error: {} (line: {})", e, line_str);518+ }519+ }520+ }521+ }522+523+ let _ = send.write_all(b"bye\n").await;524+ Ok(())525+}526+304527 // ─── Iroh Client ─────────────────────────────────────────────────────────────305528 306529 #[derive(Serialize, Clone)]
@@ -319,7 +542,7 @@ pub struct ConnectResult {
319542 #[tauri::command]320543 pub async fn iroh_client_connect(321544 app: AppHandle,322- _state: State<'_, AppState>,545+ state: State<'_, AppState>,323546 addr_json: String,324547 ) -> Result<ConnectResult, String> {325548 let addr: EndpointAddr = serde_json::from_str(&addr_json)
@@ -334,21 +557,67 @@ pub async fn iroh_client_connect(
334557 335558 let _ = tokio::time::timeout(Duration::from_secs(10), endpoint.online()).await;336559 337- let conn = endpoint338- .connect(addr, ALPN)560+ // Connect frame stream561+ let frame_conn = endpoint562+ .connect(addr.clone(), FRAME_ALPN)339563 .await340- .map_err(|e| format!("Failed to connect: {}", e))?;564+ .map_err(|e| format!("Failed to connect frame stream: {}", e))?;341565 342- let (mut send, mut recv) = conn566+ let (mut frame_send, mut frame_recv) = frame_conn343567 .open_bi()344568 .await345- .map_err(|e| format!("Failed to open stream: {}", e))?;569+ .map_err(|e| format!("Failed to open frame stream: {}", e))?;346570 347571 // Send start signal348- send.write_all(&[1u8])572+ frame_send573+ .write_all(&[1u8])349574 .await350575 .map_err(|e| format!("Failed to send start: {}", e))?;351576 577+ // Connect input stream578+ let input_conn = endpoint579+ .connect(addr, INPUT_ALPN)580+ .await581+ .map_err(|e| format!("Failed to connect input stream: {}", e))?;582+583+ let (mut input_send, mut _input_recv) = input_conn584+ .open_bi()585+ .await586+ .map_err(|e| format!("Failed to open input stream: {}", e))?;587+588+ // Send start signal for input589+ input_send590+ .write_all(&[1u8])591+ .await592+ .map_err(|e| format!("Failed to send input start: {}", e))?;593+594+ // Store input sender in state595+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<InputEvent>();596+ {597+ let mut input_send_guard = state.input_send.lock().await;598+ *input_send_guard = Some(tx);599+ }600+601+ // Spawn input forwarder: reads from channel, writes to Iroh stream602+ let mut input_stream = input_send;603+ tokio::spawn(async move {604+ let mut rx = rx;605+ while let Some(event) = rx.recv().await {606+ let json = match serde_json::to_string(&event) {607+ Ok(j) => j + "\n",608+ Err(e) => {609+ eprintln!("[client] serialize error: {}", e);610+ continue;611+ }612+ };613+ if input_stream.write_all(json.as_bytes()).await.is_err() {614+ eprintln!("[client] input stream write failed, disconnecting");615+ break;616+ }617+ }618+ let _ = input_stream.finish();619+ });620+352621 // Spawn background task to read frames and emit events353622 tokio::spawn(async move {354623 let mut frame_count = 0u32;
@@ -356,7 +625,7 @@ pub async fn iroh_client_connect(
356625 357626 loop {358627 let mut header = [0u8; 12];359- if recv.read_exact(&mut header).await.is_err() {628+ if frame_recv.read_exact(&mut header).await.is_err() {360629 let _ = app.emit("frame-error", "Connection lost");361630 break;362631 }
@@ -367,7 +636,7 @@ pub async fn iroh_client_connect(
367636 u32::from_be_bytes([header[8], header[9], header[10], header[11]]) as usize;368637 369638 let mut jpeg_buf = vec![0u8; jpeg_len];370- if recv.read_exact(&mut jpeg_buf).await.is_err() {639+ if frame_recv.read_exact(&mut jpeg_buf).await.is_err() {371640 let _ = app.emit("frame-error", "Connection lost");372641 break;373642 }
@@ -385,10 +654,12 @@ pub async fn iroh_client_connect(
385654 data: b64,386655 },387656 );388- let _ = app.emit("frame-stats", serde_json::json!({ "fps": fps, "count": frame_count }));657+ let _ = app.emit(658+ "frame-stats",659+ serde_json::json!({ "fps": fps, "count": frame_count }),660+ );389661 }390662 391- // Keep endpoint alive until task ends392663 drop(endpoint);393664 });394665
@@ -397,3 +668,19 @@ pub async fn iroh_client_connect(
397668 error: None,398669 })399670 }671+672+#[tauri::command]673+pub async fn iroh_client_send_input(674+ state: State<'_, AppState>,675+ event: InputEvent,676+) -> Result<bool, String> {677+ let input_send = state.input_send.lock().await;678+ match &*input_send {679+ Some(tx) => {680+ tx.send(event)681+ .map_err(|e| format!("Input channel closed: {}", e))?;682+ Ok(true)683+ }684+ None => Err("Not connected to host".into()),685+ }686+}
M src-tauri/src/lib.rs +11 -0
M src/ui/index.html +183 -3
@@ -62,6 +62,10 @@
6262 background: rgba(88,166,255,0.1); border-color: rgba(88,166,255,0.3);6363 color: var(--accent);6464 }65+ .btn-danger {66+ background: rgba(248,81,73,0.1); border-color: rgba(248,81,73,0.3);67+ color: var(--red);68+ }6569 .btn-bar { display: flex; gap: 8px; margin-top: 12px; }6670 .addr-box {6771 margin-top: 12px; padding: 8px 12px;
@@ -93,7 +97,11 @@
9397 overflow: hidden; background: #000; position: relative;9498 min-height: 300px; display: flex; align-items: center; justify-content: center;9599 }96- #frame-canvas { max-width: 100%; max-height: 500px; display: none; }100+ #frame-canvas {101+ max-width: 100%; max-height: 500px; display: none;102+ cursor: crosshair;103+ }104+ #frame-canvas.control-active { cursor: default; }97105 .viewer-placeholder {98106 color: var(--text-dim); font-size: 13px; padding: 60px 20px; text-align: center;99107 }
@@ -109,6 +117,27 @@
109117 font-size: 12px; margin-left: 8px;110118 }111119 .copy-btn:hover { color: var(--accent); }120+ .control-bar {121+ display: flex; align-items: center; gap: 12px; margin-top: 12px;122+ }123+ .control-toggle {124+ display: flex; align-items: center; gap: 8px; cursor: pointer;125+ padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border);126+ background: var(--surface); font-size: 13px;127+ transition: all 0.2s;128+ }129+ .control-toggle:hover { border-color: var(--accent); }130+ .control-toggle.active {131+ background: rgba(63,185,80,0.1); border-color: rgba(63,185,80,0.3);132+ color: var(--green);133+ }134+ .control-toggle .dot {135+ width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim);136+ }137+ .control-toggle.active .dot { background: var(--green); }138+ .control-hint {139+ color: var(--text-dim); font-size: 11px;140+ }112141 </style>113142 </head>114143 <body>
@@ -181,7 +210,7 @@
181210 <div id="connect-error" class="error-msg"></div>182211 </div>183212 184- <!-- Frame Viewer -->213+ <!-- Frame Viewer + Input Control -->185214 <div class="card viewer-card">186215 <h2>Remote Screen <span id="viewer-stats" class="viewer-stats"></span></h2>187216 <div class="viewer-wrap">
@@ -191,11 +220,20 @@
191220 and connect from another.192221 </div>193222 </div>223+ <div class="control-bar">224+ <div class="control-toggle" id="control-toggle" onclick="toggleControl()">225+ <span class="dot"></span>226+ <span id="control-label">Take Control</span>227+ </div>228+ <span class="control-hint" id="control-hint">Connect to a host first</span>229+ </div>194230 </div>195231 </div>196232 197233 <script>198234 let hostAddrJson = '';235+ let controlMode = false;236+ let connected = false;199237 200238 // Access Tauri API lazily — avoids TDZ errors if __TAURI__ isn't ready yet201239 const invoke = (...args) => window.__TAURI__.core.invoke(...args);
@@ -287,11 +325,13 @@
287325 try {288326 const result = await invoke('iroh_client_connect', { addrJson: addr });289327 if (result.connected) {328+ connected = true;290329 errBox.style.color = 'var(--green)';291330 errBox.textContent = 'Connected! Streaming frames...';292331 errBox.classList.add('visible');293332 document.getElementById('frame-canvas').style.display = 'block';294333 document.getElementById('viewer-placeholder').style.display = 'none';334+ document.getElementById('control-hint').textContent = 'Click "Take Control" to send input';295335 } else {296336 errBox.style.color = 'var(--red)';297337 errBox.textContent = result.error || 'Connection failed';
@@ -307,10 +347,13 @@
307347 // ─── Frame rendering ───────────────────────────────────────────────308348 const canvas = document.getElementById('frame-canvas');309349 const ctx = canvas.getContext('2d');310- let lastFrameTime = 0;350+ let frameWidth = 0;351+ let frameHeight = 0;311352 312353 listen('frame', (event) => {313354 const { width, height, data } = event.payload;355+ frameWidth = width;356+ frameHeight = height;314357 if (canvas.width !== width) canvas.width = width;315358 if (canvas.height !== height) canvas.height = height;316359
@@ -339,6 +382,143 @@
339382 document.getElementById('frame-canvas').style.display = 'none';340383 document.getElementById('viewer-placeholder').style.display = 'block';341384 document.getElementById('viewer-placeholder').textContent = 'Disconnected: ' + event.payload;385+ connected = false;386+ controlMode = false;387+ updateControlUI();388+ });389+390+ // ─── Input Control ─────────────────────────────────────────────────391+392+ function toggleControl() {393+ if (!connected) return;394+ controlMode = !controlMode;395+ updateControlUI();396+ }397+398+ function updateControlUI() {399+ const toggle = document.getElementById('control-toggle');400+ const label = document.getElementById('control-label');401+ const hint = document.getElementById('control-hint');402+ const canvas = document.getElementById('frame-canvas');403+404+ if (controlMode) {405+ toggle.classList.add('active');406+ label.textContent = 'Controlling';407+ hint.textContent = 'Mouse and keyboard are being sent. Click to toggle off.';408+ canvas.classList.add('control-active');409+ } else {410+ toggle.classList.remove('active');411+ label.textContent = 'Take Control';412+ if (connected) {413+ hint.textContent = 'Click "Take Control" to send input';414+ } else {415+ hint.textContent = 'Connect to a host first';416+ }417+ canvas.classList.remove('control-active');418+ }419+ }420+421+ // Scale canvas click coordinates to remote frame coordinates422+ function scaleCoords(clientX, clientY) {423+ const rect = canvas.getBoundingClientRect();424+ const scaleX = frameWidth / rect.width;425+ const scaleY = frameHeight / rect.height;426+ return {427+ x: Math.round((clientX - rect.left) * scaleX),428+ y: Math.round((clientY - rect.top) * scaleY),429+ };430+ }431+432+ // Map JS key names to enigo key names433+ function mapKey(e) {434+ const key = e.key;435+ // Special keys436+ const map = {437+ 'ArrowUp': 'Up',438+ 'ArrowDown': 'Down',439+ 'ArrowLeft': 'Left',440+ 'ArrowRight': 'Right',441+ ' ': 'Space',442+ 'Delete': 'Delete',443+ 'Backspace': 'Backspace',444+ 'Enter': 'Enter',445+ 'Tab': 'Tab',446+ 'Escape': 'Escape',447+ 'Shift': 'Shift',448+ 'Control': 'Control',449+ 'Alt': 'Alt',450+ 'Meta': 'Meta',451+ 'Home': 'Home',452+ 'End': 'End',453+ 'PageUp': 'PageUp',454+ 'PageDown': 'PageDown',455+ };456+ return map[key] || (key.length === 1 ? key : null);457+ }458+459+ async function sendInput(event) {460+ try {461+ await invoke('iroh_client_send_input', { event });462+ } catch (e) {463+ // Silently ignore — input errors shouldn't disrupt the stream464+ console.warn('input send failed:', e);465+ }466+ }467+468+ // Mouse events on canvas469+ canvas.addEventListener('mousemove', (e) => {470+ if (!controlMode) return;471+ const { x, y } = scaleCoords(e.clientX, e.clientY);472+ sendInput({ t: 'mm', x, y });473+ });474+475+ canvas.addEventListener('mousedown', (e) => {476+ if (!controlMode) return;477+ e.preventDefault();478+ const btn = e.button === 2 ? 2 : (e.button === 1 ? 3 : 1);479+ sendInput({ t: 'md', b: btn });480+ });481+482+ canvas.addEventListener('mouseup', (e) => {483+ if (!controlMode) return;484+ e.preventDefault();485+ const btn = e.button === 2 ? 2 : (e.button === 1 ? 3 : 1);486+ sendInput({ t: 'mu', b: btn });487+ });488+489+ canvas.addEventListener('contextmenu', (e) => {490+ if (!controlMode) return;491+ e.preventDefault();492+ });493+494+ canvas.addEventListener('wheel', (e) => {495+ if (!controlMode) return;496+ e.preventDefault();497+ sendInput({ t: 'ms', dx: Math.round(e.deltaX), dy: Math.round(e.deltaY) });498+ }, { passive: false });499+500+ // Keyboard events — capture on window when control mode is active501+ window.addEventListener('keydown', (e) => {502+ if (!controlMode) return;503+ e.preventDefault();504+ const k = mapKey(e);505+ if (k) sendInput({ t: 'kd', k });506+ });507+508+ window.addEventListener('keyup', (e) => {509+ if (!controlMode) return;510+ e.preventDefault();511+ const k = mapKey(e);512+ if (k) sendInput({ t: 'ku', k });513+ });514+515+ // Text input for printable characters516+ window.addEventListener('keypress', (e) => {517+ if (!controlMode) return;518+ if (e.key.length === 1) {519+ e.preventDefault();520+ sendInput({ t: 'tx', s: e.key });521+ }342522 });343523 </script>344524 </body>