diff --git a/Cargo.lock b/Cargo.lock index 79f881e..740d11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,7 +67,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -76,6 +76,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[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" @@ -85,17 +91,66 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arboard" @@ -223,9 +278,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitstream-io" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "block" @@ -303,7 +358,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -320,9 +375,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.31" +version = "1.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" dependencies = [ "jobserver", "libc", @@ -363,6 +418,60 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cleave" version = "0.1.0" @@ -370,15 +479,32 @@ dependencies = [ "anyhow", "arboard", "bytemuck", + "chrono", + "clap", + "cleave-daemon", "cleave-graphics", "glam", "image", "pollster", + "thiserror 2.0.0", "wgpu", "winit", "xcap", ] +[[package]] +name = "cleave-daemon" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitflags 2.6.0", + "clap", + "device_query", + "dirs", + "fd-lock", + "thiserror 2.0.0", +] + [[package]] name = "cleave-graphics" version = "0.1.0" @@ -386,7 +512,7 @@ dependencies = [ "bytemuck", "glam", "image", - "thiserror", + "thiserror 2.0.0", "wgpu", "winit", ] @@ -416,6 +542,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -566,6 +698,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "device_query" +version = "2.1.0" +source = "git+https://github.com/exotik850/device_query?branch=refactor#65abb2d257fe01150b1fc2c04249cd58e331a928" +dependencies = [ + "macos-accessibility-client", + "pkg-config", + "readkey", + "readmouse", + "windows 0.48.0", + "x11", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -645,6 +811,17 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "fdeflate" version = "0.3.6" @@ -735,9 +912,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.29.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c9417a5dc586fc0c0cb67891170e59cc11e9dc79ba1c11ddd2c56ca3f3b90" +checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" dependencies = [ "bytemuck", ] @@ -790,7 +967,7 @@ checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.68", "windows 0.58.0", ] @@ -836,9 +1013,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "heck" @@ -858,11 +1035,34 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[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 = "image" -version = "0.25.4" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -904,7 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", ] [[package]] @@ -918,6 +1118,12 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -938,7 +1144,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.68", "walkdir", "windows-sys 0.45.0", ] @@ -998,9 +1204,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libdbus-sys" @@ -1013,13 +1219,12 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -1080,6 +1285,16 @@ dependencies = [ "imgref", ] +[[package]] +name = "macos-accessibility-client" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1096,6 +1311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", + "rayon", ] [[package]] @@ -1161,7 +1377,7 @@ dependencies = [ "rustc-hash", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.68", "unicode-xid", ] @@ -1177,7 +1393,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1524,6 +1740,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.48" @@ -1624,9 +1846,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -1803,7 +2025,7 @@ dependencies = [ "rand_chacha", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.68", "v_frame", "wasm-bindgen", ] @@ -1819,6 +2041,7 @@ dependencies = [ "loop9", "quick-error", "rav1e", + "rayon", "rgb", ] @@ -1848,6 +2071,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "readkey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7677f98ca49bc9bb26e04c8abf80ba579e2cb98e8a384a0ff8128ad70670d249" + +[[package]] +name = "readmouse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be105c72a1e6a5a1198acee3d5b506a15676b74a02ecd78060042a447f408d94" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1866,6 +2101,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.68", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -1886,9 +2132,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ "bitflags 2.6.0", "errno", @@ -2019,7 +2265,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.68", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -2060,11 +2306,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[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.86" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -2115,18 +2367,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.68", +] + +[[package]] +name = "thiserror" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668" +dependencies = [ + "thiserror-impl 2.0.0", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972" dependencies = [ "proc-macro2", "quote", @@ -2249,6 +2521,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "v_frame" version = "0.3.8" @@ -2535,7 +2813,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.68", "wgpu-hal", "wgpu-types", ] @@ -2577,7 +2855,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.68", "wasm-bindgen", "web-sys", "wgpu-types", @@ -2627,6 +2905,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.57.0" @@ -2647,6 +2934,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -3019,6 +3315,16 @@ dependencies = [ "memchr", ] +[[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 = "x11-dl" version = "2.21.0" @@ -3064,7 +3370,7 @@ dependencies = [ "log", "percent-encoding", "sysinfo", - "thiserror", + "thiserror 1.0.68", "windows 0.58.0", "xcb", ] @@ -3107,9 +3413,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index c4aa50b..58474ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ name = "cleave" version = "0.1.0" edition = "2021" +authors = ["Exotik850"] [workspace] -members = ["cleave-graphics"] - +members = [ "cleave-daemon","cleave-graphics"] [dependencies] bytemuck = { workspace = true } @@ -17,18 +17,24 @@ image = { workspace = true } pollster = { workspace = true } wgpu = { workspace = true } xcap = { workspace = true } +thiserror = { workspace = true } +clap = { workspace = true } cleave-graphics = { path = "cleave-graphics" } - +cleave-daemon = { path = "cleave-daemon" } +chrono = "0.4.38" [workspace.dependencies] +clap = { version = "4.5.20", features = ["derive"] } anyhow = "1" arboard = "3.4.1" +thiserror = "2" bytemuck = { version = "1.19.0", features = ["derive"] } -glam = { version = "0.29.1", features = ["bytemuck"] } -image = "0.25.4" +glam = { version = "0.29", features = ["bytemuck"] } +image = "0.25" pollster = "0.4.0" wgpu = "23.0.0" winit = { version = "0.30.5", features = ["rwh_06"] } +winit_input_helper = { git = "https://github.com/zachdedoo13/winit_input_helper_updated.git" } xcap = "0.0.14" [profile.release] @@ -37,3 +43,13 @@ lto = "thin" [build] rustflags = ["-C", "target-cpu=native"] + +[features] + +[[bin]] +name = "cleave-daemon" +path = "cleave-daemon/src/main.rs" + +[[bin]] +name = "cleave" +path = "src/main.rs" \ No newline at end of file diff --git a/cleave-daemon/Cargo.toml b/cleave-daemon/Cargo.toml new file mode 100644 index 0000000..9989152 --- /dev/null +++ b/cleave-daemon/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cleave-daemon" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +device_query = { git = "https://github.com/exotik850/device_query", branch = "refactor" } +bitflags = "2.6.0" +fd-lock = "4.0.2" +dirs = "5.0.1" diff --git a/cleave-daemon/src/hotkey.rs b/cleave-daemon/src/hotkey.rs new file mode 100644 index 0000000..a873482 --- /dev/null +++ b/cleave-daemon/src/hotkey.rs @@ -0,0 +1,287 @@ +use std::{borrow::Borrow, fmt::Display, str::FromStr}; + +pub use crate::modifiers::Modifiers; +pub use device_query::Keycode; + +#[derive(thiserror::Error, Debug)] +pub enum HotKeyParseError { + #[error("Couldn't recognize \"{0}\" as a valid key for hotkey, if you feel like it should be, please report this to https://github.com/tauri-apps/muda")] + UnsupportedKey(String), + #[error("Found empty token while parsing hotkey: {0}")] + EmptyToken(String), + #[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")] + InvalidFormat(String), +} + +/// A keyboard shortcut that consists of an optional combination +/// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and +/// one key ([`Code`](crate::hotkey::Code)). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HotKey { + /// The hotkey modifiers. + pub mods: Modifiers, + /// The hotkey key. + pub key: Keycode, +} + +impl HotKey { + /// Creates a new hotkey to define keyboard shortcuts throughout your application. + /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] + pub fn new(mods: Option, key: Keycode) -> Self { + let mods = mods.unwrap_or_default(); + Self { mods, key } + } + + /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. + pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; + let modifiers = modifiers.borrow(); + let key = key.borrow(); + (self.mods == (*modifiers & base_mods)) && (self.key == *key) + } + + /// Converts this hotkey into a string. + pub fn into_string(self) -> String { + let mut hotkey = String::new(); + let state = self.mods; + if state.contains(Modifiers::SHIFT) { + hotkey.push_str("shift+"); + } + if state.contains(Modifiers::CONTROL) { + hotkey.push_str("control+"); + } + if state.contains(Modifiers::ALT) { + hotkey.push_str("alt+"); + } + if state.contains(Modifiers::SUPER) { + hotkey.push_str("super+"); + } + hotkey.push_str(&format!("{:?}", self.key).to_lowercase()); + hotkey + } +} + +impl Display for HotKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.into_string()) + } +} + +// HotKey::from_str is available to be backward +// compatible with tauri and it also open the option +// to generate hotkey from string +impl FromStr for HotKey { + type Err = HotKeyParseError; + fn from_str(hotkey_string: &str) -> Result { + parse_hotkey(hotkey_string) + } +} + +impl TryFrom<&str> for HotKey { + type Error = HotKeyParseError; + + fn try_from(value: &str) -> Result { + parse_hotkey(value) + } +} + +impl TryFrom for HotKey { + type Error = HotKeyParseError; + + fn try_from(value: String) -> Result { + parse_hotkey(&value) + } +} + +fn parse_hotkey(hotkey: &str) -> Result { + let tokens = hotkey.split('+').collect::>(); + + let mut mods = Modifiers::empty(); + let mut key = None; + + match tokens.len() { + // single key hotkey + 1 => { + key = Some(parse_key(tokens[0])?); + } + // modifiers and key comobo hotkey + _ => { + for raw in tokens { + let token = raw.trim(); + + if token.is_empty() { + return Err(HotKeyParseError::EmptyToken(hotkey.to_string())); + } + + if key.is_some() { + // At this point we have parsed the modifiers and a main key, so by reaching + // this code, the function either received more than one main key or + // the hotkey is not in the right order + // examples: + // 1. "Ctrl+Shift+C+A" => only one main key should be allowd. + // 2. "Ctrl+C+Shift" => wrong order + return Err(HotKeyParseError::InvalidFormat(hotkey.to_string())); + } + + match token.to_uppercase().as_str() { + "OPTION" | "ALT" => { + mods |= Modifiers::ALT; + } + "CONTROL" | "CTRL" => { + mods |= Modifiers::CONTROL; + } + "COMMAND" | "CMD" | "SUPER" => { + mods |= Modifiers::SUPER; + } + "SHIFT" => { + mods |= Modifiers::SHIFT; + } + #[cfg(target_os = "macos")] + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + mods |= Modifiers::SUPER; + } + #[cfg(not(target_os = "macos"))] + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + mods |= Modifiers::CONTROL; + } + "META" => { + mods |= Modifiers::META; + } + _ => { + key = Some(parse_key(token)?); + } + } + } + } + } + + Ok(HotKey::new( + Some(mods), + key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, + )) +} + +fn parse_key(key: &str) -> Result { + use Keycode::*; + match key.to_uppercase().as_str() { + "BACKQUOTE" | "`" => Ok(Grave), + "BACKSLASH" | "\\" => Ok(BackSlash), + "BRACKETLEFT" | "[" => Ok(LeftBracket), + "BRACKETRIGHT" | "]" => Ok(RightBracket), + // "PAUSE" | "PAUSEBREAK" => Ok(), + "COMMA" | "," => Ok(Comma), + "DIGIT0" | "0" => Ok(Key0), + "DIGIT1" | "1" => Ok(Key1), + "DIGIT2" | "2" => Ok(Key2), + "DIGIT3" | "3" => Ok(Key3), + "DIGIT4" | "4" => Ok(Key4), + "DIGIT5" | "5" => Ok(Key5), + "DIGIT6" | "6" => Ok(Key6), + "DIGIT7" | "7" => Ok(Key7), + "DIGIT8" | "8" => Ok(Key8), + "DIGIT9" | "9" => Ok(Key9), + "EQUAL" | "=" => Ok(Equal), + "KEYA" | "A" => Ok(A), + "KEYB" | "B" => Ok(B), + "KEYC" | "C" => Ok(C), + "KEYD" | "D" => Ok(D), + "KEYE" | "E" => Ok(E), + "KEYF" | "F" => Ok(F), + "KEYG" | "G" => Ok(G), + "KEYH" | "H" => Ok(H), + "KEYI" | "I" => Ok(I), + "KEYJ" | "J" => Ok(J), + "KEYK" | "K" => Ok(K), + "KEYL" | "L" => Ok(L), + "KEYM" | "M" => Ok(M), + "KEYN" | "N" => Ok(N), + "KEYO" | "O" => Ok(O), + "KEYP" | "P" => Ok(P), + "KEYQ" | "Q" => Ok(Q), + "KEYR" | "R" => Ok(R), + "KEYS" | "S" => Ok(S), + "KEYT" | "T" => Ok(T), + "KEYU" | "U" => Ok(U), + "KEYV" | "V" => Ok(V), + "KEYW" | "W" => Ok(W), + "KEYX" | "X" => Ok(X), + "KEYY" | "Y" => Ok(Y), + "KEYZ" | "Z" => Ok(Z), + "MINUS" | "-" => Ok(Minus), + "PERIOD" | "." => Ok(Dot), + "QUOTE" | "'" => Ok(Apostrophe), + "SEMICOLON" | ";" => Ok(Semicolon), + "SLASH" | "/" => Ok(Slash), + "BACKSPACE" => Ok(Backspace), + "CAPSLOCK" => Ok(CapsLock), + "ENTER" => Ok(Enter), + "SPACE" => Ok(Space), + "TAB" => Ok(Tab), + "DELETE" => Ok(Delete), + "END" => Ok(End), + "HOME" => Ok(Home), + "INSERT" => Ok(Insert), + "PAGEDOWN" => Ok(PageDown), + "PAGEUP" => Ok(PageUp), + // "PRINTSCREEN" => Ok(Keycode::), + // "SCROLLLOCK" => Ok(ScrollLock), + "ARROWDOWN" | "DOWN" => Ok(Down), + "ARROWLEFT" | "LEFT" => Ok(Left), + "ARROWRIGHT" | "RIGHT" => Ok(Right), + "ARROWUP" | "UP" => Ok(Up), + // "NUMLOCK" => Ok(), + "NUMPAD0" | "NUM0" => Ok(Numpad0), + "NUMPAD1" | "NUM1" => Ok(Numpad1), + "NUMPAD2" | "NUM2" => Ok(Numpad2), + "NUMPAD3" | "NUM3" => Ok(Numpad3), + "NUMPAD4" | "NUM4" => Ok(Numpad4), + "NUMPAD5" | "NUM5" => Ok(Numpad5), + "NUMPAD6" | "NUM6" => Ok(Numpad6), + "NUMPAD7" | "NUM7" => Ok(Numpad7), + "NUMPAD8" | "NUM8" => Ok(Numpad8), + "NUMPAD9" | "NUM9" => Ok(Numpad9), + "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd), + "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal), + "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide), + "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter), + "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEquals), + "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply), + "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract), + "ESCAPE" | "ESC" => Ok(Escape), + "F1" => Ok(F1), + "F2" => Ok(F2), + "F3" => Ok(F3), + "F4" => Ok(F4), + "F5" => Ok(F5), + "F6" => Ok(F6), + "F7" => Ok(F7), + "F8" => Ok(F8), + "F9" => Ok(F9), + "F10" => Ok(F10), + "F11" => Ok(F11), + "F12" => Ok(F12), + // "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(Keycode::), + // "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp), + // "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute), + // "MEDIAPLAY" => Ok(Media), + // "MEDIAPAUSE" => Ok(MediaPause), + // "MEDIAPLAYPAUSE" => Ok(MediaPlayPause), + // "MEDIASTOP" => Ok(MediaStop), + // "MEDIATRACKNEXT" => Ok(MediaTrackNext), + // "MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious), + "F13" => Ok(F13), + "F14" => Ok(F14), + "F15" => Ok(F15), + "F16" => Ok(F16), + "F17" => Ok(F17), + "F18" => Ok(F18), + "F19" => Ok(F19), + "F20" => Ok(F20), + // "F21" => Ok(F21), + // "F22" => Ok(F22), + // "F23" => Ok(F23), + // "F24" => Ok(F24), + _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), + } +} diff --git a/cleave-daemon/src/lib.rs b/cleave-daemon/src/lib.rs new file mode 100644 index 0000000..86b17d3 --- /dev/null +++ b/cleave-daemon/src/lib.rs @@ -0,0 +1,5 @@ +pub use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; +pub use hotkey::HotKey; +pub use modifiers::Modifiers; +mod hotkey; +mod modifiers; diff --git a/cleave-daemon/src/main.rs b/cleave-daemon/src/main.rs new file mode 100644 index 0000000..4405d61 --- /dev/null +++ b/cleave-daemon/src/main.rs @@ -0,0 +1,91 @@ +use clap::Parser; +use cleave_daemon::{DeviceEvents, DeviceEventsHandler, HotKey, Keycode, Modifiers}; +use std::{collections::HashSet, time::Duration}; + +#[derive(clap::Parser, Debug)] +struct Args { + /// The amount of time to sleep between each event loop iteration in milliseconds + #[arg(short, long, default_value = "100")] + sleep: u64, + + /// The hotkey to use to start the event loop + #[arg(short = 'm', long, default_value = "Shift+X")] + hotkey: HotKey, + + /// Whether or not to stay alive after the hotkey is pressed + #[arg(short, long)] + persist: bool, +} + +#[derive(Debug)] +struct KeyAction { + key: Keycode, + pressed: bool, +} + +fn main() -> anyhow::Result<()> { + let config_path = dirs::config_dir().expect("Could not find config directory"); + let args: Args = Args::parse(); + let handler = DeviceEventsHandler::new(Duration::from_millis(args.sleep)) + .expect("Could not create event loop"); + let (tx, rx) = std::sync::mpsc::channel(); + let ta = tx.clone(); + let _g1 = handler.on_key_down(move |key| { + ta.send(KeyAction { key, pressed: true }).unwrap(); + }); + let tb = tx; + let _g2 = handler.on_key_up(move |key| { + tb.send(KeyAction { + key, + pressed: false, + }) + .unwrap(); + }); + + let mut pressed = HashSet::new(); + let mut mods = Modifiers::empty(); + for event in rx.iter() { + if let Some(m) = Modifiers::from_keycode(event.key) { + if event.pressed { + mods |= m; + } else { + mods &= !m; + } + } + if event.pressed { + pressed.insert(event.key); + } else { + pressed.remove(&event.key); + } + if args.hotkey.matches(mods, event.key) && event.pressed { + run_cleave()?; + pressed.clear(); + mods = Modifiers::empty(); + if !args.persist { + break; + } + } + } + Ok(()) +} + +fn run_cleave() -> anyhow::Result<()> { + let mut cleave = std::process::Command::new("cleave"); + cleave.args(std::env::args().skip(1)); + match cleave.status() { + Ok(status) => { + if !status.success() { + anyhow::bail!("cleave exited with status: {}", status); + } + } + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + anyhow::bail!("Could not find cleave in PATH"); + } + _ => { + anyhow::bail!("Could not start cleave: {}", e); + } + }, + }; + Ok(()) +} diff --git a/cleave-daemon/src/modifiers.rs b/cleave-daemon/src/modifiers.rs new file mode 100644 index 0000000..2c33355 --- /dev/null +++ b/cleave-daemon/src/modifiers.rs @@ -0,0 +1,64 @@ +//! Modifier key data. +//! +//! Modifier keys like Shift and Control alter the character value +//! and are used in keyboard shortcuts. +//! +//! Use the constants to match for combinations of the modifier keys. + +use device_query::Keycode; + +bitflags::bitflags! { + /// Pressed modifier keys. + /// + /// Specification: + /// + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Modifiers: u32 { + const ALT = 0x01; + const ALT_GRAPH = 0x2; + const CAPS_LOCK = 0x4; + const CONTROL = 0x8; + const FN = 0x10; + const FN_LOCK = 0x20; + const META = 0x40; + const NUM_LOCK = 0x80; + const SCROLL_LOCK = 0x100; + const SHIFT = 0x200; + const SYMBOL = 0x400; + const SYMBOL_LOCK = 0x800; + const HYPER = 0x1000; + const SUPER = 0x2000; + } +} + +impl Modifiers { + /// Return `true` if a shift key is pressed. + pub fn shift(&self) -> bool { + self.contains(Modifiers::SHIFT) + } + + /// Return `true` if a control key is pressed. + pub fn ctrl(&self) -> bool { + self.contains(Modifiers::CONTROL) + } + + /// Return `true` if an alt key is pressed. + pub fn alt(&self) -> bool { + self.contains(Modifiers::ALT) + } + + /// Return `true` if a meta key is pressed. + pub fn meta(&self) -> bool { + self.contains(Modifiers::META) + } + + pub fn from_keycode(key: Keycode) -> Option { + match key { + Keycode::LShift | Keycode::RShift => Some(Modifiers::SHIFT), + Keycode::LControl | Keycode::RControl => Some(Modifiers::CONTROL), + Keycode::LAlt | Keycode::RAlt => Some(Modifiers::ALT), + Keycode::LMeta | Keycode::RMeta => Some(Modifiers::META), + _ => None, + } + } +} diff --git a/cleave-graphics/Cargo.toml b/cleave-graphics/Cargo.toml index 455f466..e3c4d14 100644 --- a/cleave-graphics/Cargo.toml +++ b/cleave-graphics/Cargo.toml @@ -9,4 +9,4 @@ image = { workspace = true } wgpu = { workspace = true } bytemuck = { workspace = true } winit = { workspace = true } -thiserror = "1" \ No newline at end of file +thiserror = { workspace = true } \ No newline at end of file diff --git a/cleave-graphics/src/graphics_impl.rs b/cleave-graphics/src/graphics_impl.rs index 1e4e77b..f0fd542 100644 --- a/cleave-graphics/src/graphics_impl.rs +++ b/cleave-graphics/src/graphics_impl.rs @@ -163,6 +163,7 @@ impl GraphicsPass<'_, '_, W> { pub fn finish(mut self) { drop(self.pass); let Some(encoder) = self.encoder.take() else { + eprintln!("No encoder available"); return; }; self.graphics.queue.submit(Some(encoder.finish())); @@ -179,13 +180,12 @@ fn find_config(surface: &Surface, adapter: &wgpu::Adapter, size: UVec2) -> Surfa .iter() .find(|f| f.is_srgb()) .unwrap_or(&surface_config.formats[0]); - SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: *format, width: size.x, height: size.y, - present_mode: wgpu::PresentMode::Immediate, + present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: 2, alpha_mode: surface_config.alpha_modes[0], view_formats: vec![], diff --git a/src/app/context.rs b/src/app/context.rs new file mode 100644 index 0000000..7bfd913 --- /dev/null +++ b/src/app/context.rs @@ -0,0 +1,115 @@ +use bytemuck::{Pod, Zeroable}; +use glam::Vec2; +use image::GenericImageView; +// use pixels::{Pixels, SurfaceTexture}; +use winit::{ + dpi::PhysicalSize, + window::{Icon, Window, WindowAttributes}, +}; + +// use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; +use cleave_graphics::prelude::*; + +#[repr(C)] +#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, Default, Debug)] +pub struct SelectionUniforms { + pub screen_size: Vec2, + pub drag_start: Vec2, + pub drag_end: Vec2, + pub selection_start: Vec2, + pub selection_end: Vec2, + pub time: f32, + pub is_dragging: u32, // 0 = None, 1 = Dragging, 2 = Selected, 3 = Both +} + +impl std::fmt::Display for SelectionUniforms { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "size: {:?}, is_dragging: {}, drag_start: {:?}, drag_end: {:?}, selection_start: {:?}, selection_end: {:?}, time: {}", + self.screen_size, self.is_dragging, self.drag_start, self.drag_end, self.selection_start, self.selection_end, self.time) + } +} + +pub struct CleaveContext { + pub graphics: Graphics, + pub total_time: f32, + last_frame: std::time::Instant, +} + +impl CleaveContext { + pub fn new( + event_loop: &winit::event_loop::ActiveEventLoop, + width: u32, + height: u32, + ) -> anyhow::Result { + let (ico_width, ico_height, rgba) = crate::util::load_icon()?; + let window = event_loop.create_window( + WindowAttributes::default() + .with_inner_size(PhysicalSize::new(width, height)) + .with_title("Cleave") + .with_resizable(false) + .with_decorations(false) + .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) + .with_visible(false) + .with_window_icon(Some(Icon::from_rgba(rgba, ico_width, ico_height)?)), + )?; + + let graphics = Graphics::new(window, width, height); + let graphics = pollster::block_on(graphics)?; + + graphics + .window + .set_cursor_grab(winit::window::CursorGrabMode::Confined) + .or_else(|_| { + graphics + .window + .set_cursor_grab(winit::window::CursorGrabMode::Locked) + })?; + + Ok(Self { + total_time: 0.0, + last_frame: std::time::Instant::now(), + graphics, + }) + } + + pub fn draw( + &mut self, + bundle: Option<&GraphicsBundle>, + ) { + let mut pass = match self.graphics.render() { + Ok(pass) => pass, + Err(err) => { + eprintln!("Error rendering frame: {:?}", err); + return; + } + }; + if let Some(bundle) = bundle { + bundle.draw(&mut pass); + } + pass.finish(); + self.graphics.request_redraw(); + } + + pub fn update(&mut self) { + let time = self.last_frame.elapsed().as_secs_f32(); + self.total_time += time; + self.last_frame = std::time::Instant::now(); + } + + pub fn window_id(&self) -> winit::window::WindowId { + self.graphics.id() + } + + pub fn destroy(&self) { + self.graphics.window.set_minimized(true); + } + + pub fn set_window_visibility(&self, val: bool) { + self.graphics.set_visible(val); + } + + pub fn size(&self) -> (f32, f32) { + let size = self.graphics.window.outer_size(); + (size.width as f32, size.height as f32) + } +} diff --git a/src/app/current_image.rs b/src/app/current_image.rs new file mode 100644 index 0000000..31be4c9 --- /dev/null +++ b/src/app/current_image.rs @@ -0,0 +1,64 @@ +use cleave_graphics::prelude::GraphicsBundle; +use glam::Vec2; +use image::RgbaImage; + +use crate::selection::UserSelection; + +use super::context::SelectionUniforms; + +pub struct CurrentImage { + pub image: RgbaImage, + pub bundle: GraphicsBundle, +} + +impl CurrentImage { + pub fn capture_image( + monitor: Option, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> anyhow::Result { + let img = crate::util::capture_screen(monitor)?; + let bundle = GraphicsBundle::new( + img.clone().into(), + device, + queue, + wgpu::PrimitiveTopology::TriangleStrip, + format, + ); + Ok(Self { image: img, bundle }) + } + + pub fn update_uniforms(&mut self, time: f32, user: &UserSelection, (w, h): (f32, f32)) { + self.bundle.uniforms.time = time; + + // println!("{}", self.bundle.uniforms); + self.bundle.uniforms.screen_size.x = w; + self.bundle.uniforms.screen_size.y = h; + + let drag = &user.drag; + let selection = &user.selection; + self.bundle.uniforms.is_dragging = match (drag, selection) { + (Some(d), Some(s)) if (d.x != 0. || d.y != 0.) && (s.x != 0. || s.y != 0.) => 3, + (Some(d), None) if (d.x != 0. || d.y != 0.) => 1, + (None, Some(s)) if (s.x != 0. || s.y != 0.) => 2, + _ => 0, + }; + if let Some(drag) = drag { + self.bundle.uniforms.drag_start = Vec2::new(drag.x, drag.y); + self.bundle.uniforms.drag_end = Vec2::new(drag.x + drag.w, drag.y + drag.h); + } else { + self.bundle.uniforms.drag_start = Vec2::ZERO; + self.bundle.uniforms.drag_end = Vec2::ZERO; + }; + + if let Some(selection) = selection { + self.bundle.uniforms.selection_start = Vec2::new(selection.x, selection.y); + self.bundle.uniforms.selection_end = + Vec2::new(selection.x + selection.w, selection.y + selection.h); + } else { + self.bundle.uniforms.selection_start = Vec2::ZERO; + self.bundle.uniforms.selection_end = Vec2::ZERO; + }; + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..395811f --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,240 @@ + +use crate::{ + args::{Args, Verified}, + selection::modes::{Direction, SelectionMode}, +}; + +use current_image::CurrentImage; +use state::CleaveState; +use winit::{ + application::ApplicationHandler, + event::{ElementState, KeyEvent, WindowEvent}, + event_loop::EventLoop, + keyboard::{Key, NamedKey}, +}; + +mod context; +mod current_image; +mod state; + +use context::CleaveContext; + +pub struct App { + args: Option, + context: Option, + state: CleaveState, + current_image: Option, +} + +impl App { + pub fn new(args: Option) -> anyhow::Result { + Ok(App { + args: args.map(Args::verify).transpose()?, + context: None, + state: Default::default(), + current_image: None, + }) + } + + fn start_loop(&mut self) -> anyhow::Result<()> { + let event_loop = EventLoop::new()?; + Ok(event_loop.run_app(self)?) + } + + pub fn run(mut self) -> anyhow::Result<()> { + let Some(args) = &self.args else { + return self.start_loop(); + }; + + if args.monitor_list { + println!("Available monitors:"); + for monitor in xcap::Monitor::all().into_iter().flatten() { + println!("ID: {}", monitor.id()); + } + std::process::exit(0); + } + + if args.delay > 0 { + std::thread::sleep(std::time::Duration::from_millis(args.delay)); + } + + if let Some(output_dir) = &args.output_dir { + std::fs::create_dir_all(output_dir)?; + } + + if let Some(region) = args.region { + let img = crate::util::capture_screen(args.monitor)?; + let cropped = crate::util::crop_image(&img, Some(args), region)?; + if let Some(out) = &args.output_dir { + crate::util::save_selection(cropped, Some(args), out)?; + } else { + crate::util::save_to_clipboard(&cropped)?; + } + return Ok(()); + } + + self.start_loop() + } + + fn execute_key_command( + &mut self, + event: KeyEvent, + event_loop: &winit::event_loop::ActiveEventLoop, + ) -> bool { + let Some(context) = &mut self.context else { + return false; + }; + let KeyEvent { + logical_key: Key::Named(key), + state: pressed, + .. + } = event + else { + return false; + }; + match (pressed, key) { + (ElementState::Pressed, NamedKey::Escape) => { + event_loop.exit(); + context.destroy(); + } + (ElementState::Pressed, NamedKey::Space) => { + let Some(c_img) = self.current_image.take() else { + eprintln!("No image to crop"); + return false; + }; + let Some(rect) = self.state.selection.selection else { + eprintln!("No selection to crop"); + return false; + }; + let Ok(cropped) = crate::util::crop_image(&c_img.image, self.args.as_ref(), rect) + else { + eprintln!("Could not crop image"); + return false; + }; + match self.args.as_ref().and_then(|a| a.output_dir.as_ref()) { + Some(path) => { + if let Err(e) = + crate::util::save_selection(cropped, self.args.as_ref(), path) + { + eprintln!("{}", e); + }; + } + None => { + // Save to clipboard + if let Err(e) = crate::util::save_to_clipboard(&cropped) { + eprintln!("{}", e); + }; + } + } + event_loop.exit(); + } + (ElementState::Pressed, NamedKey::ArrowDown) => { + self.state.handle_move(Direction::Down); + } + (ElementState::Pressed, NamedKey::ArrowUp) => { + self.state.handle_move(Direction::Up); + } + (ElementState::Pressed, NamedKey::ArrowLeft) => { + self.state.handle_move(Direction::Left); + } + (ElementState::Pressed, NamedKey::ArrowRight) => { + self.state.handle_move(Direction::Right); + } + (ElementState::Pressed, NamedKey::Shift) => { + self.state.set_mode(SelectionMode::InverseResize); + } + (ElementState::Released, NamedKey::Shift | NamedKey::Control) => { + self.state.set_mode(SelectionMode::Move); + } + (ElementState::Pressed, NamedKey::Control) => { + self.state.set_mode(SelectionMode::Resize); + } + _ => {} + } + true + } + + fn handle_input( + &mut self, + event: &WindowEvent, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.state.handle_event(event); + if let WindowEvent::KeyboardInput { event, .. } = event { + self.execute_key_command(event.clone(), event_loop); + } + } + + fn capture_image(&mut self) { + let Some(context) = &self.context else { + return; + }; + let mut current_image = CurrentImage::capture_image( + self.args.as_ref().and_then(|a| a.monitor), + &context.graphics.device, + &context.graphics.queue, + context.graphics.config.format, + ) + .expect("Could not capture image"); + let (w, h) = current_image.image.dimensions(); + let (w, h) = (w as f32, h as f32); + current_image.update_uniforms(context.total_time, &self.state.selection, (w, h)); + current_image.bundle.update_buffer(&context.graphics.queue); + context.set_window_visibility(true); + self.current_image = Some(current_image); + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.context.is_some() { + return; + } + let size = crate::util::get_monitor(self.args.as_ref().and_then(|a| a.monitor)) + .expect("Could not find monitor!"); + let context = CleaveContext::new(event_loop, size.width(), size.height()) + .expect("Could not start context"); + self.context = Some(context); + self.capture_image(); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + self.handle_input(&event, event_loop); + if let Some(context) = &self.context { + if !context.graphics.is_visible().unwrap_or(true) && self.current_image.is_none() { + self.capture_image(); + } + } + match event { + WindowEvent::RedrawRequested => { + let Some(context) = &mut self.context else { + return; + }; + + if id != context.window_id() { + return; + } + context.update(); + let bund = self.current_image.as_mut().map(|c_img| { + c_img.update_uniforms( + context.total_time, + &self.state.selection, + context.size(), + ); + c_img.bundle.update_buffer(&context.graphics.queue); + &c_img.bundle + }); + context.draw(bund); + } + WindowEvent::CloseRequested => { + event_loop.exit(); + } + _ => {} + } + } +} diff --git a/src/app/state.rs b/src/app/state.rs new file mode 100644 index 0000000..6543463 --- /dev/null +++ b/src/app/state.rs @@ -0,0 +1,143 @@ + +use glam::DVec2; +use wgpu::core::command::Rect; +use winit::{ + event::{ElementState, KeyEvent, MouseButton, WindowEvent}, + keyboard::{KeyCode, ModifiersState, PhysicalKey}, +}; + +use crate::selection::{ + modes::{Direction, SelectionMode}, + UserSelection, +}; + +#[derive(Debug, Default)] +pub struct CleaveState { + pub selection: UserSelection, + mouse_position: DVec2, + mode: SelectionMode, + size: Option<(f32, f32)>, + mods: ModifiersState, +} + +impl CleaveState { + pub fn handle_event(&mut self, event: &WindowEvent) { + match event { + WindowEvent::KeyboardInput { event, .. } => { + self.handle_key(event); + } + WindowEvent::ModifiersChanged(mods) => self.mods = mods.state(), + WindowEvent::CursorMoved { position, .. } => { + self.mouse_position = DVec2::new(position.x, position.y); + if let Some(drag) = self.selection.drag.as_mut() { + drag.w = position.x as f32 - drag.x; + drag.h = position.y as f32 - drag.y; + } + } + WindowEvent::MouseInput { state, button, .. } => match (state, button) { + (ElementState::Pressed, MouseButton::Left) => self.start_drag(), + (ElementState::Released, MouseButton::Left) => self.end_drag(), + (_, MouseButton::Right) => self.cancel_drag(), + _ => {} + }, + _ => {} + } + // println!("Pressed: {:?}, mods: {:?}", self.pressed, self.mods); + } + + pub fn handle_key(&mut self, event: &KeyEvent) { + let PhysicalKey::Code(code) = event.physical_key else { + return; + }; + match (event.state, code) { + (ElementState::Pressed, KeyCode::ArrowUp) => { + self.handle_move(Direction::Up); + } + (ElementState::Pressed, KeyCode::ArrowDown) => { + self.handle_move(Direction::Down); + } + (ElementState::Pressed, KeyCode::ArrowLeft) => { + self.handle_move(Direction::Left); + } + (ElementState::Pressed, KeyCode::ArrowRight) => { + self.handle_move(Direction::Right); + } + (ElementState::Pressed, KeyCode::ShiftLeft) => { + self.set_mode(SelectionMode::InverseResize); + } + (ElementState::Released, KeyCode::ShiftLeft | KeyCode::ControlLeft) => { + self.set_mode(SelectionMode::Move); + } + (ElementState::Pressed, KeyCode::ControlLeft) => { + self.set_mode(SelectionMode::Resize); + } + _ => {} + }; + } + + pub fn start_drag(&mut self) { + if let Some(drag) = self.selection.drag.as_mut() { + if drag.x != 0. && drag.y != 0. { + return; + } + }; + let mouse_pos = self.mouse_position.as_vec2(); + self.selection.drag = Some(Rect { + x: mouse_pos.x, + y: mouse_pos.y, + w: 0.0, + h: 0.0, + }); + } + + pub fn end_drag(&mut self) { + self.selection.selection = self.selection.drag.take(); + } + + pub fn cancel_drag(&mut self) { + self.selection.drag = None; + self.selection.selection = None; + } + + pub fn handle_move(&mut self, dir: Direction) -> Option<()> { + let (width, height) = self.size?; + let selection = self.selection.selection.as_mut()?; + + let (dx, dy) = match dir { + Direction::Up => (0.0, -1.0), + Direction::Down => (0.0, 1.0), + Direction::Left => (-1.0, 0.0), + Direction::Right => (1.0, 0.0), + }; + let (x_delta, y_delta) = match self.mode { + SelectionMode::Move => (dx, dy), + SelectionMode::InverseResize => (dx, dy), + SelectionMode::Resize => (0.0, 0.0), + }; + + if matches!( + self.mode, + SelectionMode::Move | SelectionMode::InverseResize + ) { + selection.x = (selection.x + x_delta).clamp(0.0, width); + selection.y = (selection.y + y_delta).clamp(0.0, height); + } + + if matches!(self.mode, SelectionMode::Move | SelectionMode::Resize) { + selection.w = (selection.w + dx).clamp(0.0, width); + selection.h = (selection.h + dy).clamp(0.0, height); + } + + Some(()) + } + + pub fn size(&mut self, size: (f32, f32)) -> &mut Self { + self.size = Some(size); + self + } + + pub fn set_mode(&mut self, mode: SelectionMode) -> &mut Self { + self.mode = mode; + self + } +} diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..b258d83 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,237 @@ +use std::path::PathBuf; + +use cleave_daemon::HotKey; +use image::ImageFormat; +use wgpu::core::command::Rect; + +use crate::selection::modes::SelectionMode; + +fn parse_region(s: &str) -> Result, String> { + let coords: Vec = s + .split(',') + .map(|s| s.parse().map_err(|_| "Invalid region format")) + .collect::, _>>()?; + + if coords.len() != 4 { + return Err("Region must be in format: x,y,width,height".into()); + } + + Ok(Rect { + x: coords[0], + y: coords[1], + w: coords[2], + h: coords[3], + }) +} + +fn parse_format(s: &str) -> Result { + match s { + "bmp" => Ok(ImageFormat::Bmp), + "gif" => Ok(ImageFormat::Gif), + "ico" => Ok(ImageFormat::Ico), + "jpeg" => Ok(ImageFormat::Jpeg), + "png" => Ok(ImageFormat::Png), + "tiff" => Ok(ImageFormat::Tiff), + "webp" => Ok(ImageFormat::WebP), + _ => Err("Invalid image format".into()), + } +} + +fn parse_filter(s: &str) -> Result { + match s { + "Nearest" => Ok(image::imageops::FilterType::Nearest), + "Triangle" => Ok(image::imageops::FilterType::Triangle), + "CatmullRom" => Ok(image::imageops::FilterType::CatmullRom), + "Gaussian" => Ok(image::imageops::FilterType::Gaussian), + "Lanczos3" => Ok(image::imageops::FilterType::Lanczos3), + _ => Err("Invalid filter type".into()), + } +} + +/// Cleave - A GPU-accelerated screen capture tool +#[derive(clap::Parser, Debug)] +#[command( + name = "cleave", + author, + version, + about, + long_about = "A lightweight, GPU-accelerated screen capture tool that allows users to quickly select and copy portions of their screen" +)] +pub struct Args { + /// Output directory for the captured image + /// + /// If not provided, the capture is copied to the clipboard + #[arg(short, long)] + pub output_dir: Option, + /// Output format for the captured image + /// + /// Supported formats: bmp, gif, ico, jpeg, png, tiff, webp + /// + /// Only used when output_dir is provided + #[arg(long="format", value_parser=parse_format)] + pub image_format: Option, + /// Selection mode for the capture tool + #[arg(short, long, default_value = "move")] + pub mode: SelectionMode, + /// Monitor index to capture + /// + /// If not provided, the primary monitor is used + #[arg(long)] + pub monitor: Option, // If not provided, the primary monitor is used + /// Region to capture in the format: x,y,width,height + /// + /// If not provided, the entire screen is captured and the user is prompted to select a region + /// If provided, the user is not prompted and the region is captured immediately + #[arg(long, short='i', value_parser=parse_region)] + pub region: Option>, + /// Filename for the captured image + /// + /// If not provided, the image is saved with a timestamp: 'cleave-YYYY-MM-DD-HH-MM-SS' + /// Only used when output_dir is provided + #[arg(long, short = 'f')] + pub filename: Option, + /// Delay in milliseconds before capturing the screen + /// + /// If not provided, the screen is captured immediately + #[arg(long, short = 'd', default_value = "0")] + pub delay: u64, + /// List available monitors and exit + #[arg(long, short = 'l')] + pub monitor_list: bool, + // /// Path to the configuration file + // /// + // /// If not provided, the default configuration is used + // #[arg(long, short = 'c')] + // pub config_path: Option, + // TODO: Implement these features + // /// Optimize the captured image when applicable + // #[arg(long, short='p')] + // optimize: bool, + /// Scale the captured image by a factor + #[arg(long, short = 'r')] + pub scale: Option, + /// Filter to use when scaling the image + /// + /// Supported filters: Nearest, Triangle, CatmullRom, Gaussian, Lanczos3 + /// + /// Only used when scale is provided + #[arg(long, short = 'q', value_parser=parse_filter)] + pub filter: Option, + + /// Daemon Mode Hotkey + /// + /// If provided, app runs in the background and captures the screen whenever the user presses a hotkey + #[arg(long)] + pub daemon_hotkey: Option, + + /// Persistent Daemon Mode + /// + /// If true, the app will continue to run in the background even after the hotkey is pressed, + /// allowing the user to capture the screen multiple times + /// + /// Only used when daemon_hotkey is provided + #[arg(long, short)] + pub persistent: bool, + + /// Key Listen Sleep Duration + /// + /// If provided, the app will sleep for the specified duration in milliseconds before listening for the hotkey + /// + /// Only used when daemon_hotkey is provided + #[arg(long, short, default_value = "100")] + pub sleep: u64, +} + +impl Args { + pub fn verify(self) -> anyhow::Result { + if self.monitor_list + && (self.output_dir.is_some() + || self.image_format.is_some() + || self.filename.is_some() + || self.region.is_some() + || self.scale.is_some() + || self.daemon_hotkey.is_some()) + { + anyhow::bail!("Monitor list option cannot be used with other options"); + } + if let Some(scale) = self.scale { + if scale <= 0.0 { + anyhow::bail!("Scale factor must be greater than 0"); + } + } + if let Some(region) = self.region { + if region.w == 0. || region.h == 0. { + anyhow::bail!("Region width and height must be greater than 0"); + } + } + if (self.image_format.is_some() || self.filename.is_some()) && self.output_dir.is_none() { + anyhow::bail!( + "Output format and filename is only used when output directory is provided" + ); + } + if self.persistent && self.daemon_hotkey.is_none() { + anyhow::bail!("Persistent daemon mode can only be used with daemon hotkey"); + } + if self.daemon_hotkey.is_some() && self.delay > 0 { + anyhow::bail!("Delay cannot be used with daemon hotkey"); + } + if let Some(hotkey) = &self.daemon_hotkey { + if hotkey.is_empty() { + anyhow::bail!("Hotkey cannot be empty"); + } + } + + if let Some(hotkey) = self + .daemon_hotkey + .map(|s| s.parse::()) + .transpose()? + { + let mut daemon = std::process::Command::new("cleave-daemon"); + daemon.args(["--hotkey", &hotkey.to_string()]); + daemon.args(["--sleep", &self.sleep.to_string()]); + if self.persistent { + daemon.arg("--persistent"); + } + if let Err(e) = daemon.spawn() { + match e.kind() { + std::io::ErrorKind::NotFound => { + anyhow::bail!("Could not find cleave-daemon in PATH"); + } + _ => { + anyhow::bail!("Could not start cleave-daemon: {}", e); + } + } + }; + println!("Daemon started, press {} to capture the screen", hotkey); + std::process::exit(0); + } + + Ok(Verified { + output_dir: self.output_dir, + image_format: self.image_format, + mode: self.mode, + monitor: self.monitor, + region: self.region, + filename: self.filename, + delay: self.delay, + monitor_list: self.monitor_list, + config_path: None, + scale: self.scale, + filter: self.filter, + }) + } +} + +pub struct Verified { + pub output_dir: Option, + pub image_format: Option, + pub mode: SelectionMode, + pub monitor: Option, + pub region: Option>, + pub filename: Option, + pub delay: u64, + pub monitor_list: bool, + pub config_path: Option, + pub scale: Option, + pub filter: Option, +} diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index f303756..0000000 --- a/src/context.rs +++ /dev/null @@ -1,328 +0,0 @@ -use anyhow::Context; -use arboard::ImageData; -use glam::{DVec2, Vec2}; -use image::{GenericImageView, ImageBuffer, Rgba}; -// use pixels::{Pixels, SurfaceTexture}; -use winit::{ - dpi::PhysicalSize, - window::{Icon, Window, WindowAttributes}, -}; - -// use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; -use cleave_graphics::prelude::*; - -pub enum MoveMode { - Move, // Move the selection - InverseResize, // Make the selection smaller - Resize, // Make the selection larger -} - -pub enum Direction { - Up, - Down, - Left, - Right, -} - -#[repr(C)] -#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, Default, Debug)] -pub struct SelectionUniforms { - screen_size: Vec2, - drag_start: Vec2, - drag_end: Vec2, - selection_start: Vec2, - selection_end: Vec2, - time: f32, - is_dragging: u32, // 0 = None, 1 = Dragging, 2 = Selected, 3 = Both -} - -impl std::fmt::Display for SelectionUniforms { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "size: {:?}, is_dragging: {}, drag_start: {:?}, drag_end: {:?}, selection_start: {:?}, selection_end: {:?}, time: {}", - self.screen_size, self.is_dragging, self.drag_start, self.drag_end, self.selection_start, self.selection_end, self.time) - } -} - -#[derive(Clone, Copy, Debug)] -pub struct Drag { - start: Vec2, - end: Option, -} - -#[derive(Clone, Copy, Debug)] -pub struct Selection { - start: Vec2, - end: Vec2, -} - -pub struct UserSelection { - drag: Option, - selection: Option, -} - -impl UserSelection { - fn new() -> Self { - Self { - drag: None, - selection: None, - } - } - - fn sel_coords(&self) -> Option<((u32, u32), (u32, u32))> { - let selection = self.selection.as_ref()?; - let (start_x, start_y) = (selection.start.x, selection.start.y); - let (end_x, end_y) = (selection.end.x, selection.end.y); - - let (min_x, max_x) = (start_x.min(end_x).ceil(), start_x.max(end_x).floor()); - let (min_y, max_y) = (start_y.min(end_y).ceil(), start_y.max(end_y).floor()); - Some(((min_x as u32, min_y as u32), (max_x as u32, max_y as u32))) - } - - fn sel_dimensions(&self) -> Option<(f32, f32)> { - let selection = self.selection.as_ref()?; - let width = (selection.end.x - selection.start.x).abs(); - let height = (selection.end.y - selection.start.y).abs(); - Some((width, height)) - } - - // fn get_ -} - -pub struct AppContext { - size: PhysicalSize, - mouse_position: DVec2, - selection: UserSelection, - // current_drag: Option, - // selection: Option, - image: ImageBuffer, Vec>, - // pixels: Pixels<'static>, - total_time: f32, - last_frame: std::time::Instant, - graphics: Graphics, - bundle: GraphicsBundle, - mode: MoveMode, -} - -impl AppContext { - pub fn start_drag(&mut self) { - if let Some(drag) = self.selection.drag.as_mut() { - if drag.start != Vec2::ZERO { - return; - } - }; - self.selection.drag = Some(Drag { - start: self.mouse_position.as_vec2(), - end: Some(self.mouse_position.as_vec2()), - }); - } - - pub fn end_drag(&mut self) { - self.selection.selection = None; - if let Some(drag) = self.selection.drag.take() { - let end_pos = drag.end.unwrap_or(drag.start); // Use end if set, otherwise use start - self.selection.selection = Some(Selection { - start: drag.start, - end: end_pos, - }); - } - } - - pub fn cancel_drag(&mut self) { - self.selection.drag = None; - self.selection.selection = None; - } - - fn get_selection_data(&self) -> Option> { - let ((min_x, min_y), (max_x, max_y)) = self.selection.sel_coords()?; - let img = self - .image - .view(min_x, min_y, max_x.abs_diff(min_x), max_y.abs_diff(min_y)); - let image_data = img.to_image().to_vec(); - Some(image_data) - } - - pub fn save_selection_to_clipboard(&self) { - let (width, height) = self.selection.sel_dimensions().unwrap(); - - let width = width.floor() as usize; - let height = height.floor() as usize; - - let image_data = self.get_selection_data().unwrap(); - - let mut clipboard = arboard::Clipboard::new().unwrap(); - if width * height != image_data.len() / 4 { - eprintln!( - "Invalid selection size {:?} (w h p)", - (width, height, image_data.len() / 4) - ); - return; - } - let image_data = ImageData { - width, - height, - bytes: std::borrow::Cow::Owned(image_data), - }; - let _ = clipboard.set_image(image_data); - } - - pub fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> anyhow::Result { - let monitor = xcap::Monitor::all()? - .into_iter() - .find(|m| m.is_primary()) - .with_context(|| "Could not get primary monitor")?; - let img = monitor.capture_image()?; - let size = PhysicalSize::new(monitor.width(), monitor.height()); - - let icon_bytes = include_bytes!("../icon.png"); - let rgba = image::load_from_memory(icon_bytes)?.to_rgba8(); - let (width, height) = rgba.dimensions(); - let rgba = rgba.into_raw(); - - let window = event_loop.create_window( - WindowAttributes::default() - .with_inner_size(size) - .with_title("Cleave") - .with_resizable(false) - .with_decorations(false) - .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) - .with_visible(false) - .with_window_icon(Some(Icon::from_rgba(rgba, width, height)?)), - )?; - - let graphics = Graphics::new(window, size.width, size.height); - let graphics = pollster::block_on(graphics)?; - - let bundle = GraphicsBundle::new( - img.clone().into(), - &graphics.device, - &graphics.queue, - wgpu::PrimitiveTopology::TriangleStrip, - graphics.config.format, - ); - - graphics.window.set_visible(true); - let _ = graphics - .window - .set_cursor_grab(winit::window::CursorGrabMode::Confined); - - // let surface_texture = SurfaceTexture::new(size.width, size.height, window.clone()); - // let pixels = Pixels::new(size.width, size.height, surface_texture)?; - - Ok(Self { - size, - image: img, - bundle, - total_time: 0.0, - last_frame: std::time::Instant::now(), - selection: UserSelection::new(), - // window, - graphics, - mouse_position: DVec2::new(0.0, 0.0), - mode: MoveMode::Resize, - }) - } - - pub fn handle_move(&mut self, dir: Direction) -> Option<()> { - let (dx, dy) = match dir { - Direction::Up => (0.0, -1.0), - Direction::Down => (0.0, 1.0), - Direction::Left => (-1.0, 0.0), - Direction::Right => (1.0, 0.0), - }; - - let selection = self.selection.selection.as_mut()?; - - match self.mode { - MoveMode::Move => { - selection.start.x = (selection.start.x + dx).clamp(0.0, self.size.width as f32); - selection.start.y = (selection.start.y + dy).clamp(0.0, self.size.height as f32); - selection.end.x = (selection.end.x + dx).clamp(0.0, self.size.width as f32); - selection.end.y = (selection.end.y + dy).clamp(0.0, self.size.height as f32); - } - MoveMode::Resize => { - selection.end.x = (selection.end.x + dx).clamp(0.0, self.size.width as f32); - selection.end.y = (selection.end.y + dy).clamp(0.0, self.size.height as f32); - } - MoveMode::InverseResize => { - selection.start.x = (selection.start.x + dx).clamp(0.0, self.size.width as f32); - selection.start.y = (selection.start.y + dy).clamp(0.0, self.size.height as f32); - } - } - - Some(()) - } - - pub fn draw(&mut self) { - let time = self.last_frame.elapsed().as_secs_f32(); - self.total_time += time; - self.last_frame = std::time::Instant::now(); - - self.update_uniforms(); - self.bundle.update_buffer(&self.graphics.queue); - - let mut pass = match self.graphics.render() { - Ok(pass) => pass, - Err(err) => { - eprintln!("Error rendering frame: {:?}", err); - return; - } - }; - self.bundle.draw(&mut pass); - pass.finish(); - self.graphics.request_redraw(); - } - - fn update_uniforms(&mut self) { - self.bundle.uniforms.time = self.total_time; - self.bundle.uniforms.screen_size.x = self.size.width as f32; - self.bundle.uniforms.screen_size.y = self.size.height as f32; - - let drag = self.selection.drag; - let selection = self.selection.selection; - self.bundle.uniforms.is_dragging = match (drag, selection) { - (Some(d), Some(s)) if d.start != Vec2::ZERO || s.start != Vec2::ZERO => 3, - (Some(d), None) if d.start != Vec2::ZERO => 1, - (None, Some(s)) if s.start != Vec2::ZERO => 2, - _ => 0, - }; - - if let Some(drag) = drag { - self.bundle.uniforms.drag_start = drag.start; - self.bundle.uniforms.drag_end = drag.end.unwrap_or_default(); - } else { - self.bundle.uniforms.drag_start = Vec2::ZERO; - self.bundle.uniforms.drag_end = Vec2::ZERO; - }; - - if let Some(selection) = selection { - self.bundle.uniforms.selection_start = selection.start; - self.bundle.uniforms.selection_end = selection.end; - } else { - self.bundle.uniforms.selection_start = Vec2::ZERO; - self.bundle.uniforms.selection_end = Vec2::ZERO; - }; - } - - pub fn window_id(&self) -> winit::window::WindowId { - self.graphics.id() - } - - pub fn destroy(&self) { - self.graphics.window.set_minimized(true); - } - - pub fn hide_window(&self) { - self.graphics.set_visible(false); - } - - pub fn set_mode(&mut self, mode: MoveMode) { - self.mode = mode - } - - pub fn update_mouse_position(&mut self, x: f64, y: f64) { - self.mouse_position = DVec2::new(x, y); - if let Some(drag) = self.selection.drag.as_mut() { - drag.end = Some(self.mouse_position.as_vec2()); - } - } -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..716a12b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod app; +pub mod args; +pub mod selection; +pub mod util; diff --git a/src/main.rs b/src/main.rs index 04dca50..58b6ba1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,160 +1,15 @@ -#![windows_subsystem = "windows"] +// TODO When opened outside of a terminal, the program should not open the terminal. +// However, when opened inside a terminal, the program should be able to output to the terminal. +// #![windows_subsystem = "windows"] -use winit::{ - application::ApplicationHandler, - event::{ElementState, KeyEvent, MouseButton, WindowEvent}, - keyboard::{Key, NamedKey}, -}; - -mod context; -use context::{AppContext, Direction, MoveMode}; - -pub struct Drag { - start: (f64, f64), - end: Option<(f64, f64)>, -} - -impl Drag { - fn coords(&self) -> Option<((u32, u32), (u32, u32))> { - let end = self.end?; - let (start_x, start_y) = (self.start.0 as u32, self.start.1 as u32); - let (end_x, end_y) = (end.0 as u32, end.1 as u32); - - let (min_x, max_x) = (start_x.min(end_x), start_x.max(end_x)); - let (min_y, max_y) = (start_y.min(end_y), start_y.max(end_y)); - Some(((min_x, min_y), (max_x, max_y))) - } -} - -pub struct Selection { - start: (f64, f64), - end: (f64, f64), -} - -impl Selection { - fn dimensions(&self) -> (f64, f64) { - let width = (self.end.0 - self.start.0).abs(); - let height = (self.end.1 - self.start.1).abs(); - (width, height) - } - - fn area(&self) -> f64 { - let (width, height) = self.dimensions(); - width * height - } - - // fn aspect_ratio(&self) -> f64 { - // let (width, height) = self.dimensions(); - // width / height - // } - - // fn center(&self) -> (f64, f64) { - // let x = (self.start.0 + self.end.0) / 2.0; - // let y = (self.start.1 + self.end.1) / 2.0; - // (x, y) - // } - - fn coords(&self) -> ((u32, u32), (u32, u32)) { - let (start_x, start_y) = (self.start.0, self.start.1); - let (end_x, end_y) = (self.end.0, self.end.1); - - let (min_x, max_x) = (start_x.min(end_x).ceil(), start_x.max(end_x).floor()); - let (min_y, max_y) = (start_y.min(end_y).ceil(), start_y.max(end_y).floor()); - ((min_x as u32, min_y as u32), (max_x as u32, max_y as u32)) - } -} - -struct App { - context: Option, -} - -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let context = AppContext::new(event_loop).expect("Could not start context"); - self.context = Some(context); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - let Some(context) = &mut self.context else { - return; - }; - if id != context.window_id() { - return; - } - - match event { - WindowEvent::RedrawRequested => { - context.draw(); - } - WindowEvent::CursorMoved { position, .. } => { - context.update_mouse_position(position.x, position.y); - } - WindowEvent::KeyboardInput { - event: - KeyEvent { - state, - logical_key: key, - .. - }, - .. - } => match (state, key) { - (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { - event_loop.exit(); - context.destroy(); - } - (ElementState::Pressed, Key::Named(NamedKey::Space)) => { - context.hide_window(); - context.save_selection_to_clipboard(); - event_loop.exit(); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { - context.handle_move(Direction::Down); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowUp)) => { - context.handle_move(Direction::Up); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowLeft)) => { - context.handle_move(Direction::Left); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowRight)) => { - context.handle_move(Direction::Right); - } - (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { - context.set_mode(MoveMode::InverseResize); - } - (ElementState::Released, Key::Named(NamedKey::Shift)) => { - context.set_mode(MoveMode::Resize); - } - (ElementState::Pressed, Key::Named(NamedKey::Control)) => { - context.set_mode(MoveMode::Move); - } - (ElementState::Released, Key::Named(NamedKey::Control)) => { - context.set_mode(MoveMode::Resize); - } - _ => {} - }, - WindowEvent::MouseInput { state, button, .. } => match (state, button) { - (ElementState::Pressed, MouseButton::Left) => context.start_drag(), - (ElementState::Released, MouseButton::Left) => context.end_drag(), - (_, MouseButton::Right) => context.cancel_drag(), - _ => {} - }, - WindowEvent::CloseRequested => { - event_loop.exit(); - } - _ => {} - } - } -} +use clap::Parser; +use cleave::args::Args; +use std::io::IsTerminal; fn main() -> anyhow::Result<()> { - let mut app = App { context: None }; - let event_loop = winit::event_loop::EventLoop::new()?; - event_loop.run_app(&mut app)?; + let stdout = std::io::stdin(); + let args = stdout.is_terminal().then(Args::parse); + let app = cleave::app::App::new(args)?; + app.run()?; Ok(()) } diff --git a/src/selection/mod.rs b/src/selection/mod.rs new file mode 100644 index 0000000..c70c63e --- /dev/null +++ b/src/selection/mod.rs @@ -0,0 +1,8 @@ +use wgpu::core::command::Rect; + +pub mod modes; +#[derive(Debug, Clone, Default)] +pub struct UserSelection { + pub drag: Option>, + pub selection: Option>, +} diff --git a/src/selection/modes.rs b/src/selection/modes.rs new file mode 100644 index 0000000..184b452 --- /dev/null +++ b/src/selection/modes.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)] +pub enum SelectionMode { + #[default] + Move, // Move the selection + InverseResize, // Make the selection smaller + Resize, // Make the selection larger +} + +pub enum Direction { + Up, + Down, + Left, + Right, +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..c8e6d44 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,122 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Context; +use arboard::ImageData; +use image::{imageops::FilterType, GenericImageView, ImageFormat, RgbaImage}; +use wgpu::core::command::Rect; + +use crate::args::Verified; + +// pub(crate) fn crop_and_save( +// img: &RgbaImage, +// args: Option<&Verified>, +// rect: Rect, +// output: impl AsRef, +// ) -> anyhow::Result<()> { +// let img = crop_image(img, args, rect)?; +// save_selection(img, args, output) +// } + +pub(crate) fn crop_image( + img: &RgbaImage, + args: Option<&Verified>, + selection: Rect, +) -> anyhow::Result { + let rect = args.and_then(|a| a.region).unwrap_or(selection); + // Round this to be smaller rather than larger + let rect = Rect { + x: rect.x.ceil() as u32, + y: rect.y.ceil() as u32, + w: rect.w.floor() as u32, + h: rect.h.floor() as u32, + }; + let img = img.view(rect.x, rect.y, rect.w, rect.h); + Ok(img.to_image()) +} + +pub(crate) fn save_selection( + mut image: RgbaImage, + args: Option<&Verified>, + save_path: impl AsRef, +) -> anyhow::Result<()> { + // Handle scaling if requested + if let Some(scale) = args.and_then(|a| a.scale) { + image = resize_image( + &image, + scale, + args.and_then(|a| a.filter).unwrap_or(FilterType::Nearest), + )?; + } + + // Generate filename and save + let format = args + .and_then(|f| f.image_format) + .unwrap_or(ImageFormat::Png); + let path = generate_output_path( + save_path, + args.and_then(|f| f.filename.as_deref()).unwrap_or("cleave"), + format, + ); + + Ok(image.save_with_format(path, format)?) +} + +pub(crate) fn resize_image( + image: &RgbaImage, + scale: f32, + filter: FilterType, +) -> Result { + let new_width = (image.width() as f32 * scale).round() as u32; + let new_height = (image.height() as f32 * scale).round() as u32; + Ok(image::imageops::resize( + image, new_width, new_height, filter, + )) +} + +pub(crate) fn generate_output_path( + dir: impl AsRef, + filename: &str, + format: ImageFormat, +) -> PathBuf { + let ext = format.extensions_str().first().copied().unwrap_or("png"); + + let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); + let filename = format!("{filename}-{}.{ext}", timestamp); + + dir.as_ref().join(filename) +} + +pub(crate) fn save_to_clipboard(image_data: &RgbaImage) -> Result<(), arboard::Error> { + let mut clipboard = arboard::Clipboard::new()?; + let image_data = ImageData { + width: image_data.width() as usize, + height: image_data.height() as usize, + bytes: std::borrow::Cow::Borrowed(image_data.as_raw()), + }; + if let Err(e) = clipboard.set_image(image_data) { + eprintln!("Error setting image to clipboard: {:?}", e); + }; + Ok(()) +} + +pub(crate) fn load_icon() -> Result<(u32, u32, Vec), anyhow::Error> { + let icon_bytes = include_bytes!("../../icon.png"); + let rgba = image::load_from_memory(icon_bytes)?.to_rgba8(); + let (width, height) = rgba.dimensions(); + let rgba = rgba.into_raw(); + Ok((width, height, rgba)) +} + +pub(crate) fn capture_screen(monitor_id: Option) -> anyhow::Result { + get_monitor(monitor_id).and_then(|e| Ok(e.capture_image()?)) +} + +pub(crate) fn get_monitor(monitor_id: Option) -> anyhow::Result { + let mut monitors = xcap::Monitor::all()?; + let monitor = monitors + .iter() + .position(|m| monitor_id.map_or(m.is_primary(), |id| m.id() == id)) + .or_else(|| monitors.iter().position(|m| m.is_primary())) + .with_context(|| "Could not select monitor")?; + Ok(monitors.swap_remove(monitor)) +}