mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-15 09:01:15 +00:00
Phase 1: project scaffold, clap CLI, self-re-exec daemon, NDJSON IPC
- Cargo.toml with clap, tokio, serde, anyhow dependencies - Entry point with env-var routing to daemon or CLI mode - Core protocol types (Request/Response NDJSON wire format) - Session detection (X11 check with DISPLAY/XDG_SESSION_TYPE) - RefMap with @wN selector resolution (direct, prefix, substring) - Snapshot/WindowInfo shared types with Display impl - clap derive CLI with all subcommands (snapshot, click, type, etc.) - Client connection: socket path resolution, daemon auto-start via self-re-exec, NDJSON send/receive with retry backoff - Tokio async daemon: Unix socket listener, accept loop, graceful shutdown via notify - DaemonState holding session info and ref map - Placeholder handler returning hardcoded snapshot response
This commit is contained in:
parent
17d4a1edeb
commit
dfaa339594
13 changed files with 1735 additions and 0 deletions
818
Cargo.lock
generated
Normal file
818
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,818 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "desktop-ctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"dirs",
|
||||||
|
"libc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.183"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.50.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.2+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "desktop-ctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Desktop control CLI for AI agents - screenshot, click, type, window management on Linux X11"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/user/agent-computer"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
anyhow = "1"
|
||||||
|
dirs = "6"
|
||||||
|
libc = "0.2"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
164
src/cli/connection.rs
Normal file
164
src/cli/connection.rs
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use crate::cli::GlobalOpts;
|
||||||
|
use crate::core::protocol::{Request, Response};
|
||||||
|
|
||||||
|
fn socket_dir() -> PathBuf {
|
||||||
|
if let Ok(dir) = std::env::var("DESKTOP_CTL_SOCKET_DIR") {
|
||||||
|
return PathBuf::from(dir);
|
||||||
|
}
|
||||||
|
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
|
||||||
|
return PathBuf::from(runtime).join("desktop-ctl");
|
||||||
|
}
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||||
|
.join(".desktop-ctl")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_path(opts: &GlobalOpts) -> PathBuf {
|
||||||
|
if let Some(ref path) = opts.socket {
|
||||||
|
return path.clone();
|
||||||
|
}
|
||||||
|
socket_dir().join(format!("{}.sock", opts.session))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pid_path(opts: &GlobalOpts) -> PathBuf {
|
||||||
|
socket_dir().join(format!("{}.pid", opts.session))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_connect(opts: &GlobalOpts) -> Option<UnixStream> {
|
||||||
|
UnixStream::connect(socket_path(opts)).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_daemon(opts: &GlobalOpts) -> Result<()> {
|
||||||
|
let exe = std::env::current_exe()
|
||||||
|
.context("Failed to determine executable path")?;
|
||||||
|
|
||||||
|
let sock_dir = socket_dir();
|
||||||
|
std::fs::create_dir_all(&sock_dir)
|
||||||
|
.context("Failed to create socket directory")?;
|
||||||
|
|
||||||
|
let mut cmd = Command::new(exe);
|
||||||
|
cmd.env("DESKTOP_CTL_DAEMON", "1")
|
||||||
|
.env("DESKTOP_CTL_SESSION", &opts.session)
|
||||||
|
.env("DESKTOP_CTL_SOCKET_PATH", socket_path(opts))
|
||||||
|
.env("DESKTOP_CTL_PID_PATH", pid_path(opts))
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
// Detach the daemon process on Unix
|
||||||
|
unsafe {
|
||||||
|
cmd.pre_exec(|| {
|
||||||
|
libc::setsid();
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.spawn().context("Failed to spawn daemon")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_daemon(opts: &GlobalOpts) -> Result<UnixStream> {
|
||||||
|
// Try connecting first
|
||||||
|
if let Some(stream) = try_connect(opts) {
|
||||||
|
return Ok(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn daemon
|
||||||
|
spawn_daemon(opts)?;
|
||||||
|
|
||||||
|
// Retry with backoff
|
||||||
|
let max_retries = 20;
|
||||||
|
let base_delay = Duration::from_millis(50);
|
||||||
|
for i in 0..max_retries {
|
||||||
|
thread::sleep(base_delay * (i + 1).min(4));
|
||||||
|
if let Some(stream) = try_connect(opts) {
|
||||||
|
return Ok(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!(
|
||||||
|
"Failed to connect to daemon after {} retries.\n\
|
||||||
|
Socket path: {}",
|
||||||
|
max_retries,
|
||||||
|
socket_path(opts).display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_command(opts: &GlobalOpts, request: &Request) -> Result<Response> {
|
||||||
|
let mut stream = ensure_daemon(opts)?;
|
||||||
|
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
|
||||||
|
stream.set_write_timeout(Some(Duration::from_secs(5)))?;
|
||||||
|
|
||||||
|
// Send NDJSON request
|
||||||
|
let json = serde_json::to_string(request)?;
|
||||||
|
writeln!(stream, "{json}")?;
|
||||||
|
stream.flush()?;
|
||||||
|
|
||||||
|
// Read NDJSON response
|
||||||
|
let mut reader = BufReader::new(&stream);
|
||||||
|
let mut line = String::new();
|
||||||
|
reader.read_line(&mut line)?;
|
||||||
|
|
||||||
|
let response: Response = serde_json::from_str(line.trim())
|
||||||
|
.context("Failed to parse daemon response")?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_daemon(opts: &GlobalOpts) -> Result<()> {
|
||||||
|
if try_connect(opts).is_some() {
|
||||||
|
println!("Daemon already running ({})", socket_path(opts).display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
spawn_daemon(opts)?;
|
||||||
|
// Wait briefly and verify
|
||||||
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
if try_connect(opts).is_some() {
|
||||||
|
println!("Daemon started ({})", socket_path(opts).display());
|
||||||
|
} else {
|
||||||
|
bail!("Daemon failed to start");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_daemon(opts: &GlobalOpts) -> Result<()> {
|
||||||
|
match try_connect(opts) {
|
||||||
|
Some(mut stream) => {
|
||||||
|
let req = Request::new("shutdown");
|
||||||
|
let json = serde_json::to_string(&req)?;
|
||||||
|
writeln!(stream, "{json}")?;
|
||||||
|
stream.flush()?;
|
||||||
|
println!("Daemon stopped");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Try to clean up stale socket
|
||||||
|
let path = socket_path(opts);
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path)?;
|
||||||
|
println!("Removed stale socket: {}", path.display());
|
||||||
|
} else {
|
||||||
|
println!("Daemon not running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn daemon_status(opts: &GlobalOpts) -> Result<()> {
|
||||||
|
if try_connect(opts).is_some() {
|
||||||
|
println!("Daemon running ({})", socket_path(opts).display());
|
||||||
|
} else {
|
||||||
|
println!("Daemon not running");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
321
src/cli/mod.rs
Normal file
321
src/cli/mod.rs
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
mod connection;
|
||||||
|
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::core::protocol::{Request, Response};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "desktop-ctl", version, about = "Desktop control CLI for AI agents")]
|
||||||
|
pub struct App {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub global: GlobalOpts,
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct GlobalOpts {
|
||||||
|
/// Path to the daemon Unix socket
|
||||||
|
#[arg(long, global = true, env = "DESKTOP_CTL_SOCKET")]
|
||||||
|
pub socket: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Session name (allows multiple daemon instances)
|
||||||
|
#[arg(long, global = true, default_value = "default")]
|
||||||
|
pub session: String,
|
||||||
|
|
||||||
|
/// Output as JSON
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Take a screenshot and list windows with @wN refs
|
||||||
|
Snapshot {
|
||||||
|
/// Draw bounding boxes and labels on the screenshot
|
||||||
|
#[arg(long)]
|
||||||
|
annotate: bool,
|
||||||
|
},
|
||||||
|
/// Click a window ref or coordinates
|
||||||
|
Click {
|
||||||
|
/// @w1 or x,y coordinates
|
||||||
|
selector: String,
|
||||||
|
},
|
||||||
|
/// Double-click a window ref or coordinates
|
||||||
|
Dblclick {
|
||||||
|
/// @w1 or x,y coordinates
|
||||||
|
selector: String,
|
||||||
|
},
|
||||||
|
/// Type text into the focused window
|
||||||
|
Type {
|
||||||
|
/// Text to type
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
/// Press a key (e.g. enter, tab, escape)
|
||||||
|
Press {
|
||||||
|
/// Key name
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
/// Send a hotkey combination (e.g. ctrl c)
|
||||||
|
Hotkey {
|
||||||
|
/// Key names (e.g. ctrl shift t)
|
||||||
|
keys: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Mouse operations
|
||||||
|
#[command(subcommand)]
|
||||||
|
Mouse(MouseCmd),
|
||||||
|
/// Focus a window by ref or name
|
||||||
|
Focus {
|
||||||
|
/// @w1 or window name substring
|
||||||
|
selector: String,
|
||||||
|
},
|
||||||
|
/// Close a window by ref or name
|
||||||
|
Close {
|
||||||
|
/// @w1 or window name substring
|
||||||
|
selector: String,
|
||||||
|
},
|
||||||
|
/// Move a window
|
||||||
|
MoveWindow {
|
||||||
|
/// @w1 or window name substring
|
||||||
|
selector: String,
|
||||||
|
/// X position
|
||||||
|
x: i32,
|
||||||
|
/// Y position
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
|
/// Resize a window
|
||||||
|
ResizeWindow {
|
||||||
|
/// @w1 or window name substring
|
||||||
|
selector: String,
|
||||||
|
/// Width
|
||||||
|
w: u32,
|
||||||
|
/// Height
|
||||||
|
h: u32,
|
||||||
|
},
|
||||||
|
/// List all windows (same as snapshot but without screenshot)
|
||||||
|
ListWindows,
|
||||||
|
/// Get screen resolution
|
||||||
|
GetScreenSize,
|
||||||
|
/// Get current mouse position
|
||||||
|
GetMousePosition,
|
||||||
|
/// Take a screenshot without window tree
|
||||||
|
Screenshot {
|
||||||
|
/// Save path (default: /tmp/desktop-ctl-{timestamp}.png)
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
/// Draw bounding boxes and labels
|
||||||
|
#[arg(long)]
|
||||||
|
annotate: bool,
|
||||||
|
},
|
||||||
|
/// Launch an application
|
||||||
|
Launch {
|
||||||
|
/// Command to run
|
||||||
|
command: String,
|
||||||
|
/// Arguments
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
args: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Daemon management (hidden - internal use)
|
||||||
|
#[command(hide = true)]
|
||||||
|
Daemon(DaemonCmd),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum MouseCmd {
|
||||||
|
/// Move the mouse cursor
|
||||||
|
Move {
|
||||||
|
/// X coordinate
|
||||||
|
x: i32,
|
||||||
|
/// Y coordinate
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
|
/// Scroll the mouse wheel
|
||||||
|
Scroll {
|
||||||
|
/// Amount (positive = down, negative = up)
|
||||||
|
amount: i32,
|
||||||
|
/// Axis: vertical or horizontal
|
||||||
|
#[arg(long, default_value = "vertical")]
|
||||||
|
axis: String,
|
||||||
|
},
|
||||||
|
/// Drag from one position to another
|
||||||
|
Drag {
|
||||||
|
/// Start X
|
||||||
|
x1: i32,
|
||||||
|
/// Start Y
|
||||||
|
y1: i32,
|
||||||
|
/// End X
|
||||||
|
x2: i32,
|
||||||
|
/// End Y
|
||||||
|
y2: i32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct DaemonCmd {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub action: DaemonAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum DaemonAction {
|
||||||
|
/// Start the daemon
|
||||||
|
Start,
|
||||||
|
/// Stop the daemon
|
||||||
|
Stop,
|
||||||
|
/// Show daemon status
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() -> Result<()> {
|
||||||
|
let app = App::parse();
|
||||||
|
|
||||||
|
// Handle daemon subcommands that don't need a running daemon
|
||||||
|
if let Command::Daemon(ref cmd) = app.command {
|
||||||
|
return match cmd.action {
|
||||||
|
DaemonAction::Start => connection::start_daemon(&app.global),
|
||||||
|
DaemonAction::Stop => connection::stop_daemon(&app.global),
|
||||||
|
DaemonAction::Status => connection::daemon_status(&app.global),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other commands need a daemon connection
|
||||||
|
let request = build_request(&app.command)?;
|
||||||
|
let response = connection::send_command(&app.global, &request)?;
|
||||||
|
|
||||||
|
if app.global.json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&response)?);
|
||||||
|
} else {
|
||||||
|
print_response(&app.command, &response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(cmd: &Command) -> Result<Request> {
|
||||||
|
use serde_json::json;
|
||||||
|
let req = match cmd {
|
||||||
|
Command::Snapshot { annotate } => {
|
||||||
|
Request::new("snapshot")
|
||||||
|
.with_extra("annotate", json!(annotate))
|
||||||
|
}
|
||||||
|
Command::Click { selector } => {
|
||||||
|
Request::new("click")
|
||||||
|
.with_extra("selector", json!(selector))
|
||||||
|
}
|
||||||
|
Command::Dblclick { selector } => {
|
||||||
|
Request::new("dblclick")
|
||||||
|
.with_extra("selector", json!(selector))
|
||||||
|
}
|
||||||
|
Command::Type { text } => {
|
||||||
|
Request::new("type")
|
||||||
|
.with_extra("text", json!(text))
|
||||||
|
}
|
||||||
|
Command::Press { key } => {
|
||||||
|
Request::new("press")
|
||||||
|
.with_extra("key", json!(key))
|
||||||
|
}
|
||||||
|
Command::Hotkey { keys } => {
|
||||||
|
Request::new("hotkey")
|
||||||
|
.with_extra("keys", json!(keys))
|
||||||
|
}
|
||||||
|
Command::Mouse(sub) => match sub {
|
||||||
|
MouseCmd::Move { x, y } => {
|
||||||
|
Request::new("mouse-move")
|
||||||
|
.with_extra("x", json!(x))
|
||||||
|
.with_extra("y", json!(y))
|
||||||
|
}
|
||||||
|
MouseCmd::Scroll { amount, axis } => {
|
||||||
|
Request::new("mouse-scroll")
|
||||||
|
.with_extra("amount", json!(amount))
|
||||||
|
.with_extra("axis", json!(axis))
|
||||||
|
}
|
||||||
|
MouseCmd::Drag { x1, y1, x2, y2 } => {
|
||||||
|
Request::new("mouse-drag")
|
||||||
|
.with_extra("x1", json!(x1))
|
||||||
|
.with_extra("y1", json!(y1))
|
||||||
|
.with_extra("x2", json!(x2))
|
||||||
|
.with_extra("y2", json!(y2))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::Focus { selector } => {
|
||||||
|
Request::new("focus")
|
||||||
|
.with_extra("selector", json!(selector))
|
||||||
|
}
|
||||||
|
Command::Close { selector } => {
|
||||||
|
Request::new("close")
|
||||||
|
.with_extra("selector", json!(selector))
|
||||||
|
}
|
||||||
|
Command::MoveWindow { selector, x, y } => {
|
||||||
|
Request::new("move-window")
|
||||||
|
.with_extra("selector", json!(selector))
|
||||||
|
.with_extra("x", json!(x))
|
||||||
|
.with_extra("y", json!(y))
|
||||||
|
}
|
||||||
|
Command::ResizeWindow { selector, w, h } => {
|
||||||
|
Request::new("resize-window")
|
||||||
|
.with_extra("selector", json!(selector))
|
||||||
|
.with_extra("w", json!(w))
|
||||||
|
.with_extra("h", json!(h))
|
||||||
|
}
|
||||||
|
Command::ListWindows => Request::new("list-windows"),
|
||||||
|
Command::GetScreenSize => Request::new("get-screen-size"),
|
||||||
|
Command::GetMousePosition => Request::new("get-mouse-position"),
|
||||||
|
Command::Screenshot { path, annotate } => {
|
||||||
|
let mut req = Request::new("screenshot")
|
||||||
|
.with_extra("annotate", json!(annotate));
|
||||||
|
if let Some(p) = path {
|
||||||
|
req = req.with_extra("path", json!(p.to_string_lossy()));
|
||||||
|
}
|
||||||
|
req
|
||||||
|
}
|
||||||
|
Command::Launch { command, args } => {
|
||||||
|
Request::new("launch")
|
||||||
|
.with_extra("command", json!(command))
|
||||||
|
.with_extra("args", json!(args))
|
||||||
|
}
|
||||||
|
Command::Daemon(_) => unreachable!(),
|
||||||
|
};
|
||||||
|
Ok(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_response(cmd: &Command, response: &Response) -> Result<()> {
|
||||||
|
if !response.success {
|
||||||
|
if let Some(ref err) = response.error {
|
||||||
|
eprintln!("Error: {err}");
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
if let Some(ref data) = response.data {
|
||||||
|
// For snapshot, print compact text format
|
||||||
|
if matches!(cmd, Command::Snapshot { .. }) {
|
||||||
|
if let Some(screenshot) = data.get("screenshot").and_then(|v| v.as_str()) {
|
||||||
|
println!("Screenshot: {screenshot}");
|
||||||
|
}
|
||||||
|
if let Some(windows) = data.get("windows").and_then(|v| v.as_array()) {
|
||||||
|
println!("Windows:");
|
||||||
|
for w in windows {
|
||||||
|
let ref_id = w.get("ref_id").and_then(|v| v.as_str()).unwrap_or("?");
|
||||||
|
let title = w.get("title").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let focused = w.get("focused").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
let minimized = w.get("minimized").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
let x = w.get("x").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
|
let y = w.get("y").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
|
let width = w.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
let height = w.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
let state = if focused { "focused" } else if minimized { "hidden" } else { "visible" };
|
||||||
|
let display_title = if title.len() > 30 {
|
||||||
|
format!("{}...", &title[..27])
|
||||||
|
} else {
|
||||||
|
title.to_string()
|
||||||
|
};
|
||||||
|
println!("@{:<4} {:<30} ({:<7}) {},{} {}x{}", ref_id, display_title, state, x, y, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generic: print JSON data
|
||||||
|
println!("{}", serde_json::to_string_pretty(data)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
4
src/core/mod.rs
Normal file
4
src/core/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod refs;
|
||||||
|
pub mod session;
|
||||||
|
pub mod types;
|
||||||
49
src/core/protocol.rs
Normal file
49
src/core/protocol.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Request {
|
||||||
|
pub id: String,
|
||||||
|
pub action: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub extra: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub success: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub data: Option<Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub fn new(action: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
id: format!("r{}", std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_micros() % 1_000_000),
|
||||||
|
action: action.to_string(),
|
||||||
|
extra: Value::Object(serde_json::Map::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_extra(mut self, key: &str, value: Value) -> Self {
|
||||||
|
if let Value::Object(ref mut map) = self.extra {
|
||||||
|
map.insert(key.to_string(), value);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn ok(data: Value) -> Self {
|
||||||
|
Self { success: true, data: Some(data), error: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err(msg: impl Into<String>) -> Self {
|
||||||
|
Self { success: false, data: None, error: Some(msg.into()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/core/refs.rs
Normal file
75
src/core/refs.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct RefEntry {
|
||||||
|
pub xcb_id: u32,
|
||||||
|
pub app_class: String,
|
||||||
|
pub title: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub focused: bool,
|
||||||
|
pub minimized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct RefMap {
|
||||||
|
map: HashMap<String, RefEntry>,
|
||||||
|
next_ref: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl RefMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { map: HashMap::new(), next_ref: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.map.clear();
|
||||||
|
self.next_ref = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, entry: RefEntry) -> String {
|
||||||
|
let ref_id = format!("w{}", self.next_ref);
|
||||||
|
self.next_ref += 1;
|
||||||
|
self.map.insert(ref_id.clone(), entry);
|
||||||
|
ref_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a selector to a RefEntry.
|
||||||
|
/// Accepts: "@w1", "w1", "ref=w1", or a substring match on app_class/title.
|
||||||
|
pub fn resolve(&self, selector: &str) -> Option<&RefEntry> {
|
||||||
|
let normalized = selector
|
||||||
|
.strip_prefix('@')
|
||||||
|
.or_else(|| selector.strip_prefix("ref="))
|
||||||
|
.unwrap_or(selector);
|
||||||
|
|
||||||
|
// Try direct ref lookup
|
||||||
|
if let Some(entry) = self.map.get(normalized) {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try substring match on app_class or title (case-insensitive)
|
||||||
|
let lower = selector.to_lowercase();
|
||||||
|
self.map.values().find(|e| {
|
||||||
|
e.app_class.to_lowercase().contains(&lower)
|
||||||
|
|| e.title.to_lowercase().contains(&lower)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a selector to the center coordinates of the window.
|
||||||
|
pub fn resolve_to_center(&self, selector: &str) -> Option<(i32, i32)> {
|
||||||
|
self.resolve(selector).map(|e| {
|
||||||
|
(e.x + e.width as i32 / 2, e.y + e.height as i32 / 2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = (&String, &RefEntry)> {
|
||||||
|
self.map.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/core/session.rs
Normal file
45
src/core/session.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
|
pub enum SessionType {
|
||||||
|
X11,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_session() -> Result<SessionType> {
|
||||||
|
let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
|
||||||
|
|
||||||
|
match session_type.as_str() {
|
||||||
|
"x11" => {}
|
||||||
|
"" => {
|
||||||
|
// No XDG_SESSION_TYPE set - check for DISPLAY as fallback
|
||||||
|
if std::env::var("DISPLAY").is_err() {
|
||||||
|
bail!(
|
||||||
|
"No X11 session detected.\n\
|
||||||
|
XDG_SESSION_TYPE is not set and DISPLAY is not set.\n\
|
||||||
|
desktop-ctl requires an X11 session. Wayland support coming in v0.2."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"wayland" => {
|
||||||
|
bail!(
|
||||||
|
"Wayland session detected (XDG_SESSION_TYPE=wayland).\n\
|
||||||
|
desktop-ctl currently supports X11 only. Wayland/Hyprland support coming in v0.2."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
bail!(
|
||||||
|
"Unsupported session type: {other}\n\
|
||||||
|
desktop-ctl currently supports X11 only."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm DISPLAY is set for X11
|
||||||
|
if std::env::var("DISPLAY").is_err() {
|
||||||
|
bail!(
|
||||||
|
"X11 session detected but DISPLAY is not set.\n\
|
||||||
|
Ensure your X server is running and DISPLAY is exported."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SessionType::X11)
|
||||||
|
}
|
||||||
55
src/core/types.rs
Normal file
55
src/core/types.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Snapshot {
|
||||||
|
pub screenshot: String,
|
||||||
|
pub windows: Vec<WindowInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct WindowInfo {
|
||||||
|
pub ref_id: String,
|
||||||
|
pub xcb_id: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub app_name: String,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub focused: bool,
|
||||||
|
pub minimized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for WindowInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let state = if self.focused {
|
||||||
|
"focused"
|
||||||
|
} else if self.minimized {
|
||||||
|
"hidden"
|
||||||
|
} else {
|
||||||
|
"visible"
|
||||||
|
};
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"@{:<4} {:<30} ({:<7}) {},{} {}x{}",
|
||||||
|
self.ref_id,
|
||||||
|
truncate(&self.title, 30),
|
||||||
|
state,
|
||||||
|
self.x,
|
||||||
|
self.y,
|
||||||
|
self.width,
|
||||||
|
self.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn truncate(s: &str, max: usize) -> String {
|
||||||
|
if s.len() <= max {
|
||||||
|
s.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}...", &s[..max - 3])
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/daemon/handler.rs
Normal file
31
src/daemon/handler.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::core::protocol::{Request, Response};
|
||||||
|
use super::state::DaemonState;
|
||||||
|
|
||||||
|
pub async fn handle_request(
|
||||||
|
request: &Request,
|
||||||
|
_state: &Arc<Mutex<DaemonState>>,
|
||||||
|
) -> Response {
|
||||||
|
match request.action.as_str() {
|
||||||
|
"snapshot" => {
|
||||||
|
Response::ok(serde_json::json!({
|
||||||
|
"screenshot": "/tmp/desktop-ctl-placeholder.png",
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"ref_id": "w1",
|
||||||
|
"xcb_id": 0,
|
||||||
|
"title": "Placeholder Window",
|
||||||
|
"app_name": "placeholder",
|
||||||
|
"x": 0, "y": 0, "width": 1920, "height": 1080,
|
||||||
|
"focused": true, "minimized": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
action => {
|
||||||
|
Response::err(format!("Unknown action: {action}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/daemon/mod.rs
Normal file
127
src/daemon/mod.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
mod handler;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::net::UnixListener;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::core::session;
|
||||||
|
use state::DaemonState;
|
||||||
|
|
||||||
|
pub fn run() -> Result<()> {
|
||||||
|
// Validate session before starting
|
||||||
|
session::detect_session()?;
|
||||||
|
|
||||||
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
runtime.block_on(async_run())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn async_run() -> Result<()> {
|
||||||
|
let socket_path = std::env::var("DESKTOP_CTL_SOCKET_PATH")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.context("DESKTOP_CTL_SOCKET_PATH not set")?;
|
||||||
|
|
||||||
|
let pid_path = std::env::var("DESKTOP_CTL_PID_PATH")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// Clean up stale socket
|
||||||
|
if socket_path.exists() {
|
||||||
|
std::fs::remove_file(&socket_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write PID file
|
||||||
|
if let Some(ref pid_path) = pid_path {
|
||||||
|
std::fs::write(pid_path, std::process::id().to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = UnixListener::bind(&socket_path)
|
||||||
|
.context(format!("Failed to bind socket: {}", socket_path.display()))?;
|
||||||
|
|
||||||
|
let session = std::env::var("DESKTOP_CTL_SESSION").unwrap_or_else(|_| "default".to_string());
|
||||||
|
let state = Arc::new(Mutex::new(DaemonState::new(session, socket_path.clone())));
|
||||||
|
|
||||||
|
let shutdown = Arc::new(tokio::sync::Notify::new());
|
||||||
|
let shutdown_clone = shutdown.clone();
|
||||||
|
|
||||||
|
// Accept loop
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = listener.accept() => {
|
||||||
|
match result {
|
||||||
|
Ok((stream, _addr)) => {
|
||||||
|
let state = state.clone();
|
||||||
|
let shutdown = shutdown.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_connection(stream, state, shutdown).await {
|
||||||
|
eprintln!("Connection error: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Accept error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = shutdown_clone.notified() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if socket_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&socket_path);
|
||||||
|
}
|
||||||
|
if let Some(ref pid_path) = pid_path {
|
||||||
|
let _ = std::fs::remove_file(pid_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_connection(
|
||||||
|
stream: tokio::net::UnixStream,
|
||||||
|
state: Arc<Mutex<DaemonState>>,
|
||||||
|
shutdown: Arc<tokio::sync::Notify>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (reader, mut writer) = stream.into_split();
|
||||||
|
let mut reader = BufReader::new(reader);
|
||||||
|
let mut line = String::new();
|
||||||
|
|
||||||
|
reader.read_line(&mut line).await?;
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let request: crate::core::protocol::Request = serde_json::from_str(line)?;
|
||||||
|
|
||||||
|
// Handle shutdown specially - notify before writing so the accept loop
|
||||||
|
// exits even if the client has already closed the connection.
|
||||||
|
if request.action == "shutdown" {
|
||||||
|
shutdown.notify_one();
|
||||||
|
let response = crate::core::protocol::Response::ok(
|
||||||
|
serde_json::json!({"message": "Shutting down"})
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&response)?;
|
||||||
|
// Ignore write errors - client may have already closed the connection.
|
||||||
|
let _ = writer.write_all(format!("{json}\n").as_bytes()).await;
|
||||||
|
let _ = writer.flush().await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = handler::handle_request(&request, &state).await;
|
||||||
|
let json = serde_json::to_string(&response)?;
|
||||||
|
writer.write_all(format!("{json}\n").as_bytes()).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
19
src/daemon/state.rs
Normal file
19
src/daemon/state.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::core::refs::RefMap;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct DaemonState {
|
||||||
|
pub session: String,
|
||||||
|
pub socket_path: PathBuf,
|
||||||
|
pub ref_map: RefMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DaemonState {
|
||||||
|
pub fn new(session: String, socket_path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
session,
|
||||||
|
socket_path,
|
||||||
|
ref_map: RefMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.rs
Normal file
10
src/main.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
mod cli;
|
||||||
|
mod core;
|
||||||
|
mod daemon;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
if std::env::var("DESKTOP_CTL_DAEMON").is_ok() {
|
||||||
|
return daemon::run();
|
||||||
|
}
|
||||||
|
cli::run()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue