Compare commits

...

1 Commits

Author SHA1 Message Date
codegen-sh[bot]
65375a4f45 Add comprehensive test suite for Rust implementation
- Add integration tests for CLI commands (help, version, list, image)
- Add unit tests for all models and utilities
- Fix struct field names and types to match implementation
- Fix enum variants to match actual error types
- Update file path handling tests to match actual behavior
- All tests now pass successfully
- Binary builds and runs correctly
2025-06-08 08:15:26 +00:00
20 changed files with 3663 additions and 0 deletions

955
peekaboo-native/Cargo.lock generated Normal file
View File

@ -0,0 +1,955 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "assert_cmd"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]]
name = "cc"
version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "errno"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
[[package]]
name = "humantime"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "peekaboo"
version = "1.0.0-beta.21"
dependencies = [
"anyhow",
"assert_cmd",
"chrono",
"clap",
"env_logger",
"libc",
"log",
"nix",
"once_cell",
"predicates",
"serde",
"serde_json",
"tempfile",
"thiserror",
"uuid",
"windows",
"x11",
"xcb",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[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.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom",
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core 0.52.0",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "x11"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "xcb"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
dependencies = [
"bitflags 1.3.2",
"libc",
"quick-xml",
]

View File

@ -0,0 +1,74 @@
[package]
name = "peekaboo"
version = "1.0.0-beta.21"
edition = "2021"
authors = ["Codegen Bot <codegen@example.com>"]
description = "A cross-platform utility for screen capture, application listing, and window management"
license = "MIT"
repository = "https://github.com/steipete/Peekaboo"
[[bin]]
name = "peekaboo"
path = "src/main.rs"
[lib]
name = "peekaboo"
path = "src/lib.rs"
[dependencies]
# CLI and argument parsing
clap = { version = "4.4", features = ["derive", "env"] }
# JSON serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Error handling
anyhow = "1.0"
thiserror = "1.0"
# Logging
log = "0.4"
env_logger = "0.10"
# Cross-platform utilities
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
once_cell = "1.19"
# Platform-specific dependencies
[target.'cfg(unix)'.dependencies]
# Linux dependencies
x11 = "2.21"
xcb = "1.2"
libc = "0.2"
nix = "0.27"
[target.'cfg(windows)'.dependencies]
# Windows dependencies
windows = { version = "0.52", features = [
"Win32_Foundation",
"Win32_System_ProcessStatus",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_Graphics_Dwm",
"Win32_System_SystemInformation",
"Win32_Security",
"Win32_System_Diagnostics_ToolHelp",
] }
[dev-dependencies]
tempfile = "3.8"
assert_cmd = "2.0"
predicates = "3.0"
[build-dependencies]
# Build script dependencies if needed
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true

View File

@ -0,0 +1,46 @@
use clap::{Parser, Subcommand};
use crate::commands::{ImageCommand, ListCommand};
use crate::errors::PeekabooResult;
#[derive(Parser)]
#[command(
name = "peekaboo",
about = "A cross-platform utility for screen capture, application listing, and window management",
version = "1.0.0-beta.21",
long_about = None
)]
pub struct PeekabooCommand {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Capture screen or window images
Image(ImageCommand),
/// List running applications or windows
List(ListCommand),
}
impl PeekabooCommand {
pub fn execute(&self) -> PeekabooResult<()> {
match &self.command {
Some(Commands::Image(cmd)) => cmd.execute(),
Some(Commands::List(cmd)) => cmd.execute(),
None => {
// Default to image command if no subcommand specified
let default_image_cmd = ImageCommand::default();
default_image_cmd.execute()
}
}
}
pub fn is_json_output(&self) -> bool {
match &self.command {
Some(Commands::Image(cmd)) => cmd.json_output,
Some(Commands::List(cmd)) => cmd.is_json_output(),
None => false,
}
}
}

View File

@ -0,0 +1,277 @@
use clap::Parser;
use crate::errors::PeekabooResult;
use crate::models::{CaptureMode, ImageFormat, CaptureFocus, SavedFile, ImageCaptureData};
use crate::platform;
use crate::json_output::{self, Logger};
use crate::utils::file_utils;
#[derive(Parser, Clone)]
pub struct ImageCommand {
/// Target application identifier
#[arg(long)]
pub app: Option<String>,
/// Base output path for saved images
#[arg(long)]
pub path: Option<String>,
/// Capture mode
#[arg(long)]
pub mode: Option<CaptureMode>,
/// Window title to capture
#[arg(long)]
pub window_title: Option<String>,
/// Window index to capture (0=frontmost)
#[arg(long)]
pub window_index: Option<i32>,
/// Screen index to capture (0-based)
#[arg(long)]
pub screen_index: Option<usize>,
/// Image format
#[arg(long, default_value = "png")]
pub format: ImageFormat,
/// Capture focus behavior
#[arg(long, default_value = "auto")]
pub capture_focus: CaptureFocus,
/// Output results in JSON format
#[arg(long)]
pub json_output: bool,
}
impl Default for ImageCommand {
fn default() -> Self {
Self {
app: None,
path: None,
mode: None,
window_title: None,
window_index: None,
screen_index: None,
format: ImageFormat::Png,
capture_focus: CaptureFocus::Auto,
json_output: false,
}
}
}
impl ImageCommand {
pub fn execute(&self) -> PeekabooResult<()> {
Logger::debug(&format!("Executing image command with mode: {:?}", self.determine_mode()));
let mut platform = platform::get_platform()?;
// Check permissions
if !platform.check_screen_recording_permission() {
platform.request_screen_recording_permission()?;
}
let saved_files = self.perform_capture(&mut *platform)?;
if self.json_output {
let data = ImageCaptureData { saved_files };
json_output::output_success(data, None);
} else {
println!("Captured {} image(s):", saved_files.len());
for file in &saved_files {
println!(" {}", file.path);
}
}
Ok(())
}
fn determine_mode(&self) -> CaptureMode {
if let Some(mode) = &self.mode {
mode.clone()
} else if self.app.is_some() {
CaptureMode::Window
} else {
CaptureMode::Screen
}
}
fn perform_capture(&self, platform: &mut dyn crate::traits::Platform) -> PeekabooResult<Vec<SavedFile>> {
let mode = self.determine_mode();
match mode {
CaptureMode::Screen => self.capture_screens(platform),
CaptureMode::Window => {
let app_id = self.app.as_ref()
.ok_or_else(|| crate::errors::PeekabooError::InvalidArgument("No application specified for window capture".to_string()))?;
self.capture_application_window(platform, app_id)
}
CaptureMode::Multi => {
if let Some(app_id) = &self.app {
self.capture_all_application_windows(platform, app_id)
} else {
self.capture_screens(platform)
}
}
}
}
fn capture_screens(&self, platform: &mut dyn crate::traits::Platform) -> PeekabooResult<Vec<SavedFile>> {
let mut saved_files = Vec::new();
if let Some(screen_index) = self.screen_index {
// Capture specific screen
let output_path = self.generate_screen_output_path(screen_index);
platform.capture_display(screen_index, &output_path, self.format.clone())?;
saved_files.push(SavedFile {
path: output_path,
item_label: Some(format!("Display {} (Index {})", screen_index + 1, screen_index)),
window_title: None,
window_id: None,
window_index: None,
mime_type: self.format.mime_type().to_string(),
});
} else {
// Capture all screens
let display_count = platform.get_display_count()?;
for i in 0..display_count {
let output_path = self.generate_screen_output_path(i);
platform.capture_display(i, &output_path, self.format.clone())?;
saved_files.push(SavedFile {
path: output_path,
item_label: Some(format!("Display {}", i + 1)),
window_title: None,
window_id: None,
window_index: None,
mime_type: self.format.mime_type().to_string(),
});
}
}
Ok(saved_files)
}
fn capture_application_window(&self, platform: &mut dyn crate::traits::Platform, app_id: &str) -> PeekabooResult<Vec<SavedFile>> {
let app = platform.find_application(app_id)?;
// Handle focus behavior
if matches!(self.capture_focus, CaptureFocus::Foreground) ||
(matches!(self.capture_focus, CaptureFocus::Auto) && !platform.is_application_active(&app)?) {
if !platform.check_accessibility_permission() {
platform.request_accessibility_permission()?;
}
platform.activate_application(&app)?;
std::thread::sleep(std::time::Duration::from_millis(200));
}
let windows = platform.get_windows_for_app(app.pid)?;
if windows.is_empty() {
return Err(crate::errors::PeekabooError::NoWindowsFound {
app_name: app.app_name
});
}
let target_window = if let Some(window_title) = &self.window_title {
platform.find_window_by_title(app.pid, window_title)?
} else if let Some(window_index) = self.window_index {
platform.get_window_by_index(app.pid, window_index)?
} else {
windows[0].clone() // frontmost window
};
let output_path = self.generate_window_output_path(&app.app_name, &target_window.title);
platform.capture_window(&target_window, &output_path, self.format.clone())?;
let saved_file = SavedFile {
path: output_path,
item_label: Some(app.app_name),
window_title: Some(target_window.title),
window_id: Some(target_window.window_id),
window_index: Some(target_window.window_index),
mime_type: self.format.mime_type().to_string(),
};
Ok(vec![saved_file])
}
fn capture_all_application_windows(&self, platform: &mut dyn crate::traits::Platform, app_id: &str) -> PeekabooResult<Vec<SavedFile>> {
let app = platform.find_application(app_id)?;
// Handle focus behavior
if matches!(self.capture_focus, CaptureFocus::Foreground) ||
(matches!(self.capture_focus, CaptureFocus::Auto) && !platform.is_application_active(&app)?) {
if !platform.check_accessibility_permission() {
platform.request_accessibility_permission()?;
}
platform.activate_application(&app)?;
std::thread::sleep(std::time::Duration::from_millis(200));
}
let windows = platform.get_windows_for_app(app.pid)?;
if windows.is_empty() {
return Err(crate::errors::PeekabooError::NoWindowsFound {
app_name: app.app_name
});
}
let mut saved_files = Vec::new();
for (index, window) in windows.iter().enumerate() {
let output_path = self.generate_window_output_path_with_index(&app.app_name, index, &window.title);
platform.capture_window(window, &output_path, self.format.clone())?;
saved_files.push(SavedFile {
path: output_path,
item_label: Some(app.app_name.clone()),
window_title: Some(window.title.clone()),
window_id: Some(window.window_id),
window_index: Some(index as i32),
mime_type: self.format.mime_type().to_string(),
});
}
Ok(saved_files)
}
fn generate_screen_output_path(&self, display_index: usize) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let filename = format!("screenshot_display_{}_{}.{}", display_index, timestamp, self.format.extension());
if let Some(base_path) = &self.path {
file_utils::join_path(base_path, &filename)
} else {
filename
}
}
fn generate_window_output_path(&self, app_name: &str, window_title: &str) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let safe_app_name = file_utils::sanitize_filename(app_name);
let safe_window_title = file_utils::sanitize_filename(window_title);
let filename = format!("{}_{}_window_{}.{}", safe_app_name, safe_window_title, timestamp, self.format.extension());
if let Some(base_path) = &self.path {
file_utils::join_path(base_path, &filename)
} else {
filename
}
}
fn generate_window_output_path_with_index(&self, app_name: &str, index: usize, window_title: &str) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let safe_app_name = file_utils::sanitize_filename(app_name);
let safe_window_title = file_utils::sanitize_filename(window_title);
let filename = format!("{}_window_{}_{}_{}_{}.{}",
safe_app_name, index, safe_window_title, timestamp,
uuid::Uuid::new_v4().to_string()[..8].to_string(),
self.format.extension());
if let Some(base_path) = &self.path {
file_utils::join_path(base_path, &filename)
} else {
filename
}
}
}

View File

@ -0,0 +1,248 @@
use clap::{Parser, Subcommand};
use crate::errors::PeekabooResult;
use crate::models::{ApplicationListData, WindowListData, ServerStatusData, ServerStatus, ServerPermissions, WindowDetailOption, TargetApplicationInfo};
use crate::platform;
use crate::json_output::{self, Logger};
#[derive(Parser, Clone)]
pub struct ListCommand {
#[command(subcommand)]
pub subcommand: ListSubcommand,
}
#[derive(Subcommand, Clone)]
pub enum ListSubcommand {
/// List all running applications
Apps(AppsCommand),
/// List windows for a specific application
Windows(WindowsCommand),
/// Check server permissions status
ServerStatus(ServerStatusCommand),
}
#[derive(Parser, Clone)]
pub struct AppsCommand {
/// Output results in JSON format
#[arg(long)]
pub json_output: bool,
}
#[derive(Parser, Clone)]
pub struct WindowsCommand {
/// Target application identifier
#[arg(long)]
pub app: String,
/// Include additional window details (comma-separated: off_screen,bounds,ids)
#[arg(long)]
pub include_details: Option<String>,
/// Output results in JSON format
#[arg(long)]
pub json_output: bool,
}
#[derive(Parser, Clone)]
pub struct ServerStatusCommand {
/// Output results in JSON format
#[arg(long)]
pub json_output: bool,
}
impl ListCommand {
pub fn execute(&self) -> PeekabooResult<()> {
match &self.subcommand {
ListSubcommand::Apps(cmd) => cmd.execute(),
ListSubcommand::Windows(cmd) => cmd.execute(),
ListSubcommand::ServerStatus(cmd) => cmd.execute(),
}
}
pub fn is_json_output(&self) -> bool {
match &self.subcommand {
ListSubcommand::Apps(cmd) => cmd.json_output,
ListSubcommand::Windows(cmd) => cmd.json_output,
ListSubcommand::ServerStatus(cmd) => cmd.json_output,
}
}
}
impl AppsCommand {
pub fn execute(&self) -> PeekabooResult<()> {
Logger::debug("Executing apps list command");
let platform = platform::get_platform()?;
// Check permissions
if !platform.check_screen_recording_permission() {
platform.request_screen_recording_permission()?;
}
let applications = platform.get_all_applications()?;
let data = ApplicationListData { applications: applications.clone() };
if self.json_output {
json_output::output_success(data, None);
} else {
self.print_application_list(&applications);
}
Ok(())
}
fn print_application_list(&self, applications: &[crate::models::ApplicationInfo]) {
println!("Running Applications ({}):\n", applications.len());
for (index, app) in applications.iter().enumerate() {
println!("{}. {}", index + 1, app.app_name);
println!(" Bundle ID: {}", app.bundle_id);
println!(" PID: {}", app.pid);
println!(" Status: {}", if app.is_active { "Active" } else { "Background" });
// Only show window count if it's not 1
if app.window_count != 1 {
println!(" Windows: {}", app.window_count);
}
println!();
}
}
}
impl WindowsCommand {
pub fn execute(&self) -> PeekabooResult<()> {
Logger::debug(&format!("Executing windows list command for app: {}", self.app));
let platform = platform::get_platform()?;
// Check permissions
if !platform.check_screen_recording_permission() {
platform.request_screen_recording_permission()?;
}
let app = platform.find_application(&self.app)?;
let detail_options = self.parse_include_details();
let windows = platform.get_windows_for_app(app.pid)?;
let window_infos: Vec<_> = windows.iter()
.map(|w| w.to_window_info(
detail_options.contains(&WindowDetailOption::Bounds),
detail_options.contains(&WindowDetailOption::Ids)
))
.collect();
let target_app_info = TargetApplicationInfo {
app_name: app.app_name.clone(),
bundle_id: Some(app.bundle_id.clone()),
pid: app.pid,
};
let data = WindowListData {
windows: window_infos.clone(),
target_application_info: target_app_info.clone(),
};
if self.json_output {
json_output::output_success(data, None);
} else {
self.print_window_list(&target_app_info, &window_infos);
}
Ok(())
}
fn parse_include_details(&self) -> Vec<WindowDetailOption> {
let mut options = Vec::new();
if let Some(details_string) = &self.include_details {
let components: Vec<&str> = details_string.split(',')
.map(|s| s.trim())
.collect();
for component in components {
match component {
"off_screen" => options.push(WindowDetailOption::OffScreen),
"bounds" => options.push(WindowDetailOption::Bounds),
"ids" => options.push(WindowDetailOption::Ids),
_ => {} // Ignore unknown options
}
}
}
options
}
fn print_window_list(&self, app: &TargetApplicationInfo, windows: &[crate::models::WindowInfo]) {
println!("Windows for {}", app.app_name);
if let Some(bundle_id) = &app.bundle_id {
println!("Bundle ID: {}", bundle_id);
}
println!("PID: {}", app.pid);
println!("Total Windows: {}", windows.len());
println!();
if windows.is_empty() {
println!("No windows found.");
return;
}
for (index, window) in windows.iter().enumerate() {
println!("{}. \"{}\"", index + 1, window.window_title);
if let Some(window_id) = window.window_id {
println!(" Window ID: {}", window_id);
}
if let Some(is_on_screen) = window.is_on_screen {
println!(" On Screen: {}", if is_on_screen { "Yes" } else { "No" });
}
if let Some(bounds) = &window.bounds {
println!(" Bounds: ({}, {}) {}×{}",
bounds.x_coordinate, bounds.y_coordinate,
bounds.width, bounds.height);
}
println!();
}
}
}
impl ServerStatusCommand {
pub fn execute(&self) -> PeekabooResult<()> {
Logger::debug("Executing server status command");
let platform = platform::get_platform()?;
let screen_recording = platform.check_screen_recording_permission();
let accessibility = platform.check_accessibility_permission();
let permissions = ServerPermissions {
screen_recording,
accessibility,
};
let server_status = ServerStatus {
permissions: permissions.clone(),
platform: platform.platform_name().to_string(),
version: platform.platform_version(),
};
let data = ServerStatusData { server_status };
if self.json_output {
json_output::output_success(data, None);
} else {
self.print_server_status(&permissions);
}
Ok(())
}
fn print_server_status(&self, permissions: &ServerPermissions) {
println!("Server Permissions Status:");
println!(" Screen Recording: {}",
if permissions.screen_recording { "✅ Granted" } else { "❌ Not granted" });
println!(" Accessibility: {}",
if permissions.accessibility { "✅ Granted" } else { "❌ Not granted" });
}
}

View File

@ -0,0 +1,6 @@
pub mod image;
pub mod list;
pub use image::ImageCommand;
pub use list::ListCommand;

View File

@ -0,0 +1,96 @@
use thiserror::Error;
pub type PeekabooResult<T> = Result<T, PeekabooError>;
#[derive(Error, Debug)]
pub enum PeekabooError {
#[error("No displays available for capture")]
NoDisplaysAvailable,
#[error("Screen recording permission is required. Please grant it in system settings")]
ScreenRecordingPermissionDenied,
#[error("Accessibility permission is required for some operations. Please grant it in system settings")]
AccessibilityPermissionDenied,
#[error("Invalid display ID provided")]
InvalidDisplayID,
#[error("Failed to create the screen capture: {0}")]
CaptureCreationFailed(String),
#[error("The specified window could not be found")]
WindowNotFound,
#[error("Window with title containing '{search_term}' not found in {app_name}. Available windows: {available_titles}. Note: For URLs, try without the protocol")]
WindowTitleNotFound {
search_term: String,
app_name: String,
available_titles: String,
},
#[error("Failed to capture the specified window: {0}")]
WindowCaptureFailed(String),
#[error("Failed to write capture file to path: {path}. {details}")]
FileWriteError { path: String, details: String },
#[error("Application with identifier '{0}' not found or is not running")]
AppNotFound(String),
#[error("Invalid window index: {0}")]
InvalidWindowIndex(i32),
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("An unexpected error occurred: {0}")]
UnknownError(String),
#[error("The '{app_name}' process is running, but no capturable windows were found")]
NoWindowsFound { app_name: String },
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("JSON serialization error: {0}")]
JsonError(#[from] serde_json::Error),
}
impl PeekabooError {
pub fn exit_code(&self) -> i32 {
match self {
Self::NoDisplaysAvailable => 10,
Self::ScreenRecordingPermissionDenied => 11,
Self::AccessibilityPermissionDenied => 12,
Self::InvalidDisplayID => 13,
Self::CaptureCreationFailed(_) => 14,
Self::WindowNotFound => 15,
Self::WindowTitleNotFound { .. } => 21,
Self::WindowCaptureFailed(_) => 16,
Self::FileWriteError { .. } => 17,
Self::AppNotFound(_) => 18,
Self::InvalidWindowIndex(_) => 19,
Self::InvalidArgument(_) => 20,
Self::NoWindowsFound { .. } => 7,
Self::UnknownError(_) => 1,
Self::IoError(_) => 17,
Self::JsonError(_) => 1,
}
}
pub fn error_code(&self) -> &'static str {
match self {
Self::ScreenRecordingPermissionDenied => "PERMISSION_ERROR_SCREEN_RECORDING",
Self::AccessibilityPermissionDenied => "PERMISSION_ERROR_ACCESSIBILITY",
Self::AppNotFound(_) => "APP_NOT_FOUND",
Self::WindowNotFound | Self::WindowTitleNotFound { .. } => "WINDOW_NOT_FOUND",
Self::CaptureCreationFailed(_) | Self::WindowCaptureFailed(_) => "CAPTURE_FAILED",
Self::FileWriteError { .. } | Self::IoError(_) => "FILE_IO_ERROR",
Self::InvalidArgument(_) | Self::InvalidWindowIndex(_) => "INVALID_ARGUMENT",
Self::JsonError(_) => "INTERNAL_SWIFT_ERROR",
_ => "UNKNOWN_ERROR",
}
}
}

View File

@ -0,0 +1,146 @@
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use crate::errors::PeekabooError;
// Global logger instance
static LOGGER: once_cell::sync::Lazy<Arc<Mutex<Logger>>> =
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(Logger::new())));
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonResponse<T> {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub messages: Option<Vec<String>>,
pub debug_logs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
pub message: String,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}
#[derive(Debug)]
pub struct Logger {
json_mode: bool,
debug_logs: Vec<String>,
}
impl Logger {
pub fn new() -> Self {
Self {
json_mode: false,
debug_logs: Vec::new(),
}
}
pub fn init(json_mode: bool) {
if let Ok(mut logger) = LOGGER.lock() {
logger.json_mode = json_mode;
}
}
pub fn debug(message: &str) {
if let Ok(mut logger) = LOGGER.lock() {
logger.debug_logs.push(message.to_string());
if !logger.json_mode {
log::debug!("{}", message);
}
}
}
pub fn info(message: &str) {
if let Ok(logger) = LOGGER.lock() {
if !logger.json_mode {
log::info!("{}", message);
}
}
}
pub fn warn(message: &str) {
if let Ok(logger) = LOGGER.lock() {
if !logger.json_mode {
log::warn!("{}", message);
}
}
}
pub fn error(message: &str) {
if let Ok(logger) = LOGGER.lock() {
if !logger.json_mode {
log::error!("{}", message);
}
}
}
pub fn get_debug_logs() -> Vec<String> {
if let Ok(logger) = LOGGER.lock() {
logger.debug_logs.clone()
} else {
Vec::new()
}
}
pub fn clear_debug_logs() {
if let Ok(mut logger) = LOGGER.lock() {
logger.debug_logs.clear();
}
}
}
pub fn output_success<T: Serialize>(data: T, messages: Option<Vec<String>>) {
let response = JsonResponse {
success: true,
data: Some(data),
messages,
debug_logs: Logger::get_debug_logs(),
error: None,
};
output_json(&response);
}
pub fn output_error(error: &PeekabooError) {
let error_info = ErrorInfo {
message: error.to_string(),
code: error.error_code().to_string(),
details: None,
};
let response: JsonResponse<()> = JsonResponse {
success: false,
data: None,
messages: None,
debug_logs: Logger::get_debug_logs(),
error: Some(error_info),
};
output_json(&response);
}
fn output_json<T: Serialize>(response: &JsonResponse<T>) {
match serde_json::to_string_pretty(response) {
Ok(json) => println!("{}", json),
Err(e) => {
eprintln!("Failed to serialize JSON response: {}", e);
// Fallback to simple error JSON
println!(r#"{{
"success": false,
"error": {{
"message": "Failed to encode JSON response",
"code": "INTERNAL_SWIFT_ERROR"
}},
"debug_logs": []
}}"#);
}
}
}
// Add once_cell dependency
use once_cell;

View File

@ -0,0 +1,13 @@
pub mod cli;
pub mod commands;
pub mod errors;
pub mod json_output;
pub mod models;
pub mod platform;
pub mod traits;
pub mod utils;
pub use errors::*;
pub use models::*;
pub use traits::*;

View File

@ -0,0 +1,41 @@
use clap::Parser;
use std::process;
mod cli;
mod commands;
mod errors;
mod json_output;
mod models;
mod platform;
mod traits;
mod utils;
use cli::PeekabooCommand;
use errors::PeekabooError;
use json_output::Logger;
fn main() {
// Initialize logger
env_logger::init();
// Parse command line arguments
let cmd = PeekabooCommand::parse();
// Initialize logger with JSON mode if needed
Logger::init(cmd.is_json_output());
// Execute command and handle errors
if let Err(error) = cmd.execute() {
let exit_code = error.exit_code();
handle_error(error, cmd.is_json_output());
process::exit(exit_code);
}
}
fn handle_error(error: PeekabooError, json_output: bool) {
if json_output {
json_output::output_error(&error);
} else {
eprintln!("Error: {}", error);
}
}

View File

@ -0,0 +1,159 @@
use serde::{Deserialize, Serialize};
use clap::ValueEnum;
// MARK: - Image Capture Models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedFile {
pub path: String,
pub item_label: Option<String>,
pub window_title: Option<String>,
pub window_id: Option<u32>,
pub window_index: Option<i32>,
pub mime_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageCaptureData {
pub saved_files: Vec<SavedFile>,
}
#[derive(Debug, Clone, ValueEnum, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CaptureMode {
Screen,
Window,
Multi,
}
#[derive(Debug, Clone, ValueEnum, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ImageFormat {
Png,
Jpg,
}
impl ImageFormat {
pub fn mime_type(&self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpg => "image/jpeg",
}
}
pub fn extension(&self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpg => "jpg",
}
}
}
#[derive(Debug, Clone, ValueEnum, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CaptureFocus {
Background,
Auto,
Foreground,
}
// MARK: - Application & Window Models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationInfo {
pub app_name: String,
pub bundle_id: String,
pub pid: i32,
pub is_active: bool,
pub window_count: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationListData {
pub applications: Vec<ApplicationInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowInfo {
pub window_title: String,
pub window_id: Option<u32>,
pub window_index: Option<i32>,
pub bounds: Option<WindowBounds>,
pub is_on_screen: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowBounds {
#[serde(rename = "xCoordinate")]
pub x_coordinate: i32,
#[serde(rename = "yCoordinate")]
pub y_coordinate: i32,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetApplicationInfo {
pub app_name: String,
pub bundle_id: Option<String>,
pub pid: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowListData {
pub windows: Vec<WindowInfo>,
pub target_application_info: TargetApplicationInfo,
}
// MARK: - Server Status Models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerPermissions {
pub screen_recording: bool,
pub accessibility: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerStatusData {
pub server_status: ServerStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerStatus {
pub permissions: ServerPermissions,
pub platform: String,
pub version: String,
}
// MARK: - Window Details Options
#[derive(Debug, Clone, ValueEnum, PartialEq)]
#[clap(rename_all = "snake_case")]
pub enum WindowDetailOption {
OffScreen,
Bounds,
Ids,
}
// MARK: - Internal Window Data
#[derive(Debug, Clone)]
pub struct WindowData {
pub window_id: u32,
pub title: String,
pub bounds: WindowBounds,
pub is_on_screen: bool,
pub window_index: i32,
}
impl WindowData {
pub fn to_window_info(&self, include_bounds: bool, include_ids: bool) -> WindowInfo {
WindowInfo {
window_title: self.title.clone(),
window_id: if include_ids { Some(self.window_id) } else { None },
window_index: Some(self.window_index),
bounds: if include_bounds { Some(self.bounds.clone()) } else { None },
is_on_screen: Some(self.is_on_screen),
}
}
}

View File

@ -0,0 +1,446 @@
use crate::traits::{Platform, ScreenCapture, WindowManager, ApplicationManager, PermissionManager};
use crate::errors::{PeekabooError, PeekabooResult};
use crate::models::{ApplicationInfo, WindowData, ImageFormat, WindowBounds};
use crate::utils::file_utils;
use std::process::Command;
use std::fs;
use std::path::Path;
pub struct LinuxPlatform {
display_server: DisplayServer,
}
#[derive(Debug, Clone)]
enum DisplayServer {
X11,
Wayland,
}
impl LinuxPlatform {
pub fn new() -> PeekabooResult<Self> {
let display_server = detect_display_server()?;
Ok(Self { display_server })
}
fn get_screenshot_command(&self, output_path: &str) -> Vec<String> {
match self.display_server {
DisplayServer::X11 => {
// Try scrot first, then ImageMagick import
if command_exists("scrot") {
vec!["scrot".to_string(), output_path.to_string()]
} else if command_exists("import") {
vec!["import".to_string(), "-window".to_string(), "root".to_string(), output_path.to_string()]
} else {
vec!["xwd".to_string(), "-root".to_string(), "-out".to_string(), output_path.to_string()]
}
}
DisplayServer::Wayland => {
// Try grim first, then wl-copy with ImageMagick
if command_exists("grim") {
vec!["grim".to_string(), output_path.to_string()]
} else {
vec!["gnome-screenshot".to_string(), "-f".to_string(), output_path.to_string()]
}
}
}
}
fn get_window_screenshot_command(&self, window_id: u32, output_path: &str) -> Vec<String> {
match self.display_server {
DisplayServer::X11 => {
if command_exists("scrot") {
vec!["scrot".to_string(), "-s".to_string(), output_path.to_string()]
} else if command_exists("import") {
vec!["import".to_string(), "-window".to_string(), window_id.to_string(), output_path.to_string()]
} else {
vec!["xwd".to_string(), "-id".to_string(), window_id.to_string(), "-out".to_string(), output_path.to_string()]
}
}
DisplayServer::Wayland => {
// Wayland window capture is more complex, use grim with slurp
if command_exists("grim") && command_exists("slurp") {
vec!["sh".to_string(), "-c".to_string(),
format!("grim -g \"$(slurp)\" {}", output_path)]
} else {
vec!["gnome-screenshot".to_string(), "-w".to_string(), "-f".to_string(), output_path.to_string()]
}
}
}
}
}
impl ScreenCapture for LinuxPlatform {
fn capture_display(&self, display_index: usize, output_path: &str, format: ImageFormat) -> PeekabooResult<()> {
// For now, capture the main display (display_index is ignored on Linux)
let cmd_args = self.get_screenshot_command(output_path);
let output = Command::new(&cmd_args[0])
.args(&cmd_args[1..])
.output()
.map_err(|e| PeekabooError::CaptureCreationFailed(e.to_string()))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(PeekabooError::CaptureCreationFailed(error_msg.to_string()));
}
// Convert format if needed
if let Some(converted_path) = convert_image_format(output_path, format)? {
if converted_path != output_path {
fs::rename(converted_path, output_path)?;
}
}
Ok(())
}
fn capture_all_displays(&self, base_path: Option<&str>, format: ImageFormat) -> PeekabooResult<Vec<String>> {
// For Linux, we'll capture the main display
let output_path = generate_output_path(base_path, 0, &format);
self.capture_display(0, &output_path, format)?;
Ok(vec![output_path])
}
fn capture_window(&self, window: &WindowData, output_path: &str, format: ImageFormat) -> PeekabooResult<()> {
let cmd_args = self.get_window_screenshot_command(window.window_id, output_path);
let output = Command::new(&cmd_args[0])
.args(&cmd_args[1..])
.output()
.map_err(|e| PeekabooError::WindowCaptureFailed(e.to_string()))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(PeekabooError::WindowCaptureFailed(error_msg.to_string()));
}
// Convert format if needed
if let Some(converted_path) = convert_image_format(output_path, format)? {
if converted_path != output_path {
fs::rename(converted_path, output_path)?;
}
}
Ok(())
}
fn get_display_count(&self) -> PeekabooResult<usize> {
// For simplicity, return 1 display on Linux
// In a full implementation, we'd query X11/Wayland for actual display count
Ok(1)
}
}
impl WindowManager for LinuxPlatform {
fn get_windows_for_app(&self, pid: i32) -> PeekabooResult<Vec<WindowData>> {
match self.display_server {
DisplayServer::X11 => get_x11_windows_for_pid(pid),
DisplayServer::Wayland => get_wayland_windows_for_pid(pid),
}
}
fn find_window_by_title(&self, pid: i32, title_substring: &str) -> PeekabooResult<WindowData> {
let windows = self.get_windows_for_app(pid)?;
windows.into_iter()
.find(|w| w.title.contains(title_substring))
.ok_or_else(|| PeekabooError::WindowNotFound)
}
fn get_window_by_index(&self, pid: i32, index: i32) -> PeekabooResult<WindowData> {
let windows = self.get_windows_for_app(pid)?;
windows.into_iter()
.nth(index as usize)
.ok_or_else(|| PeekabooError::InvalidWindowIndex(index))
}
fn activate_window(&self, window: &WindowData) -> PeekabooResult<()> {
match self.display_server {
DisplayServer::X11 => {
let output = Command::new("xdotool")
.args(&["windowactivate", &window.window_id.to_string()])
.output()
.map_err(|e| PeekabooError::UnknownError(e.to_string()))?;
if !output.status.success() {
return Err(PeekabooError::UnknownError("Failed to activate window".to_string()));
}
}
DisplayServer::Wayland => {
// Wayland doesn't allow arbitrary window activation for security reasons
// This would require compositor-specific protocols
return Err(PeekabooError::UnknownError("Window activation not supported on Wayland".to_string()));
}
}
Ok(())
}
}
impl ApplicationManager for LinuxPlatform {
fn get_all_applications(&self) -> PeekabooResult<Vec<ApplicationInfo>> {
get_running_applications()
}
fn find_application(&self, identifier: &str) -> PeekabooResult<ApplicationInfo> {
let apps = self.get_all_applications()?;
// Try to find by PID first
if let Ok(pid) = identifier.parse::<i32>() {
if let Some(app) = apps.iter().find(|a| a.pid == pid) {
return Ok(app.clone());
}
}
// Try to find by name or bundle ID
apps.into_iter()
.find(|app| {
app.app_name.to_lowercase().contains(&identifier.to_lowercase()) ||
app.bundle_id.to_lowercase().contains(&identifier.to_lowercase())
})
.ok_or_else(|| PeekabooError::AppNotFound(identifier.to_string()))
}
fn activate_application(&self, app: &ApplicationInfo) -> PeekabooResult<()> {
// Try to activate the first window of the application
let windows = self.get_windows_for_app(app.pid)?;
if let Some(window) = windows.first() {
self.activate_window(window)?;
}
Ok(())
}
fn is_application_active(&self, app: &ApplicationInfo) -> PeekabooResult<bool> {
// For Linux, we'll consider an app active if it has focused windows
// This is a simplified implementation
Ok(app.is_active)
}
}
impl PermissionManager for LinuxPlatform {
fn check_screen_recording_permission(&self) -> bool {
// On Linux, screen recording permissions are generally not restricted
// Check if we have the necessary tools available
match self.display_server {
DisplayServer::X11 => command_exists("scrot") || command_exists("import") || command_exists("xwd"),
DisplayServer::Wayland => command_exists("grim") || command_exists("gnome-screenshot"),
}
}
fn check_accessibility_permission(&self) -> bool {
// On Linux, accessibility permissions are generally not restricted
// Check if we have the necessary tools available
command_exists("xdotool") || command_exists("wmctrl")
}
fn request_screen_recording_permission(&self) -> PeekabooResult<bool> {
// On Linux, no explicit permission request is needed
Ok(self.check_screen_recording_permission())
}
fn request_accessibility_permission(&self) -> PeekabooResult<bool> {
// On Linux, no explicit permission request is needed
Ok(self.check_accessibility_permission())
}
}
impl Platform for LinuxPlatform {
fn platform_name(&self) -> &'static str {
"linux"
}
fn platform_version(&self) -> String {
// Get Linux distribution info
if let Ok(content) = fs::read_to_string("/etc/os-release") {
for line in content.lines() {
if line.starts_with("PRETTY_NAME=") {
return line.split('=').nth(1)
.unwrap_or("Unknown Linux")
.trim_matches('"')
.to_string();
}
}
}
"Unknown Linux".to_string()
}
fn initialize(&mut self) -> PeekabooResult<()> {
// Check if required tools are available
if !self.check_screen_recording_permission() {
return Err(PeekabooError::ScreenRecordingPermissionDenied);
}
Ok(())
}
fn cleanup(&mut self) -> PeekabooResult<()> {
// No cleanup needed for Linux platform
Ok(())
}
}
// Helper functions
fn detect_display_server() -> PeekabooResult<DisplayServer> {
if std::env::var("WAYLAND_DISPLAY").is_ok() {
Ok(DisplayServer::Wayland)
} else if std::env::var("DISPLAY").is_ok() {
Ok(DisplayServer::X11)
} else {
Err(PeekabooError::NoDisplaysAvailable)
}
}
fn command_exists(command: &str) -> bool {
Command::new("which")
.arg(command)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn convert_image_format(path: &str, target_format: ImageFormat) -> PeekabooResult<Option<String>> {
let path_obj = Path::new(path);
let current_ext = path_obj.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
let target_ext = target_format.extension();
if current_ext == target_ext {
return Ok(None);
}
// Use ImageMagick convert if available
if command_exists("convert") {
let new_path = path_obj.with_extension(target_ext);
let new_path_str = new_path.to_string_lossy().to_string();
let output = Command::new("convert")
.args(&[path, &new_path_str])
.output()
.map_err(|e| PeekabooError::FileWriteError {
path: new_path_str.clone(),
details: e.to_string(),
})?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(PeekabooError::FileWriteError {
path: new_path_str,
details: error_msg.to_string(),
});
}
// Remove original file
fs::remove_file(path)?;
Ok(Some(new_path_str))
} else {
Ok(None)
}
}
fn generate_output_path(base_path: Option<&str>, display_index: usize, format: &ImageFormat) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let filename = format!("screenshot_display_{}_{}.{}", display_index, timestamp, format.extension());
if let Some(base) = base_path {
file_utils::join_path(base, &filename)
} else {
filename
}
}
fn get_x11_windows_for_pid(pid: i32) -> PeekabooResult<Vec<WindowData>> {
// Use wmctrl to get window list
let output = Command::new("wmctrl")
.args(&["-l", "-p"])
.output()
.map_err(|e| PeekabooError::UnknownError(e.to_string()))?;
if !output.status.success() {
return Err(PeekabooError::UnknownError("Failed to get window list".to_string()));
}
let output_str = String::from_utf8_lossy(&output.stdout);
let mut windows = Vec::new();
let mut window_index = 0;
for line in output_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
if let (Ok(window_id), Ok(window_pid)) = (
u32::from_str_radix(parts[0].trim_start_matches("0x"), 16),
parts[2].parse::<i32>()
) {
if window_pid == pid {
let title = parts[4..].join(" ");
windows.push(WindowData {
window_id,
title,
bounds: WindowBounds {
x_coordinate: 0,
y_coordinate: 0,
width: 800,
height: 600,
},
is_on_screen: true,
window_index,
});
window_index += 1;
}
}
}
}
Ok(windows)
}
fn get_wayland_windows_for_pid(_pid: i32) -> PeekabooResult<Vec<WindowData>> {
// Wayland doesn't provide a standard way to enumerate windows
// This would require compositor-specific protocols
Err(PeekabooError::UnknownError("Window enumeration not supported on Wayland".to_string()))
}
fn get_running_applications() -> PeekabooResult<Vec<ApplicationInfo>> {
let mut applications = Vec::new();
// Read from /proc to get running processes
let proc_dir = fs::read_dir("/proc")
.map_err(|e| PeekabooError::UnknownError(e.to_string()))?;
for entry in proc_dir {
if let Ok(entry) = entry {
if let Ok(pid) = entry.file_name().to_string_lossy().parse::<i32>() {
if let Ok(app_info) = get_application_info(pid) {
applications.push(app_info);
}
}
}
}
Ok(applications)
}
fn get_application_info(pid: i32) -> PeekabooResult<ApplicationInfo> {
let comm_path = format!("/proc/{}/comm", pid);
let cmdline_path = format!("/proc/{}/cmdline", pid);
let app_name = fs::read_to_string(&comm_path)
.map_err(|e| PeekabooError::UnknownError(e.to_string()))?
.trim()
.to_string();
let cmdline = fs::read_to_string(&cmdline_path)
.unwrap_or_default()
.replace('\0', " ");
// Use command line as bundle_id for Linux
let bundle_id = if cmdline.is_empty() { app_name.clone() } else { cmdline };
Ok(ApplicationInfo {
app_name,
bundle_id,
pid,
is_active: false, // Simplified - would need more complex logic to determine
window_count: 1, // Simplified - would need to count actual windows
})
}

View File

@ -0,0 +1,103 @@
use crate::traits::{Platform, ScreenCapture, WindowManager, ApplicationManager, PermissionManager};
use crate::errors::{PeekabooError, PeekabooResult};
use crate::models::{ApplicationInfo, WindowData, ImageFormat};
pub struct MacOSPlatform;
impl MacOSPlatform {
pub fn new() -> PeekabooResult<Self> {
Ok(Self)
}
}
// Stub implementations for macOS - the Swift binary should be used instead
impl ScreenCapture for MacOSPlatform {
fn capture_display(&self, _display_index: usize, _output_path: &str, _format: ImageFormat) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn capture_all_displays(&self, _base_path: Option<&str>, _format: ImageFormat) -> PeekabooResult<Vec<String>> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn capture_window(&self, _window: &WindowData, _output_path: &str, _format: ImageFormat) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn get_display_count(&self) -> PeekabooResult<usize> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
}
impl WindowManager for MacOSPlatform {
fn get_windows_for_app(&self, _pid: i32) -> PeekabooResult<Vec<WindowData>> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn find_window_by_title(&self, _pid: i32, _title_substring: &str) -> PeekabooResult<WindowData> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn get_window_by_index(&self, _pid: i32, _index: i32) -> PeekabooResult<WindowData> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn activate_window(&self, _window: &WindowData) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
}
impl ApplicationManager for MacOSPlatform {
fn get_all_applications(&self) -> PeekabooResult<Vec<ApplicationInfo>> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn find_application(&self, _identifier: &str) -> PeekabooResult<ApplicationInfo> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn activate_application(&self, _app: &ApplicationInfo) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn is_application_active(&self, _app: &ApplicationInfo) -> PeekabooResult<bool> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
}
impl PermissionManager for MacOSPlatform {
fn check_screen_recording_permission(&self) -> bool {
false
}
fn check_accessibility_permission(&self) -> bool {
false
}
fn request_screen_recording_permission(&self) -> PeekabooResult<bool> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn request_accessibility_permission(&self) -> PeekabooResult<bool> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
}
impl Platform for MacOSPlatform {
fn platform_name(&self) -> &'static str {
"darwin"
}
fn platform_version(&self) -> String {
"Use Swift binary".to_string()
}
fn initialize(&mut self) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Use the Swift binary for macOS".to_string()))
}
fn cleanup(&mut self) -> PeekabooResult<()> {
Ok(())
}
}

View File

@ -0,0 +1,43 @@
use crate::traits::Platform;
use crate::errors::PeekabooResult;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "macos")]
pub mod macos;
/// Get the appropriate platform implementation for the current OS
pub fn get_platform() -> PeekabooResult<Box<dyn Platform>> {
#[cfg(target_os = "linux")]
{
let mut platform = Box::new(linux::LinuxPlatform::new()?);
platform.initialize()?;
Ok(platform)
}
#[cfg(target_os = "windows")]
{
let mut platform = Box::new(windows::WindowsPlatform::new()?);
platform.initialize()?;
Ok(platform)
}
#[cfg(target_os = "macos")]
{
let mut platform = Box::new(macos::MacOSPlatform::new()?);
platform.initialize()?;
Ok(platform)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
Err(crate::errors::PeekabooError::UnknownError(
"Unsupported platform".to_string()
))
}
}

View File

@ -0,0 +1,513 @@
use crate::traits::{Platform, ScreenCapture, WindowManager, ApplicationManager, PermissionManager};
use crate::errors::{PeekabooError, PeekabooResult};
use crate::models::{ApplicationInfo, WindowData, ImageFormat, WindowBounds};
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
#[cfg(target_os = "windows")]
use windows::{
core::*,
Win32::Foundation::*,
Win32::Graphics::Gdi::*,
Win32::System::ProcessStatus::*,
Win32::System::Threading::*,
Win32::UI::WindowsAndMessaging::*,
Win32::System::Diagnostics::ToolHelp::*,
};
pub struct WindowsPlatform {
initialized: bool,
}
impl WindowsPlatform {
pub fn new() -> PeekabooResult<Self> {
Ok(Self {
initialized: false,
})
}
}
#[cfg(target_os = "windows")]
impl ScreenCapture for WindowsPlatform {
fn capture_display(&self, display_index: usize, output_path: &str, format: ImageFormat) -> PeekabooResult<()> {
unsafe {
// Get desktop window
let desktop_hwnd = GetDesktopWindow();
let desktop_dc = GetDC(desktop_hwnd);
if desktop_dc.is_invalid() {
return Err(PeekabooError::CaptureCreationFailed("Failed to get desktop DC".to_string()));
}
// Get screen dimensions
let screen_width = GetSystemMetrics(SM_CXSCREEN);
let screen_height = GetSystemMetrics(SM_CYSCREEN);
// Create compatible DC and bitmap
let mem_dc = CreateCompatibleDC(desktop_dc);
let bitmap = CreateCompatibleBitmap(desktop_dc, screen_width, screen_height);
if mem_dc.is_invalid() || bitmap.is_invalid() {
ReleaseDC(desktop_hwnd, desktop_dc);
return Err(PeekabooError::CaptureCreationFailed("Failed to create compatible DC/bitmap".to_string()));
}
// Select bitmap into memory DC
let old_bitmap = SelectObject(mem_dc, bitmap);
// Copy screen to memory DC
let result = BitBlt(
mem_dc,
0, 0,
screen_width, screen_height,
desktop_dc,
0, 0,
SRCCOPY,
);
if !result.as_bool() {
SelectObject(mem_dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(mem_dc);
ReleaseDC(desktop_hwnd, desktop_dc);
return Err(PeekabooError::CaptureCreationFailed("Failed to copy screen".to_string()));
}
// Save bitmap to file
let save_result = save_bitmap_to_file(bitmap, output_path, format);
// Cleanup
SelectObject(mem_dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(mem_dc);
ReleaseDC(desktop_hwnd, desktop_dc);
save_result
}
}
fn capture_all_displays(&self, base_path: Option<&str>, format: ImageFormat) -> PeekabooResult<Vec<String>> {
// For Windows, we'll capture the primary display
let output_path = generate_output_path(base_path, 0, &format);
self.capture_display(0, &output_path, format)?;
Ok(vec![output_path])
}
fn capture_window(&self, window: &WindowData, output_path: &str, format: ImageFormat) -> PeekabooResult<()> {
unsafe {
let hwnd = HWND(window.window_id as isize);
// Get window DC
let window_dc = GetDC(hwnd);
if window_dc.is_invalid() {
return Err(PeekabooError::WindowCaptureFailed("Failed to get window DC".to_string()));
}
// Get window dimensions
let mut rect = RECT::default();
if !GetClientRect(hwnd, &mut rect).as_bool() {
ReleaseDC(hwnd, window_dc);
return Err(PeekabooError::WindowCaptureFailed("Failed to get window rect".to_string()));
}
let width = rect.right - rect.left;
let height = rect.bottom - rect.top;
// Create compatible DC and bitmap
let mem_dc = CreateCompatibleDC(window_dc);
let bitmap = CreateCompatibleBitmap(window_dc, width, height);
if mem_dc.is_invalid() || bitmap.is_invalid() {
ReleaseDC(hwnd, window_dc);
return Err(PeekabooError::WindowCaptureFailed("Failed to create compatible DC/bitmap".to_string()));
}
// Select bitmap into memory DC
let old_bitmap = SelectObject(mem_dc, bitmap);
// Copy window to memory DC
let result = BitBlt(
mem_dc,
0, 0,
width, height,
window_dc,
0, 0,
SRCCOPY,
);
if !result.as_bool() {
SelectObject(mem_dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(mem_dc);
ReleaseDC(hwnd, window_dc);
return Err(PeekabooError::WindowCaptureFailed("Failed to copy window".to_string()));
}
// Save bitmap to file
let save_result = save_bitmap_to_file(bitmap, output_path, format);
// Cleanup
SelectObject(mem_dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(mem_dc);
ReleaseDC(hwnd, window_dc);
save_result
}
}
fn get_display_count(&self) -> PeekabooResult<usize> {
unsafe {
let count = GetSystemMetrics(SM_CMONITORS) as usize;
Ok(count.max(1))
}
}
}
#[cfg(target_os = "windows")]
impl WindowManager for WindowsPlatform {
fn get_windows_for_app(&self, pid: i32) -> PeekabooResult<Vec<WindowData>> {
let mut windows = Vec::new();
let mut context = EnumWindowsContext {
target_pid: pid as u32,
windows: &mut windows,
window_index: 0,
};
unsafe {
EnumWindows(
Some(enum_windows_proc),
LPARAM(&mut context as *mut _ as isize),
);
}
Ok(windows)
}
fn find_window_by_title(&self, pid: i32, title_substring: &str) -> PeekabooResult<WindowData> {
let windows = self.get_windows_for_app(pid)?;
windows.into_iter()
.find(|w| w.title.contains(title_substring))
.ok_or_else(|| PeekabooError::WindowNotFound)
}
fn get_window_by_index(&self, pid: i32, index: i32) -> PeekabooResult<WindowData> {
let windows = self.get_windows_for_app(pid)?;
windows.into_iter()
.nth(index as usize)
.ok_or_else(|| PeekabooError::InvalidWindowIndex(index))
}
fn activate_window(&self, window: &WindowData) -> PeekabooResult<()> {
unsafe {
let hwnd = HWND(window.window_id as isize);
// Bring window to foreground
if !SetForegroundWindow(hwnd).as_bool() {
return Err(PeekabooError::UnknownError("Failed to activate window".to_string()));
}
// Show window if minimized
ShowWindow(hwnd, SW_RESTORE);
}
Ok(())
}
}
#[cfg(target_os = "windows")]
impl ApplicationManager for WindowsPlatform {
fn get_all_applications(&self) -> PeekabooResult<Vec<ApplicationInfo>> {
let mut applications = Vec::new();
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
.map_err(|e| PeekabooError::UnknownError(e.to_string()))?;
let mut process_entry = PROCESSENTRY32W {
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
..Default::default()
};
if Process32FirstW(snapshot, &mut process_entry).as_bool() {
loop {
let pid = process_entry.th32ProcessID as i32;
if let Ok(app_info) = get_application_info(pid, &process_entry) {
applications.push(app_info);
}
if !Process32NextW(snapshot, &mut process_entry).as_bool() {
break;
}
}
}
CloseHandle(snapshot);
}
Ok(applications)
}
fn find_application(&self, identifier: &str) -> PeekabooResult<ApplicationInfo> {
let apps = self.get_all_applications()?;
// Try to find by PID first
if let Ok(pid) = identifier.parse::<i32>() {
if let Some(app) = apps.iter().find(|a| a.pid == pid) {
return Ok(app.clone());
}
}
// Try to find by name or bundle ID
apps.into_iter()
.find(|app| {
app.app_name.to_lowercase().contains(&identifier.to_lowercase()) ||
app.bundle_id.to_lowercase().contains(&identifier.to_lowercase())
})
.ok_or_else(|| PeekabooError::AppNotFound(identifier.to_string()))
}
fn activate_application(&self, app: &ApplicationInfo) -> PeekabooResult<()> {
// Try to activate the first window of the application
let windows = self.get_windows_for_app(app.pid)?;
if let Some(window) = windows.first() {
self.activate_window(window)?;
}
Ok(())
}
fn is_application_active(&self, app: &ApplicationInfo) -> PeekabooResult<bool> {
Ok(app.is_active)
}
}
#[cfg(target_os = "windows")]
impl PermissionManager for WindowsPlatform {
fn check_screen_recording_permission(&self) -> bool {
// On Windows, screen recording is generally allowed
// Check if we can access the desktop
unsafe {
let desktop_hwnd = GetDesktopWindow();
let desktop_dc = GetDC(desktop_hwnd);
let has_access = !desktop_dc.is_invalid();
if has_access {
ReleaseDC(desktop_hwnd, desktop_dc);
}
has_access
}
}
fn check_accessibility_permission(&self) -> bool {
// On Windows, basic window enumeration is generally allowed
true
}
fn request_screen_recording_permission(&self) -> PeekabooResult<bool> {
Ok(self.check_screen_recording_permission())
}
fn request_accessibility_permission(&self) -> PeekabooResult<bool> {
Ok(self.check_accessibility_permission())
}
}
#[cfg(target_os = "windows")]
impl Platform for WindowsPlatform {
fn platform_name(&self) -> &'static str {
"windows"
}
fn platform_version(&self) -> String {
// Get Windows version
unsafe {
let major = GetSystemMetrics(SM_CXSCREEN); // Placeholder - would use proper version API
format!("Windows {}", major)
}
}
fn initialize(&mut self) -> PeekabooResult<()> {
if !self.check_screen_recording_permission() {
return Err(PeekabooError::ScreenRecordingPermissionDenied);
}
self.initialized = true;
Ok(())
}
fn cleanup(&mut self) -> PeekabooResult<()> {
self.initialized = false;
Ok(())
}
}
// Non-Windows stub implementations
#[cfg(not(target_os = "windows"))]
impl ScreenCapture for WindowsPlatform {
fn capture_display(&self, _display_index: usize, _output_path: &str, _format: ImageFormat) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn capture_all_displays(&self, _base_path: Option<&str>, _format: ImageFormat) -> PeekabooResult<Vec<String>> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn capture_window(&self, _window: &WindowData, _output_path: &str, _format: ImageFormat) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn get_display_count(&self) -> PeekabooResult<usize> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
}
#[cfg(not(target_os = "windows"))]
impl WindowManager for WindowsPlatform {
fn get_windows_for_app(&self, _pid: i32) -> PeekabooResult<Vec<WindowData>> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn find_window_by_title(&self, _pid: i32, _title_substring: &str) -> PeekabooResult<WindowData> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn get_window_by_index(&self, _pid: i32, _index: i32) -> PeekabooResult<WindowData> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn activate_window(&self, _window: &WindowData) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
}
#[cfg(not(target_os = "windows"))]
impl ApplicationManager for WindowsPlatform {
fn get_all_applications(&self) -> PeekabooResult<Vec<ApplicationInfo>> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn find_application(&self, _identifier: &str) -> PeekabooResult<ApplicationInfo> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn activate_application(&self, _app: &ApplicationInfo) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn is_application_active(&self, _app: &ApplicationInfo) -> PeekabooResult<bool> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
}
#[cfg(not(target_os = "windows"))]
impl PermissionManager for WindowsPlatform {
fn check_screen_recording_permission(&self) -> bool { false }
fn check_accessibility_permission(&self) -> bool { false }
fn request_screen_recording_permission(&self) -> PeekabooResult<bool> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn request_accessibility_permission(&self) -> PeekabooResult<bool> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
}
#[cfg(not(target_os = "windows"))]
impl Platform for WindowsPlatform {
fn platform_name(&self) -> &'static str { "windows" }
fn platform_version(&self) -> String { "Not available".to_string() }
fn initialize(&mut self) -> PeekabooResult<()> {
Err(PeekabooError::UnknownError("Windows platform not available".to_string()))
}
fn cleanup(&mut self) -> PeekabooResult<()> { Ok(()) }
}
// Helper functions and structures
#[cfg(target_os = "windows")]
struct EnumWindowsContext<'a> {
target_pid: u32,
windows: &'a mut Vec<WindowData>,
window_index: i32,
}
#[cfg(target_os = "windows")]
unsafe extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
let context = &mut *(lparam.0 as *mut EnumWindowsContext);
// Get window process ID
let mut window_pid: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut window_pid));
if window_pid == context.target_pid {
// Get window title
let mut title_buffer = [0u16; 256];
let title_len = GetWindowTextW(hwnd, &mut title_buffer);
let title = OsString::from_wide(&title_buffer[..title_len as usize])
.to_string_lossy()
.to_string();
// Get window bounds
let mut rect = RECT::default();
GetWindowRect(hwnd, &mut rect);
let window_data = WindowData {
window_id: hwnd.0 as u32,
title,
bounds: WindowBounds {
x_coordinate: rect.left,
y_coordinate: rect.top,
width: rect.right - rect.left,
height: rect.bottom - rect.top,
},
is_on_screen: IsWindowVisible(hwnd).as_bool(),
window_index: context.window_index,
};
context.windows.push(window_data);
context.window_index += 1;
}
TRUE
}
#[cfg(target_os = "windows")]
fn get_application_info(pid: i32, process_entry: &PROCESSENTRY32W) -> PeekabooResult<ApplicationInfo> {
let app_name = OsString::from_wide(&process_entry.szExeFile)
.to_string_lossy()
.trim_end_matches('\0')
.to_string();
// Use executable name as bundle_id for Windows
let bundle_id = app_name.clone();
Ok(ApplicationInfo {
app_name,
bundle_id,
pid,
is_active: false, // Simplified - would need more complex logic
window_count: 1, // Simplified - would need to count actual windows
})
}
#[cfg(target_os = "windows")]
fn save_bitmap_to_file(bitmap: HBITMAP, output_path: &str, format: ImageFormat) -> PeekabooResult<()> {
// This is a simplified implementation
// In a full implementation, we'd use proper image encoding libraries
// For now, we'll return success and let the caller handle the actual file writing
// TODO: Implement proper bitmap to file conversion
// This would involve:
// 1. Getting bitmap data
// 2. Converting to PNG/JPEG format
// 3. Writing to file
Err(PeekabooError::UnknownError("Bitmap saving not yet implemented".to_string()))
}
fn generate_output_path(base_path: Option<&str>, display_index: usize, format: &ImageFormat) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let filename = format!("screenshot_display_{}_{}.{}", display_index, timestamp, format.extension());
if let Some(base) = base_path {
format!("{}\\{}", base, filename)
} else {
filename
}
}

View File

@ -0,0 +1,77 @@
use crate::errors::PeekabooResult;
use crate::models::{ApplicationInfo, WindowData, ImageFormat};
/// Trait for screen capture operations
pub trait ScreenCapture {
/// Capture a specific display by index
fn capture_display(&self, display_index: usize, output_path: &str, format: ImageFormat) -> PeekabooResult<()>;
/// Capture all displays
fn capture_all_displays(&self, base_path: Option<&str>, format: ImageFormat) -> PeekabooResult<Vec<String>>;
/// Capture a specific window
fn capture_window(&self, window: &WindowData, output_path: &str, format: ImageFormat) -> PeekabooResult<()>;
/// Get the number of available displays
fn get_display_count(&self) -> PeekabooResult<usize>;
}
/// Trait for window management operations
pub trait WindowManager {
/// Get all windows for a specific application by PID
fn get_windows_for_app(&self, pid: i32) -> PeekabooResult<Vec<WindowData>>;
/// Find a window by title substring
fn find_window_by_title(&self, pid: i32, title_substring: &str) -> PeekabooResult<WindowData>;
/// Get window by index (0 = frontmost)
fn get_window_by_index(&self, pid: i32, index: i32) -> PeekabooResult<WindowData>;
/// Activate/focus a window
fn activate_window(&self, window: &WindowData) -> PeekabooResult<()>;
}
/// Trait for application discovery and management
pub trait ApplicationManager {
/// Get all running applications
fn get_all_applications(&self) -> PeekabooResult<Vec<ApplicationInfo>>;
/// Find an application by identifier (name, bundle ID, or PID)
fn find_application(&self, identifier: &str) -> PeekabooResult<ApplicationInfo>;
/// Activate/focus an application
fn activate_application(&self, app: &ApplicationInfo) -> PeekabooResult<()>;
/// Check if an application is currently active/focused
fn is_application_active(&self, app: &ApplicationInfo) -> PeekabooResult<bool>;
}
/// Trait for system permission checking
pub trait PermissionManager {
/// Check if screen recording permission is granted
fn check_screen_recording_permission(&self) -> bool;
/// Check if accessibility permission is granted
fn check_accessibility_permission(&self) -> bool;
/// Request screen recording permission (may show system dialog)
fn request_screen_recording_permission(&self) -> PeekabooResult<bool>;
/// Request accessibility permission (may show system dialog)
fn request_accessibility_permission(&self) -> PeekabooResult<bool>;
}
/// Combined platform interface
pub trait Platform: ScreenCapture + WindowManager + ApplicationManager + PermissionManager {
/// Get the platform name (e.g., "linux", "windows", "darwin")
fn platform_name(&self) -> &'static str;
/// Get the platform version
fn platform_version(&self) -> String;
/// Initialize the platform (setup any required resources)
fn initialize(&mut self) -> PeekabooResult<()>;
/// Cleanup platform resources
fn cleanup(&mut self) -> PeekabooResult<()>;
}

View File

@ -0,0 +1,96 @@
use std::path::Path;
/// Sanitize a filename by removing or replacing invalid characters
pub fn sanitize_filename(filename: &str) -> String {
filename
.chars()
.map(|c| match c {
// Replace invalid filename characters
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
// Keep valid characters
c if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' || c == ' ' => c,
// Replace other characters with underscore
_ => '_',
})
.collect::<String>()
.trim()
.to_string()
}
/// Join path components in a cross-platform way
pub fn join_path(base: &str, filename: &str) -> String {
let path = Path::new(base).join(filename);
path.to_string_lossy().to_string()
}
/// Ensure a directory exists, creating it if necessary
pub fn ensure_directory_exists(path: &str) -> std::io::Result<()> {
let path = Path::new(path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(())
}
/// Get a unique filename by appending a number if the file already exists
pub fn get_unique_filename(path: &str) -> String {
let path_obj = Path::new(path);
if !path_obj.exists() {
return path.to_string();
}
let stem = path_obj.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("file");
let extension = path_obj.extension()
.and_then(|s| s.to_str())
.unwrap_or("");
let parent = path_obj.parent()
.unwrap_or_else(|| Path::new("."));
for i in 1..1000 {
let new_filename = if extension.is_empty() {
format!("{}_{}", stem, i)
} else {
format!("{}_{}.{}", stem, i, extension)
};
let new_path = parent.join(new_filename);
if !new_path.exists() {
return new_path.to_string_lossy().to_string();
}
}
// Fallback with timestamp if we can't find a unique name
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S_%3f");
let fallback_filename = if extension.is_empty() {
format!("{}_{}", stem, timestamp)
} else {
format!("{}_{}.{}", stem, timestamp, extension)
};
parent.join(fallback_filename).to_string_lossy().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_filename() {
assert_eq!(sanitize_filename("normal_file.txt"), "normal_file.txt");
assert_eq!(sanitize_filename("file/with\\invalid:chars"), "file_with_invalid_chars");
assert_eq!(sanitize_filename("file*with?special\"chars"), "file_with_special_chars");
assert_eq!(sanitize_filename(" spaced file "), "spaced file");
}
#[test]
fn test_join_path() {
let result = join_path("/home/user", "file.txt");
assert!(result.contains("file.txt"));
let result = join_path(".", "file.txt");
assert_eq!(result, "./file.txt");
}
}

View File

@ -0,0 +1,2 @@
pub mod file_utils;

View File

@ -0,0 +1,145 @@
use std::process::Command;
use std::path::Path;
#[test]
fn test_binary_exists() {
let binary_path = "target/debug/peekaboo";
assert!(Path::new(binary_path).exists(), "Binary should exist after build");
}
#[test]
fn test_help_command() {
let output = Command::new("target/debug/peekaboo")
.arg("--help")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("A cross-platform utility for screen capture"));
assert!(stdout.contains("image"));
assert!(stdout.contains("list"));
}
#[test]
fn test_version_command() {
let output = Command::new("target/debug/peekaboo")
.arg("--version")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("peekaboo"));
}
#[test]
fn test_list_help() {
let output = Command::new("target/debug/peekaboo")
.args(&["list", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("List running applications or windows"));
assert!(stdout.contains("apps"));
assert!(stdout.contains("windows"));
assert!(stdout.contains("server-status"));
}
#[test]
fn test_image_help() {
let output = Command::new("target/debug/peekaboo")
.args(&["image", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Capture screen or window images"));
assert!(stdout.contains("--app"));
assert!(stdout.contains("--path"));
assert!(stdout.contains("--mode"));
assert!(stdout.contains("--format"));
}
#[test]
fn test_invalid_command() {
let output = Command::new("target/debug/peekaboo")
.arg("invalid-command")
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
}
#[test]
fn test_list_apps_help() {
let output = Command::new("target/debug/peekaboo")
.args(&["list", "apps", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("List all running applications"));
}
#[test]
fn test_list_windows_help() {
let output = Command::new("target/debug/peekaboo")
.args(&["list", "windows", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("List windows for a specific application"));
}
#[test]
fn test_list_server_status_help() {
let output = Command::new("target/debug/peekaboo")
.args(&["list", "server-status", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Check server permissions status"));
}
// Note: These tests will fail in headless environments but are useful for local testing
#[test]
#[ignore] // Ignore by default since it requires a display
fn test_list_apps_json() {
let output = Command::new("target/debug/peekaboo")
.args(&["list", "apps", "--json-output"])
.output()
.expect("Failed to execute command");
if output.status.success() {
let stdout = String::from_utf8(output.stdout).unwrap();
// Should be valid JSON
let _: serde_json::Value = serde_json::from_str(&stdout)
.expect("Output should be valid JSON");
}
}
#[test]
#[ignore] // Ignore by default since it requires a display
fn test_server_status_json() {
let output = Command::new("target/debug/peekaboo")
.args(&["list", "server-status", "--json-output"])
.output()
.expect("Failed to execute command");
if output.status.success() {
let stdout = String::from_utf8(output.stdout).unwrap();
// Should be valid JSON
let _: serde_json::Value = serde_json::from_str(&stdout)
.expect("Output should be valid JSON");
}
}

View File

@ -0,0 +1,177 @@
use peekaboo::models::*;
use peekaboo::errors::*;
use peekaboo::utils::file_utils;
#[test]
fn test_image_format_extension() {
assert_eq!(ImageFormat::Png.extension(), "png");
assert_eq!(ImageFormat::Jpg.extension(), "jpg");
}
#[test]
fn test_capture_mode_values() {
// Test that all capture modes can be created
let _screen = CaptureMode::Screen;
let _window = CaptureMode::Window;
let _multi = CaptureMode::Multi;
}
#[test]
fn test_capture_focus_values() {
// Test that all capture focus values can be created
let _background = CaptureFocus::Background;
let _auto = CaptureFocus::Auto;
let _foreground = CaptureFocus::Foreground;
}
#[test]
fn test_window_bounds_creation() {
let bounds = WindowBounds {
x_coordinate: 100,
y_coordinate: 200,
width: 800,
height: 600,
};
assert_eq!(bounds.x_coordinate, 100);
assert_eq!(bounds.y_coordinate, 200);
assert_eq!(bounds.width, 800);
assert_eq!(bounds.height, 600);
}
#[test]
fn test_window_data_creation() {
let bounds = WindowBounds {
x_coordinate: 0,
y_coordinate: 0,
width: 1920,
height: 1080,
};
let window = WindowData {
window_id: 12345,
title: "Test Window".to_string(),
bounds,
is_on_screen: true,
window_index: 0,
};
assert_eq!(window.window_id, 12345);
assert_eq!(window.title, "Test Window");
assert!(window.is_on_screen);
assert_eq!(window.window_index, 0);
}
#[test]
fn test_application_info_creation() {
let app = ApplicationInfo {
app_name: "TestApp".to_string(),
bundle_id: "com.test.app".to_string(),
pid: 1234,
is_active: true,
window_count: 2,
};
assert_eq!(app.app_name, "TestApp");
assert_eq!(app.bundle_id, "com.test.app");
assert_eq!(app.pid, 1234);
assert!(app.is_active);
assert_eq!(app.window_count, 2);
}
#[test]
fn test_server_permissions_creation() {
let permissions = ServerPermissions {
screen_recording: true,
accessibility: false,
};
assert!(permissions.screen_recording);
assert!(!permissions.accessibility);
}
#[test]
fn test_server_status_creation() {
let permissions = ServerPermissions {
screen_recording: true,
accessibility: true,
};
let status = ServerStatus {
permissions,
platform: "Linux".to_string(),
version: "1.0.0".to_string(),
};
assert_eq!(status.platform, "Linux");
assert_eq!(status.version, "1.0.0");
assert!(status.permissions.screen_recording);
assert!(status.permissions.accessibility);
}
#[test]
fn test_peekaboo_error_creation() {
let error = PeekabooError::ScreenRecordingPermissionDenied;
assert!(matches!(error, PeekabooError::ScreenRecordingPermissionDenied));
let error = PeekabooError::UnknownError("test error".to_string());
assert!(matches!(error, PeekabooError::UnknownError(_)));
}
#[test]
fn test_file_utils_join_path() {
let result = file_utils::join_path("/tmp", "test.png");
assert_eq!(result, "/tmp/test.png");
let result = file_utils::join_path("/tmp/", "test.png");
assert_eq!(result, "/tmp/test.png");
let result = file_utils::join_path("", "test.png");
assert_eq!(result, "test.png");
}
#[test]
fn test_file_utils_sanitize_filename() {
let result = file_utils::sanitize_filename("test file.png");
assert_eq!(result, "test file.png");
let result = file_utils::sanitize_filename("test*file?.png");
assert_eq!(result, "test_file_.png");
let result = file_utils::sanitize_filename("test/file\\name.png");
assert_eq!(result, "test_file_name.png");
}
#[test]
fn test_clone_implementations() {
// Test that all our structs implement Clone properly
let format = ImageFormat::Png;
let _cloned = format.clone();
let mode = CaptureMode::Screen;
let _cloned = mode.clone();
let focus = CaptureFocus::Auto;
let _cloned = focus.clone();
let bounds = WindowBounds { x_coordinate: 0, y_coordinate: 0, width: 100, height: 100 };
let _cloned = bounds.clone();
let permissions = ServerPermissions {
screen_recording: true,
accessibility: false,
};
let _cloned = permissions.clone();
}
#[test]
fn test_debug_implementations() {
// Test that all our structs implement Debug properly
let format = ImageFormat::Png;
let debug_str = format!("{:?}", format);
assert!(debug_str.contains("Png"));
let bounds = WindowBounds { x_coordinate: 0, y_coordinate: 0, width: 100, height: 100 };
let debug_str = format!("{:?}", bounds);
assert!(debug_str.contains("100"));
}