diff --git a/native/Cargo.lock b/native/Cargo.lock index a832afca6..3c0803f89 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +17,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.11.0" @@ -28,12 +84,33 @@ dependencies = [ "serde", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -43,6 +120,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -68,6 +154,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "ctor" version = "0.2.9" @@ -92,6 +184,16 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "either" version = "1.15.0" @@ -116,6 +218,88 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + [[package]] name = "globset" version = "0.4.18" @@ -170,14 +354,17 @@ dependencies = [ name = "gsd-engine" version = "0.1.0" dependencies = [ + "arboard", "dashmap", "globset", "gsd-grep", "ignore", + "image", "libc", "napi", "napi-build", "napi-derive", + "syntect", ] [[package]] @@ -191,6 +378,17 @@ dependencies = [ "rayon", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -213,6 +411,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + [[package]] name = "libc" version = "0.2.183" @@ -229,6 +441,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.4.14" @@ -259,6 +477,26 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "napi" version = "2.16.17" @@ -316,12 +554,104 @@ dependencies = [ "libloading", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.12" @@ -335,6 +665,25 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -344,6 +693,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.45" @@ -411,6 +772,19 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "same-file" version = "1.0.6" @@ -461,6 +835,12 @@ dependencies = [ "syn", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "smallvec" version = "1.15.1" @@ -478,6 +858,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "regex-syntax", + "serde", + "serde_derive", + "thiserror", + "walkdir", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -500,13 +932,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -515,6 +953,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -523,3 +970,120 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/native/crates/engine/Cargo.toml b/native/crates/engine/Cargo.toml index 2babd9848..992a5811e 100644 --- a/native/crates/engine/Cargo.toml +++ b/native/crates/engine/Cargo.toml @@ -12,9 +12,11 @@ crate-type = ["cdylib"] [dependencies] gsd-grep = { path = "../grep" } +arboard = "3" dashmap = "6" globset = "0.4" ignore = "0.4" +image = { version = "0.25", default-features = false, features = ["png"] } napi = { version = "2", features = ["napi8"] } napi-derive = "2" syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "regex-fancy"] } diff --git a/native/crates/engine/src/clipboard.rs b/native/crates/engine/src/clipboard.rs new file mode 100644 index 000000000..cc376c024 --- /dev/null +++ b/native/crates/engine/src/clipboard.rs @@ -0,0 +1,110 @@ +//! Clipboard utilities backed by arboard. +//! +//! Provides text copy/read and image read support across Linux, macOS, and Windows. +//! Text copy runs synchronously so macOS writes execute on the caller thread, +//! avoiding worker-thread `AppKit` pasteboard warnings in CLI contexts. + +use std::io::Cursor; + +use arboard::{Clipboard, Error as ClipboardError, ImageData}; +use image::{DynamicImage, ImageFormat, RgbaImage}; +use napi::bindgen_prelude::*; +use napi::{Env, Error, Result, Task}; +use napi_derive::napi; + +/// Clipboard image payload encoded as PNG bytes. +#[napi(object)] +pub struct ClipboardImage { + /// PNG-encoded image bytes. + pub data: Uint8Array, + #[napi(js_name = "mimeType")] + /// MIME type for the encoded image payload. + pub mime_type: String, +} + +fn encode_png(image: ImageData<'_>) -> Result> { + let width = u32::try_from(image.width) + .map_err(|_| Error::from_reason("Clipboard image width overflow"))?; + let height = u32::try_from(image.height) + .map_err(|_| Error::from_reason("Clipboard image height overflow"))?; + let bytes = image.bytes.into_owned(); + let buffer = RgbaImage::from_raw(width, height, bytes) + .ok_or_else(|| Error::from_reason("Clipboard image buffer size mismatch"))?; + let capacity = width.saturating_mul(height).saturating_mul(4) as usize; + let mut output = Vec::with_capacity(capacity); + DynamicImage::ImageRgba8(buffer) + .write_to(&mut Cursor::new(&mut output), ImageFormat::Png) + .map_err(|err| Error::from_reason(format!("Failed to encode clipboard image: {err}")))?; + Ok(output) +} + +/// Copy plain text to the system clipboard. +/// +/// Runs synchronously to avoid macOS AppKit pasteboard warnings +/// when writing from worker threads. +#[napi(js_name = "copyToClipboard")] +pub fn copy_to_clipboard(text: String) -> Result<()> { + let mut clipboard = Clipboard::new() + .map_err(|err| Error::from_reason(format!("Failed to access clipboard: {err}")))?; + clipboard + .set_text(text) + .map_err(|err| Error::from_reason(format!("Failed to copy to clipboard: {err}")))?; + Ok(()) +} + +/// Read plain text from the system clipboard. +/// +/// Returns `None` when no text data is available. +#[napi(js_name = "readTextFromClipboard")] +pub fn read_text_from_clipboard() -> Result> { + let mut clipboard = Clipboard::new() + .map_err(|err| Error::from_reason(format!("Failed to access clipboard: {err}")))?; + match clipboard.get_text() { + Ok(text) => Ok(Some(text)), + Err(ClipboardError::ContentNotAvailable) => Ok(None), + Err(err) => Err(Error::from_reason(format!( + "Failed to read clipboard text: {err}" + ))), + } +} + +// ── Async image read task ──────────────────────────────────────────── + +pub(crate) struct ReadImageTask; + +impl Task for ReadImageTask { + type JsValue = Option; + type Output = Option; + + fn compute(&mut self) -> Result { + let mut clipboard = Clipboard::new() + .map_err(|err| Error::from_reason(format!("Failed to access clipboard: {err}")))?; + match clipboard.get_image() { + Ok(image) => { + let bytes = encode_png(image)?; + Ok(Some(ClipboardImage { + data: Uint8Array::from(bytes), + mime_type: "image/png".to_string(), + })) + } + Err(ClipboardError::ContentNotAvailable) => Ok(None), + Err(err) => Err(Error::from_reason(format!( + "Failed to read clipboard image: {err}" + ))), + } + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Read an image from the system clipboard. +/// +/// Returns a Promise that resolves to a `ClipboardImage` (PNG-encoded bytes) +/// or `null` when no image data is available. Runs on libuv's thread pool +/// to avoid blocking the main JS thread during PNG encoding. +#[napi(js_name = "readImageFromClipboard")] +pub fn read_image_from_clipboard() -> AsyncTask { + AsyncTask::new(ReadImageTask) +} diff --git a/native/crates/engine/src/lib.rs b/native/crates/engine/src/lib.rs index c27156def..c37ad583b 100644 --- a/native/crates/engine/src/lib.rs +++ b/native/crates/engine/src/lib.rs @@ -8,6 +8,7 @@ #![allow(clippy::needless_pass_by_value)] +mod clipboard; mod fs_cache; mod glob; mod glob_util; diff --git a/packages/native/package.json b/packages/native/package.json index 404fe52ec..2542389c1 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,14 +1,14 @@ { "name": "@gsd/native", "version": "0.1.0", - "description": "Native Rust bindings for GSD — high-performance grep, glob, and process management via N-API", + "description": "Native Rust bindings for GSD — high-performance native modules via N-API", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { "build:native": "node ../../native/scripts/build.js", "build:native:dev": "node ../../native/scripts/build.js --dev", - "test": "node --test src/__tests__/grep.test.mjs src/__tests__/ps.test.mjs src/__tests__/glob.test.mjs" + "test": "node --test src/__tests__/grep.test.mjs src/__tests__/ps.test.mjs src/__tests__/glob.test.mjs src/__tests__/clipboard.test.mjs" }, "exports": { ".": { @@ -26,6 +26,10 @@ "./glob": { "types": "./src/glob/index.ts", "import": "./src/glob/index.ts" + }, + "./clipboard": { + "types": "./src/clipboard/index.ts", + "import": "./src/clipboard/index.ts" } }, "files": [ diff --git a/packages/native/src/__tests__/clipboard.test.mjs b/packages/native/src/__tests__/clipboard.test.mjs new file mode 100644 index 000000000..407b6bdc9 --- /dev/null +++ b/packages/native/src/__tests__/clipboard.test.mjs @@ -0,0 +1,79 @@ +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); + +const addonDir = path.resolve(__dirname, "..", "..", "..", "..", "native", "addon"); +const platformTag = `${process.platform}-${process.arch}`; +const candidates = [ + path.join(addonDir, `gsd_engine.${platformTag}.node`), + path.join(addonDir, "gsd_engine.dev.node"), +]; + +let native; +for (const candidate of candidates) { + try { + native = require(candidate); + break; + } catch { + // try next + } +} + +if (!native) { + console.error("Native addon not found. Run build:native first."); + process.exit(1); +} + +describe("native clipboard: copyToClipboard()", () => { + test("copies text without throwing", () => { + assert.doesNotThrow(() => { + native.copyToClipboard("GSD clipboard test"); + }); + }); + + test("accepts empty string", () => { + assert.doesNotThrow(() => { + native.copyToClipboard(""); + }); + }); + + test("accepts unicode text", () => { + assert.doesNotThrow(() => { + native.copyToClipboard("Hello 世界"); + }); + }); +}); + +describe("native clipboard: readTextFromClipboard()", () => { + test("reads back text that was copied", () => { + const testText = `GSD clipboard roundtrip ${Date.now()}`; + native.copyToClipboard(testText); + const result = native.readTextFromClipboard(); + assert.equal(result, testText); + }); + + test("returns a string or null", () => { + const result = native.readTextFromClipboard(); + assert.ok(result === null || typeof result === "string"); + }); +}); + +describe("native clipboard: readImageFromClipboard()", () => { + test("returns a promise", () => { + const result = native.readImageFromClipboard(); + assert.ok(result instanceof Promise); + }); + + test("resolves to ClipboardImage or null", async () => { + const result = await native.readImageFromClipboard(); + if (result !== null) { + assert.ok(result.data instanceof Uint8Array, "data should be Uint8Array"); + assert.equal(result.mimeType, "image/png"); + } + }); +}); diff --git a/packages/native/src/clipboard/index.ts b/packages/native/src/clipboard/index.ts new file mode 100644 index 000000000..a363942af --- /dev/null +++ b/packages/native/src/clipboard/index.ts @@ -0,0 +1,40 @@ +/** + * Native clipboard access using N-API. + * + * Cross-platform clipboard read/write backed by the `arboard` Rust crate. + * No external tools (pbcopy, xclip, etc.) required. + */ + +import { native } from "../native.js"; +import type { ClipboardImage } from "./types.js"; + +export type { ClipboardImage }; + +/** + * Copy plain text to the system clipboard. + * + * Runs synchronously to avoid macOS AppKit pasteboard warnings + * when writing from worker threads. + */ +export function copyToClipboard(text: string): void { + native.copyToClipboard(text); +} + +/** + * Read plain text from the system clipboard. + * + * Returns `null` when no text data is available. + */ +export function readTextFromClipboard(): string | null { + return native.readTextFromClipboard() as string | null; +} + +/** + * Read an image from the system clipboard. + * + * Returns a Promise that resolves to a `ClipboardImage` (PNG-encoded bytes) + * or `null` when no image data is available. + */ +export function readImageFromClipboard(): Promise { + return native.readImageFromClipboard() as Promise; +} diff --git a/packages/native/src/clipboard/types.ts b/packages/native/src/clipboard/types.ts new file mode 100644 index 000000000..0ca3f508e --- /dev/null +++ b/packages/native/src/clipboard/types.ts @@ -0,0 +1,7 @@ +/** Clipboard image payload encoded as PNG bytes. */ +export interface ClipboardImage { + /** PNG-encoded image bytes. */ + data: Uint8Array; + /** MIME type for the encoded image payload (always "image/png"). */ + mimeType: string; +} diff --git a/packages/native/src/index.ts b/packages/native/src/index.ts index e823b12a0..ed92e3a92 100644 --- a/packages/native/src/index.ts +++ b/packages/native/src/index.ts @@ -2,12 +2,20 @@ * @gsd/native — High-performance Rust modules exposed via N-API. * * Modules: + * - clipboard: native clipboard access (text + image) * - grep: ripgrep-backed regex search (content + filesystem) * - ps: cross-platform process tree management * - glob: gitignore-respecting filesystem discovery with scan caching * - highlight: syntect-based syntax highlighting */ +export { + copyToClipboard, + readTextFromClipboard, + readImageFromClipboard, +} from "./clipboard/index.js"; +export type { ClipboardImage } from "./clipboard/index.js"; + export { highlightCode, supportsLanguage, diff --git a/packages/native/src/native.ts b/packages/native/src/native.ts index 5195070da..6d6958927 100644 --- a/packages/native/src/native.ts +++ b/packages/native/src/native.ts @@ -55,4 +55,7 @@ export const native = loadNative() as { highlightCode: (code: string, lang: string | null, colors: unknown) => unknown; supportsLanguage: (lang: string) => unknown; getSupportedLanguages: () => unknown; + copyToClipboard: (text: string) => void; + readTextFromClipboard: () => string | null; + readImageFromClipboard: () => Promise; };