diff --git a/Cargo.lock b/Cargo.lock index 62d4f6e..ec12c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,2247 +1,46 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "adler32" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" - -[[package]] -name = "aho-corasick" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "arc-swap" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" - -[[package]] -name = "arrayref" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" - -[[package]] -name = "arrayvec" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba" -dependencies = [ - "nodrop", -] - -[[package]] -name = "atty" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" -dependencies = [ - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "autocfg" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" - -[[package]] -name = "backtrace" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5180c5a20655b14a819b652fd2378fa5f1697b6c9ddad3e695c2f9cedf6df4e2" -dependencies = [ - "backtrace-sys", - "cfg-if", - "libc", - "rustc-demangle", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -dependencies = [ - "byteorder", -] - -[[package]] -name = "bitflags" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" - -[[package]] -name = "blake2b_simd" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf775a81bb2d464e20ff170ac20316c7b08a43d11dbc72f0f82e8e8d3d6d0499" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "byteorder" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" - -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "either", - "iovec", -] - -[[package]] -name = "bzip2" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6584aa36f5ad4c9247f5323b0a42f37802b37a836f0ad87084d7a33961abe25f" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "c2-chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" -dependencies = [ - "lazy_static", - "ppv-lite86", -] - [[package]] name = "cc" -version = "1.0.45" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc9a35e1f4290eb9e5fc54ba6cf40671ed2a2514c3eeb2b2a908dda2ea5a1be" [[package]] name = "cfg-if" -version = "0.1.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" - -[[package]] -name = "chrono" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "time", -] - -[[package]] -name = "clap" -version = "2.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "clicolors-control" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" -dependencies = [ - "atty", - "lazy_static", - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - -[[package]] -name = "cmake" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" -dependencies = [ - "cc", -] - -[[package]] -name = "console" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62828f51cfa18f8c31d3d55a43c6ce6af3f87f754cba9fbba7ff38089b9f5612" -dependencies = [ - "clicolors-control", - "encode_unicode", - "lazy_static", - "libc", - "regex", - "termios", - "unicode-width", - "winapi 0.3.9", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" - -[[package]] -name = "cookie" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" -dependencies = [ - "time", - "url 1.7.2", -] - -[[package]] -name = "cookie_store" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" -dependencies = [ - "cookie", - "failure", - "idna 0.1.5", - "log", - "publicsuffix", - "serde", - "serde_json", - "time", - "try_from", - "url 1.7.2", -] - -[[package]] -name = "core-foundation" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" - -[[package]] -name = "crc32fast" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" -dependencies = [ - "arrayvec", - "cfg-if", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard 1.0.0", -] - -[[package]] -name = "crossbeam-queue" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" -dependencies = [ - "cfg-if", - "lazy_static", -] - -[[package]] -name = "dialoguer" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "116f66c4e7b19af0d52857aa4ff710cc3b4781d9c16616e31540bc55ec57ba8c" -dependencies = [ - "console", - "lazy_static", - "tempfile", -] - -[[package]] -name = "dirs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" -dependencies = [ - "cfg-if", - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" -dependencies = [ - "cfg-if", - "libc", - "redox_users", - "winapi 0.3.9", -] - -[[package]] -name = "dtoa" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" - -[[package]] -name = "either" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "encoding_rs" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87240518927716f79692c2ed85bfe6e98196d18c6401ec75355760233a7e12e9" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "error-chain" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" -dependencies = [ - "backtrace", - "version_check", -] [[package]] name = "espanso" -version = "0.7.3" +version = "1.0.0" dependencies = [ - "backtrace", - "chrono", - "clap", - "cmake", - "dialoguer", - "dirs", - "fs2", - "html2text", - "lazy_static", - "libc", - "log", - "log-panics", - "markdown", - "named_pipe", - "notify", - "rand 0.7.2", - "regex", - "reqwest", - "serde", - "serde_json", - "serde_yaml", - "signal-hook", - "simplelog", - "tempfile", - "walkdir", - "widestring", - "winapi 0.3.9", - "zip", + "espanso-detect 0.1.0", ] [[package]] -name = "failure" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", - "synstructure", -] - -[[package]] -name = "filetime" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59efc38004c988e4201d11d263b8171f49a2e7ec0bdbb71773433f271504a5e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "winapi 0.3.9", -] - -[[package]] -name = "flate2" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adaffba6388640136149e18ed080b77a78611c1e1d6de75aedcdf78df5d4682" -dependencies = [ - "crc32fast", - "libc", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "fsevent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" -dependencies = [ - "bitflags", - "fsevent-sys", -] - -[[package]] -name = "fsevent-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" -dependencies = [ - "libc", -] - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - -[[package]] -name = "futf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" - -[[package]] -name = "futures-cpupool" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" -dependencies = [ - "futures", - "num_cpus", -] - -[[package]] -name = "getrandom" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "h2" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" -dependencies = [ - "byteorder", - "bytes", - "fnv", - "futures", - "http", - "indexmap", - "log", - "slab", - "string", - "tokio-io", -] - -[[package]] -name = "html2text" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26379dcb715e237b96102a12b505c553e2bffa74bae2e54658748d298660ef1" -dependencies = [ - "html5ever", - "markup5ever_rcdom", - "unicode-width", -] - -[[package]] -name = "html5ever" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2 1.0.24", - "quote 1.0.2", - "syn 1.0.53", -] - -[[package]] -name = "http" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" +name = "espanso-detect" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" dependencies = [ - "bytes", - "futures", - "http", - "tokio-buf", -] - -[[package]] -name = "httparse" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" - -[[package]] -name = "hyper" -version = "0.12.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6" -dependencies = [ - "bytes", - "futures", - "futures-cpupool", - "h2", - "http", - "http-body", - "httparse", - "iovec", - "itoa", - "log", - "net2", - "rustc_version", - "time", - "tokio", - "tokio-buf", - "tokio-executor", - "tokio-io", - "tokio-reactor", - "tokio-tcp", - "tokio-threadpool", - "tokio-timer", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" -dependencies = [ - "bytes", - "futures", - "hyper", - "native-tls", - "tokio-io", -] - -[[package]] -name = "idna" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" - -[[package]] -name = "inotify" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" -dependencies = [ - "libc", -] - -[[package]] -name = "iovec" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" -dependencies = [ - "libc", - "winapi 0.2.8", -] - -[[package]] -name = "itoa" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" - -[[package]] -name = "libc" -version = "0.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" - -[[package]] -name = "linked-hash-map" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" - -[[package]] -name = "lock_api" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" -dependencies = [ - "owning_ref", - "scopeguard 0.3.3", + "cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", + "widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "log" -version = "0.4.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" dependencies = [ - "cfg-if", + "cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "log-panics" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef" -dependencies = [ - "backtrace", - "log", -] - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "markdown" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef3aab6a1d529b112695f72beec5ee80e729cb45af58663ec902c8fac764ecdd" -dependencies = [ - "lazy_static", - "pipeline", - "regex", -] - -[[package]] -name = "markup5ever" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab" -dependencies = [ - "log", - "phf", - "phf_codegen", - "serde", - "serde_derive", - "serde_json", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "markup5ever_rcdom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" -dependencies = [ - "html5ever", - "markup5ever", - "tendril", - "xml5ever", -] - -[[package]] -name = "matches" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" - -[[package]] -name = "memchr" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" - -[[package]] -name = "memoffset" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "mime" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1d63acd1b78403cc0c325605908475dd9b9a3acbf65ed8bcab97e27014afcf" - -[[package]] -name = "mime_guess" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7108aff85b876d06f22503dcce091e29f76733b2bfdd91eebce81f5e68203a10" -dependencies = [ - "adler32", -] - -[[package]] -name = "mio" -version = "0.6.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" -dependencies = [ - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow", - "net2", - "slab", - "winapi 0.2.8", -] - -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log", - "mio", - "slab", -] - -[[package]] -name = "miow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", -] - -[[package]] -name = "named_pipe" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "native-tls" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "net2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" -dependencies = [ - "cfg-if", - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - -[[package]] -name = "nodrop" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" - -[[package]] -name = "notify" -version = "4.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" -dependencies = [ - "bitflags", - "filetime", - "fsevent", - "fsevent-sys", - "inotify", - "libc", - "mio", - "mio-extras", - "walkdir", - "winapi 0.3.9", -] - -[[package]] -name = "num-integer" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" -dependencies = [ - "libc", -] - -[[package]] -name = "openssl" -version = "0.10.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8152bb5a9b5b721538462336e3bef9a539f892715e5037fda0f984577311af15" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "lazy_static", - "libc", - "openssl-sys", -] - -[[package]] -name = "openssl-probe" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" - -[[package]] -name = "openssl-sys" -version = "0.9.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fad9e54bd23bd4cbbe48fdc08a1b8091707ac869ef8508edea2fec77dcc884" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "owning_ref" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "parking_lot" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" -dependencies = [ - "libc", - "rand 0.6.5", - "rustc_version", - "smallvec", - "winapi 0.3.9", -] - -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared", - "rand 0.7.2", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pipeline" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0" - -[[package]] -name = "pkg-config" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d5370d90f49f70bd033c3d75e87fc529fbfff9d6f7cccef07d6170079d91ea" - -[[package]] -name = "podio" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd" - -[[package]] -name = "ppv-lite86" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - -[[package]] -name = "proc-macro2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" -dependencies = [ - "unicode-xid 0.2.0", -] - -[[package]] -name = "publicsuffix" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf259a81de2b2eb9850ec990ec78e6a25319715584fd7652b9b26f96fcb1510" -dependencies = [ - "error-chain", - "idna 0.2.0", - "lazy_static", - "regex", - "url 2.1.0", -] - -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - -[[package]] -name = "quote" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" -dependencies = [ - "proc-macro2 1.0.24", -] - -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg 0.1.2", - "rand_xorshift", - "winapi 0.3.9", -] - -[[package]] -name = "rand" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" -dependencies = [ - "getrandom", - "libc", - "rand_chacha 0.2.1", - "rand_core 0.5.1", - "rand_hc 0.2.0", - "rand_pcg 0.2.1", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg", - "rand_core 0.3.1", -] - -[[package]] -name = "rand_chacha" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" -dependencies = [ - "c2-chacha", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi 0.3.9", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi 0.3.9", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg", - "rand_core 0.4.2", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "redox_syscall" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" - -[[package]] -name = "redox_users" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d" -dependencies = [ - "failure", - "rand_os", - "redox_syscall", - "rust-argon2", -] - -[[package]] -name = "regex" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", - "thread_local", -] - -[[package]] -name = "regex-syntax" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" - -[[package]] -name = "remove_dir_all" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "reqwest" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6d896143a583047512e59ac54a215cb203c29cc941917343edea3be8df9c78" -dependencies = [ - "base64", - "bytes", - "cookie", - "cookie_store", - "encoding_rs", - "flate2", - "futures", - "http", - "hyper", - "hyper-tls", - "log", - "mime", - "mime_guess", - "native-tls", - "serde", - "serde_json", - "serde_urlencoded", - "time", - "tokio", - "tokio-executor", - "tokio-io", - "tokio-threadpool", - "tokio-timer", - "url 1.7.2", - "uuid", - "winreg", -] - -[[package]] -name = "rust-argon2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" -dependencies = [ - "base64", - "blake2b_simd", - "crossbeam-utils", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" - -[[package]] -name = "same-file" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021" -dependencies = [ - "lazy_static", - "winapi 0.3.9", -] - -[[package]] -name = "scopeguard" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" - -[[package]] -name = "scopeguard" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" - -[[package]] -name = "security-framework" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee63d0f4a9ec776eeb30e220f0bc1e092c3ad744b2a379e3993070364d3adc2" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9636f8989cbf61385ae4824b98c1aaa54c994d7d8b41f11c601ed799f0549a56" -dependencies = [ - "core-foundation-sys", -] - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - -[[package]] -name = "serde" -version = "1.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" -dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", - "syn 1.0.53", -] - -[[package]] -name = "serde_json" -version = "1.0.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" -dependencies = [ - "dtoa", - "itoa", - "serde", - "url 1.7.2", -] - -[[package]] -name = "serde_yaml" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7" -dependencies = [ - "dtoa", - "linked-hash-map", - "serde", - "yaml-rust", -] - -[[package]] -name = "signal-hook" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff2db2112d6c761e12522c65f7768548bd6e8cd23d2a9dae162520626629bd6" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" -dependencies = [ - "arc-swap", - "libc", -] - -[[package]] -name = "simplelog" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebbe8c881061cce7ee205784634eda7a61922925e7cc2833188467d3a560e027" -dependencies = [ - "chrono", - "log", - "term", -] - -[[package]] -name = "siphasher" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" - -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" - -[[package]] -name = "smallvec" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" - -[[package]] -name = "stable_deref_trait" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" - -[[package]] -name = "string" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" -dependencies = [ - "bytes", -] - -[[package]] -name = "string_cache" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a" -dependencies = [ - "lazy_static", - "new_debug_unreachable", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2 1.0.24", - "quote 1.0.2", -] - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - -[[package]] -name = "syn" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" -dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", - "unicode-xid 0.2.0", -] - -[[package]] -name = "synstructure" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", - "unicode-xid 0.1.0", -] - -[[package]] -name = "tempfile" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" -dependencies = [ - "cfg-if", - "libc", - "rand 0.7.2", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", -] - -[[package]] -name = "tendril" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "term" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" -dependencies = [ - "dirs", - "winapi 0.3.9", -] - -[[package]] -name = "termios" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625" -dependencies = [ - "libc", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thread_local" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "time" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" -dependencies = [ - "libc", - "redox_syscall", - "winapi 0.3.9", -] - -[[package]] -name = "tokio" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" -dependencies = [ - "bytes", - "futures", - "mio", - "num_cpus", - "tokio-current-thread", - "tokio-executor", - "tokio-io", - "tokio-reactor", - "tokio-tcp", - "tokio-threadpool", - "tokio-timer", -] - -[[package]] -name = "tokio-buf" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" -dependencies = [ - "bytes", - "either", - "futures", -] - -[[package]] -name = "tokio-current-thread" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443" -dependencies = [ - "futures", - "tokio-executor", -] - -[[package]] -name = "tokio-executor" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f27ee0e6db01c5f0b2973824547ce7e637b2ed79b891a9677b0de9bd532b6ac" -dependencies = [ - "crossbeam-utils", - "futures", -] - -[[package]] -name = "tokio-io" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926" -dependencies = [ - "bytes", - "futures", - "log", -] - -[[package]] -name = "tokio-reactor" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af16bfac7e112bea8b0442542161bfc41cbfa4466b580bdda7d18cb88b911ce" -dependencies = [ - "crossbeam-utils", - "futures", - "lazy_static", - "log", - "mio", - "num_cpus", - "parking_lot", - "slab", - "tokio-executor", - "tokio-io", - "tokio-sync", -] - -[[package]] -name = "tokio-sync" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2162248ff317e2bc713b261f242b69dbb838b85248ed20bb21df56d60ea4cae7" -dependencies = [ - "fnv", - "futures", -] - -[[package]] -name = "tokio-tcp" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119" -dependencies = [ - "bytes", - "futures", - "iovec", - "mio", - "tokio-io", - "tokio-reactor", -] - -[[package]] -name = "tokio-threadpool" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ca01319dea1e376a001e8dc192d42ebde6dd532532a5bad988ac37db365b19" -dependencies = [ - "crossbeam-deque", - "crossbeam-queue", - "crossbeam-utils", - "futures", - "log", - "num_cpus", - "rand 0.6.5", - "slab", - "tokio-executor", -] - -[[package]] -name = "tokio-timer" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2106812d500ed25a4f38235b9cae8f78a09edf43203e16e59c3b769a342a60e" -dependencies = [ - "crossbeam-utils", - "futures", - "slab", - "tokio-executor", -] - -[[package]] -name = "try-lock" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" - -[[package]] -name = "try_from" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "unicase" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e2e6bd1e59e56598518beb94fd6db628ded570326f0a98c679a304bd9f00150" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -dependencies = [ - "matches", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" -dependencies = [ - "smallvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" - -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - -[[package]] -name = "unicode-xid" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" - -[[package]] -name = "url" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -dependencies = [ - "idna 0.1.5", - "matches", - "percent-encoding 1.0.1", -] - -[[package]] -name = "url" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b414f6c464c879d7f9babf951f23bc3743fb7313c081b2e6ca719067ea9d61" -dependencies = [ - "idna 0.2.0", - "matches", - "percent-encoding 2.1.0", -] - -[[package]] -name = "utf-8" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" - -[[package]] -name = "uuid" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" -dependencies = [ - "rand 0.6.5", -] - -[[package]] -name = "vcpkg" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95" - -[[package]] -name = "vec_map" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" - -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - -[[package]] -name = "walkdir" -version = "2.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" -dependencies = [ - "same-file", - "winapi 0.3.9", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" -dependencies = [ - "futures", - "log", - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" - [[package]] name = "widestring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - -[[package]] -name = "xml5ever" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" -dependencies = [ - "log", - "mac", - "markup5ever", - "time", -] - -[[package]] -name = "yaml-rust" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" -dependencies = [ - "linked-hash-map", -] -[[package]] -name = "zip" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c21bb410afa2bd823a047f5bda3adb62f51074ac7e06263b2c97ecdd47e9fc6" -dependencies = [ - "bzip2", - "crc32fast", - "flate2", - "podio", - "time", -] +[metadata] +"checksum cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)" = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +"checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +"checksum log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +"checksum widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" diff --git a/Cargo.toml b/Cargo.toml index f64e305..4fd902e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,55 +1,6 @@ -[package] -name = "espanso" -version = "0.7.3" -authors = ["Federico Terzi "] -license = "GPL-3.0" -description = "Cross-platform Text Expander written in Rust" -readme = "README.md" -homepage = "https://github.com/federico-terzi/espanso" -edition = "2018" -build="build.rs" +[workspace] -[modulo] -version = "0.1.1" - -[dependencies] -widestring = "0.4.0" -serde = { version = "1.0.117", features = ["derive"] } -serde_yaml = "0.8" -dirs = "2.0.2" -clap = "2.33.0" -regex = "1.3.1" -log = "0.4.8" -simplelog = "0.7.1" -fs2 = "0.4.3" -serde_json = "1.0.60" -log-panics = {version = "2.0.0", features = ["with-backtrace"]} -backtrace = "0.3.37" -chrono = "0.4.9" -lazy_static = "1.4.0" -walkdir = "2.2.9" -reqwest = "0.9.20" -tempfile = "3.1.0" -dialoguer = "0.4.0" -rand = "0.7.2" -zip = "0.5.3" -notify = "4.0.13" -markdown = "0.3.0" -html2text = "0.2.1" - -[target.'cfg(unix)'.dependencies] -libc = "0.2.62" -signal-hook = "0.1.15" - -[target.'cfg(windows)'.dependencies] -named_pipe = "0.4.1" -winapi = { version = "0.3.9", features = ["wincon"] } - -[build-dependencies] -cmake = "0.1.31" - -[package.metadata.deb] -maintainer = "Federico Terzi " -depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin" -section = "utility" -license-file = ["LICENSE", "1"] \ No newline at end of file +members = [ + "espanso", + "espanso-detect", +] \ No newline at end of file diff --git a/build.rs b/build.rs deleted file mode 100644 index 433fb3e..0000000 --- a/build.rs +++ /dev/null @@ -1,59 +0,0 @@ -extern crate cmake; -use cmake::Config; -use std::path::PathBuf; - -/* OS SPECIFIC CONFIGS */ - -#[cfg(target_os = "windows")] -fn get_config() -> PathBuf { - Config::new("native/libwinbridge").build() -} - -#[cfg(target_os = "linux")] -fn get_config() -> PathBuf { - Config::new("native/liblinuxbridge").build() -} - -#[cfg(target_os = "macos")] -fn get_config() -> PathBuf { - Config::new("native/libmacbridge").build() -} - -/* - OS CUSTOM CARGO CONFIG LINES - Note: this is where linked libraries should be specified. -*/ - -#[cfg(target_os = "windows")] -fn print_config() { - println!("cargo:rustc-link-lib=static=winbridge"); - println!("cargo:rustc-link-lib=dylib=user32"); - #[cfg(target_env = "gnu")] - println!("cargo:rustc-link-lib=dylib=gdiplus"); - #[cfg(target_env = "gnu")] - println!("cargo:rustc-link-lib=dylib=stdc++"); -} - -#[cfg(target_os = "linux")] -fn print_config() { - println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/"); - println!("cargo:rustc-link-lib=static=linuxbridge"); - println!("cargo:rustc-link-lib=dylib=X11"); - println!("cargo:rustc-link-lib=dylib=Xtst"); - println!("cargo:rustc-link-lib=dylib=xdo"); -} - -#[cfg(target_os = "macos")] -fn print_config() { - println!("cargo:rustc-link-lib=dylib=c++"); - println!("cargo:rustc-link-lib=static=macbridge"); - println!("cargo:rustc-link-lib=framework=Cocoa"); - println!("cargo:rustc-link-lib=framework=IOKit"); -} - -fn main() { - let dst = get_config(); - - println!("cargo:rustc-link-search=native={}", dst.display()); - print_config(); -} diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml new file mode 100644 index 0000000..3d8ae90 --- /dev/null +++ b/espanso-detect/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "espanso-detect" +version = "0.1.0" +authors = ["Federico Terzi "] +edition = "2018" +build="build.rs" + +[dependencies] +log = "0.4.14" + +[target.'cfg(windows)'.dependencies] +widestring = "0.4.3" + +[build-dependencies] +cc = "1.0.66" \ No newline at end of file diff --git a/espanso-detect/build.rs b/espanso-detect/build.rs new file mode 100644 index 0000000..9f1c2a2 --- /dev/null +++ b/espanso-detect/build.rs @@ -0,0 +1,55 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[cfg(target_os = "windows")] +fn cc_config() { + println!("cargo:rerun-if-changed=src/win32/native.cpp"); + println!("cargo:rerun-if-changed=src/win32/native.h"); + cc::Build::new() + .cpp(true) + .include("src/win32/native.h") + .file("src/win32/native.cpp") + .compile("espansodetect"); + + println!("cargo:rustc-link-lib=static=espansodetect"); + println!("cargo:rustc-link-lib=dylib=user32"); + #[cfg(target_env = "gnu")] + println!("cargo:rustc-link-lib=dylib=stdc++"); +} + +#[cfg(target_os = "linux")] +fn cc_config() { + println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/"); + println!("cargo:rustc-link-lib=static=linuxbridge"); + println!("cargo:rustc-link-lib=dylib=X11"); + println!("cargo:rustc-link-lib=dylib=Xtst"); + println!("cargo:rustc-link-lib=dylib=xdo"); +} + +#[cfg(target_os = "macos")] +fn cc_config() { + println!("cargo:rustc-link-lib=dylib=c++"); + println!("cargo:rustc-link-lib=static=macbridge"); + println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=IOKit"); +} + +fn main() { + cc_config(); +} diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs new file mode 100644 index 0000000..4f0315c --- /dev/null +++ b/espanso-detect/src/event.rs @@ -0,0 +1,117 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[derive(Debug)] +pub enum InputEvent { + Mouse(MouseEvent), + Keyboard(KeyboardEvent), +} + +#[derive(Debug)] +pub enum MouseButton { + Left, + Right, + Middle, + Button1, + Button2, + Button3, + Button4, + Button5, +} + +#[derive(Debug)] +pub struct MouseEvent { + pub button: MouseButton, + pub status: Status, +} + +#[derive(Debug)] +pub enum Status { + Pressed, + Released, +} + +#[derive(Debug)] +pub enum Variant { + Left, + Right, +} + +#[derive(Debug)] +pub struct KeyboardEvent { + pub key: Key, + pub value: Option, + pub status: Status, + pub variant: Option, +} + +// A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values +#[derive(Debug)] +pub enum Key { + // Modifiers + Alt, + CapsLock, + Control, + Meta, + NumLock, + Shift, + + // Whitespace + Enter, + Tab, + Space, + + // Navigation + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + End, + Home, + PageDown, + PageUp, + + // Editing keys + Backspace, + + // Function keys + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + + // Other keys, includes the raw code provided by the operating system + Other(i32), +} diff --git a/src/bridge/mod.rs b/espanso-detect/src/lib.rs similarity index 81% rename from src/bridge/mod.rs rename to espanso-detect/src/lib.rs index 3446e17..9543e4d 100644 --- a/src/bridge/mod.rs +++ b/espanso-detect/src/lib.rs @@ -1,7 +1,7 @@ /* * This file is part of espanso. * - * Copyright (C) 2019 Federico Terzi + * Copyright (C) 2019-2021 Federico Terzi * * espanso is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,11 +17,15 @@ * along with espanso. If not, see . */ +pub mod event; + #[cfg(target_os = "windows")] -pub(crate) mod windows; +pub mod win32; -#[cfg(target_os = "linux")] -pub(crate) mod linux; - -#[cfg(target_os = "macos")] -pub(crate) mod macos; +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 5); + } +} diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs new file mode 100644 index 0000000..f67fcf7 --- /dev/null +++ b/espanso-detect/src/win32/mod.rs @@ -0,0 +1,237 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use log::{trace, warn}; +use widestring::U16CStr; + +use crate::event::Status::*; +use crate::event::Variant::*; +use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; +use crate::event::{Key::*, MouseButton, MouseEvent}; + +const LEFT_VARIANT: i32 = 1; +const RIGHT_VARIANT: i32 = 2; + +const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1; +const INPUT_EVENT_TYPE_MOUSE: i32 = 2; + +const INPUT_STATUS_PRESSED: i32 = 1; +const INPUT_STATUS_RELEASED: i32 = 2; + +const INPUT_MOUSE_LEFT_BUTTON: i32 = 1; +const INPUT_MOUSE_RIGHT_BUTTON: i32 = 2; +const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3; +const INPUT_MOUSE_BUTTON_1: i32 = 4; +const INPUT_MOUSE_BUTTON_2: i32 = 5; +const INPUT_MOUSE_BUTTON_3: i32 = 6; +const INPUT_MOUSE_BUTTON_4: i32 = 7; +const INPUT_MOUSE_BUTTON_5: i32 = 8; + +// Take a look at the native.h header file for an explanation of the fields +#[repr(C)] +pub struct RawInputEvent { + pub event_type: i32, + + pub buffer: [u16; 24], + pub buffer_len: i32, + + pub key_code: i32, + pub variant: i32, + pub status: i32, +} + +#[allow(improper_ctypes)] +#[link(name = "native", kind = "static")] +extern "C" { + pub fn raw_eventloop( + _self: *const Win32Source, + event_callback: extern "C" fn(_self: *mut Win32Source, event: RawInputEvent), + ); +} + +pub type Win32SourceCallback = Box; +pub struct Win32Source { + callback: Win32SourceCallback, +} + +impl Win32Source { + pub fn new(callback: Win32SourceCallback) -> Win32Source { + Self { + callback + } + } + pub fn eventloop(&self) { + unsafe { + extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) { + let event: Option = event.into(); + if let Some(event) = event { + unsafe { (*(*_self).callback)(event) } + } else { + trace!("Unable to convert raw event to input event"); + } + } + + raw_eventloop( + self as *const Win32Source, + callback, + ); + } + } +} + +impl From for Option { + fn from(raw: RawInputEvent) -> Option { + let status = match raw.status { + INPUT_STATUS_RELEASED => Released, + INPUT_STATUS_PRESSED => Pressed, + _ => Pressed, + }; + + match raw.event_type { + // Keyboard events + INPUT_EVENT_TYPE_KEYBOARD => { + let (key, variant_hint) = key_code_to_key(raw.key_code); + + // If the raw event does not include an explicit variant, use the hint provided by the key code + let variant = match raw.variant { + LEFT_VARIANT => Some(Left), + RIGHT_VARIANT => Some(Right), + _ => variant_hint, + }; + + let value = if raw.buffer_len > 0 { + let raw_string_result = U16CStr::from_slice_with_nul(&raw.buffer); + match raw_string_result { + Ok(c_string) => { + let string_result = c_string.to_string(); + match string_result { + Ok(value) => Some(value), + Err(err) => { + warn!("Widechar conversion error: {}", err); + None + } + } + } + Err(err) => { + warn!("Received malformed widechar: {}", err); + None + } + } + } else { + None + }; + + return Some(InputEvent::Keyboard(KeyboardEvent { + key, + value, + status, + variant, + })); + } + // Mouse events + INPUT_EVENT_TYPE_MOUSE => { + let button = raw_to_mouse_button(raw.key_code); + + if let Some(button) = button { + return Some(InputEvent::Mouse(MouseEvent { button, status })); + } + } + _ => {} + } + + None + } +} + +// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values +fn key_code_to_key(key_code: i32) -> (Key, Option) { + match key_code { + // Modifiers + 0x12 => (Alt, None), + 0xA4 => (Alt, Some(Left)), + 0xA5 => (Alt, Some(Right)), + 0x14 => (CapsLock, None), + 0x11 => (Control, None), + 0xA2 => (Control, Some(Left)), + 0xA3 => (Control, Some(Right)), + 0x5B => (Meta, Some(Left)), + 0x5C => (Meta, Some(Right)), + 0x90 => (NumLock, None), + 0x10 => (Shift, None), + 0xA0 => (Shift, Some(Left)), + 0xA1 => (Shift, Some(Right)), + + // Whitespace + 0x0D => (Enter, None), + 0x09 => (Tab, None), + 0x20 => (Space, None), + + // Navigation + 0x28 => (ArrowDown, None), + 0x25 => (ArrowLeft, None), + 0x27 => (ArrowRight, None), + 0x26 => (ArrowUp, None), + 0x23 => (End, None), + 0x24 => (Home, None), + 0x22 => (PageDown, None), + 0x21 => (PageUp, None), + + // Editing keys + 0x08 => (Backspace, None), + + // Function keys + 0x70 => (F1, None), + 0x71 => (F2, None), + 0x72 => (F3, None), + 0x73 => (F4, None), + 0x74 => (F5, None), + 0x75 => (F6, None), + 0x76 => (F7, None), + 0x77 => (F8, None), + 0x78 => (F9, None), + 0x79 => (F10, None), + 0x7A => (F11, None), + 0x7B => (F12, None), + 0x7C => (F13, None), + 0x7D => (F14, None), + 0x7E => (F15, None), + 0x7F => (F16, None), + 0x80 => (F17, None), + 0x81 => (F18, None), + 0x82 => (F19, None), + 0x83 => (F20, None), + + // Other keys, includes the raw code provided by the operating system + _ => (Other(key_code), None), + } +} + +fn raw_to_mouse_button(raw: i32) -> Option { + match raw { + INPUT_MOUSE_LEFT_BUTTON => Some(MouseButton::Left), + INPUT_MOUSE_RIGHT_BUTTON => Some(MouseButton::Right), + INPUT_MOUSE_MIDDLE_BUTTON => Some(MouseButton::Middle), + INPUT_MOUSE_BUTTON_1 => Some(MouseButton::Button1), + INPUT_MOUSE_BUTTON_2 => Some(MouseButton::Button2), + INPUT_MOUSE_BUTTON_3 => Some(MouseButton::Button3), + INPUT_MOUSE_BUTTON_4 => Some(MouseButton::Button4), + INPUT_MOUSE_BUTTON_5 => Some(MouseButton::Button5), + _ => None, + } +} diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp new file mode 100644 index 0000000..6e586aa --- /dev/null +++ b/espanso-detect/src/win32/native.cpp @@ -0,0 +1,318 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#include "native.h" +#include +#include +#include +#include +#include + +#define UNICODE + +#ifdef __MINGW32__ +#ifndef WINVER +#define WINVER 0x0606 +#endif +#define STRSAFE_NO_DEPRECATE +#endif + +#include +#include +#include +#include + +// How many milliseconds must pass between events before refreshing the keyboard layout +const long refreshKeyboardLayoutInterval = 2000; +const USHORT mouseDownFlags = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN | + RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN | + RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_5_DOWN; +const USHORT mouseUpFlags = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP | + RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_UP | + RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_UP; + +DWORD lastKeyboardPressTick = 0; +HKL currentKeyboardLayout; +HWND window; +const wchar_t *const winclass = L"Espanso"; + +void *self = NULL; +EventCallback event_callback = NULL; + +/* + * Message handler procedure for the windows + */ +LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) +{ + switch (msg) + { + case WM_INPUT: // Message relative to the RAW INPUT events + { + InputEvent event = {}; + + // Get the input size + UINT dwSize; + GetRawInputData( + (HRAWINPUT)lp, + RID_INPUT, + NULL, + &dwSize, + sizeof(RAWINPUTHEADER)); + + // Create a proper sized structure to hold the data + std::vector lpb(dwSize); + + // Request the Raw input data + if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize, + sizeof(RAWINPUTHEADER)) != dwSize) + { + return 0; + } + + // Convert the input data + RAWINPUT *raw = reinterpret_cast(lpb.data()); + + if (raw->header.dwType == RIM_TYPEKEYBOARD) // Keyboard events + { + // We only want KEY UP AND KEY DOWN events + if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP && + raw->data.keyboard.Message != WM_SYSKEYDOWN) + { + return 0; + } + + // The alt key sends a SYSKEYDOWN instead of KEYDOWN event + int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN || + raw->data.keyboard.Message == WM_SYSKEYDOWN; + + DWORD currentTick = GetTickCount(); + + // If enough time has passed between the last keypress and now, refresh the keyboard layout + if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval) + { + + // Because keyboard layouts on windows are Window-specific, to get the current + // layout we need to get the foreground window and get its layout. + + HWND hwnd = GetForegroundWindow(); + if (hwnd) + { + DWORD threadID = GetWindowThreadProcessId(hwnd, NULL); + HKL newKeyboardLayout = GetKeyboardLayout(threadID); + + // It's not always valid, so update the current value only if available. + if (newKeyboardLayout != 0) + { + currentKeyboardLayout = newKeyboardLayout; + } + } + + lastKeyboardPressTick = currentTick; + } + + // Get keyboard state ( necessary to decode the associated Unicode char ) + std::vector lpKeyState(256); + if (GetKeyboardState(lpKeyState.data())) + { + // This flag is needed to avoid chaning the keyboard state for some layouts. + // Refer to issue: https://github.com/federico-terzi/espanso/issues/86 + UINT flags = 1 << 2; + + int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, currentKeyboardLayout); + + // Handle the corresponding string if present + if (result >= 1) + { + event.buffer_len = result; + } + else + { + // If the given key does not have a correspondent string, reset the buffer + memset(event.buffer, 0, sizeof(event.buffer)); + event.buffer_len = 0; + } + + event.event_type = INPUT_EVENT_TYPE_KEYBOARD; + event.key_code = raw->data.keyboard.VKey; + event.status = is_key_down ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED; + + // Load the key variants when appropriate + if (raw->data.keyboard.VKey == VK_SHIFT) + { + // To discriminate between the left and right shift, we need to employ a workaround. + // See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput + if (raw->data.keyboard.MakeCode == 42) + { // Left shift + event.variant = INPUT_LEFT_VARIANT; + } + if (raw->data.keyboard.MakeCode == 54) + { // Right shift + event.variant = INPUT_RIGHT_VARIANT; + } + } + else + { + // Also the ALT and CTRL key are special cases + // Check out the previous Stackoverflow question for more information + if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU) + { + if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0) + { + event.variant = INPUT_RIGHT_VARIANT; + } + else + { + event.variant = INPUT_LEFT_VARIANT; + } + } + } + } + } + else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse events + { + // Make sure the mouse event belongs to the supported ones + if ((raw->data.mouse.usButtonFlags & (mouseDownFlags | mouseUpFlags)) == 0) { + return 0; + } + + event.event_type = INPUT_EVENT_TYPE_MOUSE; + + if ((raw->data.mouse.usButtonFlags & mouseDownFlags) != 0) + { + event.status = INPUT_STATUS_PRESSED; + } else if ((raw->data.mouse.usButtonFlags & mouseUpFlags) != 0) { + event.status = INPUT_STATUS_RELEASED; + } + + // Convert the mouse flags into custom button mappings + if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_LEFT_BUTTON_UP)) != 0) { + event.key_code = INPUT_MOUSE_LEFT_BUTTON; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_UP)) != 0) { + event.key_code = INPUT_MOUSE_RIGHT_BUTTON; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_MIDDLE_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_UP)) != 0) { + event.key_code = INPUT_MOUSE_MIDDLE_BUTTON; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_1_UP)) != 0) { + event.key_code = INPUT_MOUSE_BUTTON_1; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_2_UP)) != 0) { + event.key_code = INPUT_MOUSE_BUTTON_2; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_3_DOWN | RI_MOUSE_BUTTON_3_UP)) != 0) { + event.key_code = INPUT_MOUSE_BUTTON_3; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_4_UP)) != 0) { + event.key_code = INPUT_MOUSE_BUTTON_4; + } else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_5_DOWN | RI_MOUSE_BUTTON_5_UP)) != 0) { + event.key_code = INPUT_MOUSE_BUTTON_5; + } + } + + // If valid, send the event to the Rust layer + if (event.event_type != 0 && self != NULL && event_callback != NULL) + { + event_callback(self, event); + } + + return 0; + } + default: + return DefWindowProc(window, msg, wp, lp); + } +} + +int32_t raw_eventloop(void *_self, EventCallback _callback) +{ + // Initialize the default keyboard layout + currentKeyboardLayout = GetKeyboardLayout(0); + + // Initialize the Worker window + // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa + WNDCLASSEX wndclass = { + sizeof(WNDCLASSEX), // cbSize: Size of this structure + 0, // style: Class styles + window_procedure, // lpfnWndProc: Pointer to the window procedure + 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure + 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. + GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. + NULL, // hIcon: A handle to the class icon. + LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor. + NULL, // hbrBackground: A handle to the class background brush. + NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu + winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. + NULL // hIconSm: A handle to a small icon that is associated with the window class. + }; + + if (RegisterClassEx(&wndclass)) + { + // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw + window = CreateWindowEx( + 0, // dwExStyle: The extended window style of the window being created. + winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass + L"Espanso Worker Window", // lpWindowName: The window name. + WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created. + CW_USEDEFAULT, // X: The initial horizontal position of the window. + CW_USEDEFAULT, // Y: The initial vertical position of the window. + 100, // nWidth: The width, in device units, of the window. + 100, // nHeight: The height, in device units, of the window. + NULL, // hWndParent: handle to the parent or owner window of the window being created. + NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. + GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. + NULL // lpParam: Pointer to a value to be passed to the window + ); + + // Register raw inputs + RAWINPUTDEVICE Rid[2]; + + Rid[0].usUsagePage = 0x01; + Rid[0].usUsage = 0x06; + Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; + Rid[0].hwndTarget = window; + + Rid[1].usUsagePage = 0x01; + Rid[1].usUsage = 0x02; + Rid[1].dwFlags = RIDEV_INPUTSINK; + Rid[1].hwndTarget = window; + + if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) + { // Something went wrong, error. + return -1; + } + } + else + { + // Something went wrong, error. + return -2; + } + + event_callback = _callback; + self = _self; + + if (window) + { + // Hide the window + ShowWindow(window, SW_HIDE); + + // Enter the Event loop + MSG msg; + while (GetMessage(&msg, 0, 0, 0)) + DispatchMessage(&msg); + } + + event_callback = NULL; + self = NULL; + + return 1; +} \ No newline at end of file diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h new file mode 100644 index 0000000..afd5552 --- /dev/null +++ b/espanso-detect/src/win32/native.h @@ -0,0 +1,69 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#ifndef ESPANSO_DETECT_H +#define ESPANSO_DETECT_H + +#include + +#define INPUT_EVENT_TYPE_KEYBOARD 1 +#define INPUT_EVENT_TYPE_MOUSE 2 + +#define INPUT_STATUS_PRESSED 1 +#define INPUT_STATUS_RELEASED 2 + +#define INPUT_LEFT_VARIANT 1 +#define INPUT_RIGHT_VARIANT 2 + +#define INPUT_MOUSE_LEFT_BUTTON 1 +#define INPUT_MOUSE_RIGHT_BUTTON 2 +#define INPUT_MOUSE_MIDDLE_BUTTON 3 +#define INPUT_MOUSE_BUTTON_1 4 +#define INPUT_MOUSE_BUTTON_2 5 +#define INPUT_MOUSE_BUTTON_3 6 +#define INPUT_MOUSE_BUTTON_4 7 +#define INPUT_MOUSE_BUTTON_5 8 + +typedef struct { + // Keyboard or Mouse event + int32_t event_type; + + // Contains the string corresponding to the key, if any + uint16_t buffer[24]; + // Length of the extracted string. Equals 0 if no string is extracted + int32_t buffer_len; + + // Virtual key code of the pressed key in case of keyboard events + // Mouse button code otherwise. + int32_t key_code; + + // Left or Right variant + int32_t variant; + + // Pressed or Released status + int32_t status; +} InputEvent; + +typedef void (*EventCallback)(void * self, InputEvent data); +extern EventCallback event_callback; + +// Initialize the Raw Input API and run the event loop. Blocking call. +extern "C" int32_t raw_eventloop(void * self, EventCallback callback); + +#endif //ESPANSO_DETECT_H \ No newline at end of file diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml new file mode 100644 index 0000000..396144a --- /dev/null +++ b/espanso/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "espanso" +version = "1.0.0" +authors = ["Federico Terzi "] +license = "GPL-3.0" +description = "Cross-platform Text Expander written in Rust" +readme = "README.md" +homepage = "https://github.com/federico-terzi/espanso" +edition = "2018" + +[dependencies] +espanso-detect = { path = "../espanso-detect" } \ No newline at end of file diff --git a/espanso/src/main.rs b/espanso/src/main.rs new file mode 100644 index 0000000..f4802c0 --- /dev/null +++ b/espanso/src/main.rs @@ -0,0 +1,8 @@ +fn main() { + println!("Hello, world!z"); + + let source = espanso_detect::win32::Win32Source::new(Box::new(|event| { + println!("ev {:?}", event); + })); + source.eventloop(); +} diff --git a/native/liblinuxbridge/CMakeLists.txt b/native/liblinuxbridge/CMakeLists.txt deleted file mode 100644 index 243fda2..0000000 --- a/native/liblinuxbridge/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION 3.0) -project(liblinuxbridge) - -set (CMAKE_CXX_STANDARD 14) -set(CMAKE_REQUIRED_INCLUDES "/usr/local/include" "/usr/include") - -add_library(linuxbridge STATIC bridge.cpp bridge.h fast_xdo.cpp fast_xdo.h) - -install(TARGETS linuxbridge DESTINATION .) \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp deleted file mode 100644 index ec31d09..0000000 --- a/native/liblinuxbridge/bridge.cpp +++ /dev/null @@ -1,625 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#include "bridge.h" -#include "fast_xdo.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -extern "C" { // Needed to avoid C++ compiler name mangling - #include -} - -/* -This code uses the X11 Record Extension to receive keyboard -events. Documentation of this library can be found here: -https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html - -We will refer to this extension as RE from now on. -*/ - -/* -This struct is needed to receive events from the RE. -The funny thing is: it's not defined there, it should though. -The only place this is mentioned is the libxnee library, -so check that out if you need a reference. -*/ -typedef union { - unsigned char type ; - xEvent event ; - xResourceReq req ; - xGenericReply reply ; - xError error ; - xConnSetupPrefix setup; -} XRecordDatum; - -/* -Connections to the X server, RE recommends 2 connections: -one for recording control and one for reading the recorded data. -*/ -Display *data_disp = NULL; -Display *ctrl_disp = NULL; - -XRecordRange *record_range; -XRecordContext context; - -xdo_t * xdo_context; - -// Callback invoked when a new key event occur. -void event_callback (XPointer, XRecordInterceptData*); -int error_callback(Display *display, XErrorEvent *error); - -KeypressCallback keypress_callback; -X11ErrorCallback x11_error_callback; -void * context_instance; - -void register_keypress_callback(KeypressCallback callback) { - keypress_callback = callback; -} - -void register_error_callback(X11ErrorCallback callback) { - x11_error_callback = callback; -} - -int32_t check_x11() { - Display *check_disp = XOpenDisplay(NULL); - - if (!check_disp) { - return -1; - } - - XCloseDisplay(check_disp); - return 1; -} - -int32_t initialize(void * _context_instance) { - setlocale(LC_ALL, ""); - - context_instance = _context_instance; - - /* - Open the connections to the X server. - RE recommends to open 2 connections to the X server: - one for the recording control and one to read the protocol - data. - */ - ctrl_disp = XOpenDisplay(NULL); - data_disp = XOpenDisplay(NULL); - - if (!ctrl_disp || !data_disp) { // Display error - return -1; - } - - /* - We must set the ctrl_disp to sync mode, or, when we the enable - context in data_disp, there will be a fatal X error. - */ - XSynchronize(ctrl_disp, True); - - int dummy; - - // Make sure the X RE is installed in this system. - if (!XRecordQueryVersion(ctrl_disp, &dummy, &dummy)) { - return -2; - } - - // Make sure the X Keyboard Extension is installed - if (!XkbQueryExtension(ctrl_disp, &dummy, &dummy, &dummy, &dummy, &dummy)) { - return -3; - } - - // Initialize the record range, that is the kind of events we want to track. - record_range = XRecordAllocRange (); - if (!record_range) { - return -4; - } - record_range->device_events.first = KeyPress; - record_range->device_events.last = ButtonPress; - - // We want to get the keys from all clients - XRecordClientSpec client_spec; - client_spec = XRecordAllClients; - - // Initialize the context - context = XRecordCreateContext(ctrl_disp, 0, &client_spec, 1, &record_range, 1); - if (!context) { - return -5; - } - - if (!XRecordEnableContextAsync(data_disp, context, event_callback, NULL)) { - return -6; - } - - xdo_context = xdo_new(NULL); - - // Setup a custom error handler - XSetErrorHandler(&error_callback); - - /** - * Note: We might never get a MappingNotify event if the - * modifier and keymap information was never cached in Xlib. - * The next line makes sure that this happens initially. - */ - XKeysymToKeycode(ctrl_disp, XK_F1); - - return 1; -} - -int32_t eventloop() { - bool running = true; - - int ctrl_fd = XConnectionNumber(ctrl_disp); - int data_fd = XConnectionNumber(data_disp); - - while (running) - { - fd_set fds; - FD_ZERO(&fds); - FD_SET(ctrl_fd, &fds); - FD_SET(data_fd, &fds); - timeval timeout; - timeout.tv_sec = 2; - timeout.tv_usec = 0; - int retval = select(max(ctrl_fd, data_fd) + 1, - &fds, NULL, NULL, &timeout); - - if (FD_ISSET(data_fd, &fds)) { - XRecordProcessReplies(data_disp); - } - if (FD_ISSET(ctrl_fd, &fds)) { - XEvent event; - XNextEvent(ctrl_disp, &event); - if (event.type == MappingNotify) { - XMappingEvent *e = (XMappingEvent *) &event; - if (e->request == MappingKeyboard) { - XRefreshKeyboardMapping(e); - } - } - } - } - - return 1; -} - -void cleanup() { - XRecordDisableContext(ctrl_disp, context); - XRecordFreeContext(ctrl_disp, context); - XFree (record_range); - XCloseDisplay(data_disp); - XCloseDisplay(ctrl_disp); - xdo_free(xdo_context); -} - -void event_callback(XPointer p, XRecordInterceptData *hook) -{ - // Make sure the event comes from the X11 server - if (hook->category != XRecordFromServer) { - XRecordFreeData(hook); - return; - } - - // Cast the event payload to a XRecordDatum, needed later to access the fields - // This struct was hard to find and understand. Turn's out that all the - // required data are included in the "event" field of this structure. - // The funny thing is that it's not a XEvent as one might expect, - // but a xEvent, a very different beast defined in the Xproto.h header. - // I suggest you to look at that header if you want to understand where the - // upcoming field where taken from. - XRecordDatum *data = (XRecordDatum*) hook->data; - - int event_type = data->type; - int key_code = data->event.u.u.detail; - - // In order to convert the key_code into the corresponding string, - // we need to synthesize an artificial XKeyEvent, to feed later to the - // XLookupString function. - XKeyEvent event; - event.display = ctrl_disp; - event.window = data->event.u.focus.window; - event.root = XDefaultRootWindow(ctrl_disp); - event.subwindow = None; - event.time = data->event.u.keyButtonPointer.time; - event.x = 1; - event.y = 1; - event.x_root = 1; - event.y_root = 1; - event.same_screen = True; - event.keycode = key_code; - event.state = data->event.u.keyButtonPointer.state; - event.type = KeyPress; - - // Extract the corresponding chars. - std::array buffer; - int res = XLookupString(&event, buffer.data(), buffer.size(), NULL, NULL); - - switch (event_type) { - case KeyPress: - //printf ("Press %d %d %s\n", key_code, res, buffer.data()); - if (res > 0 && key_code != 22) { // Printable character, but not backspace - keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code); - }else{ // Modifier key - keypress_callback(context_instance, NULL, 0, 1, key_code); - } - break; - case ButtonPress: // Send also mouse button presses as "other events" - //printf ("Press button %d\n", key_code); - keypress_callback(context_instance, NULL, 0, 2, key_code); - default: - break; - } - - XRecordFreeData(hook); -} - -int error_callback(Display *display, XErrorEvent *error) { - x11_error_callback(context_instance, error->error_code, error->request_code, error->minor_code); - return 0; -} - -void release_all_keys() { - char keys[32]; - XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard - for (int i = 0; i<32; i++) { - // Only those that show a keypress should be changed - if (keys[i] != 0) { - for (int k = 0; k<8; k++) { - if ((keys[i] & (1 << k)) != 0) { // Bit by bit check - int key_code = i*8 + k; - XTestFakeKeyEvent(xdo_context->xdpy, key_code, false, CurrentTime); - } - } - } - } -} - -void send_string(const char * string) { - // It may happen that when an expansion is triggered, some keys are still pressed. - // This causes a problem if the expanded match contains that character, as the injection - // will not be able to register that keypress (as it is already pressed). - // To solve the problem, before an expansion we get which keys are currently pressed - // and inject a key_release event so that they can be further registered. - release_all_keys(); - - xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1000); -} - -void send_enter() { - xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000); -} - -void fast_release_all_keys() { - Window focused; - int revert_to; - XGetInputFocus(xdo_context->xdpy, &focused, &revert_to); - - char keys[32]; - XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard - for (int i = 0; i<32; i++) { - // Only those that show a keypress should be changed - if (keys[i] != 0) { - for (int k = 0; k<8; k++) { - if ((keys[i] & (1 << k)) != 0) { // Bit by bit check - int key_code = i*8 + k; - fast_send_event(xdo_context, focused, key_code, 0); - } - } - } - } - - XFlush(xdo_context->xdpy); -} - -void fast_send_string(const char * string, int32_t delay) { - // It may happen that when an expansion is triggered, some keys are still pressed. - // This causes a problem if the expanded match contains that character, as the injection - // will not be able to register that keypress (as it is already pressed). - // To solve the problem, before an expansion we get which keys are currently pressed - // and inject a key_release event so that they can be further registered. - fast_release_all_keys(); - - Window focused; - int revert_to; - XGetInputFocus(xdo_context->xdpy, &focused, &revert_to); - - int actual_delay = 1; - if (delay > 0) { - actual_delay = delay * 1000; - } - - fast_enter_text_window(xdo_context, focused, string, actual_delay); -} - -void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count, int32_t delay) { - int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode); - - Window focused; - int revert_to; - XGetInputFocus(xdo_context->xdpy, &focused, &revert_to); - - for (int i = 0; i 0) { - usleep(delay * 1000); - XFlush(xdo_context->xdpy); - } - } - - XFlush(xdo_context->xdpy); -} - -void fast_send_enter() { - _fast_send_keycode_to_focused_window(XK_Return, 1, 0); -} - -void delete_string(int32_t count) { - for (int i = 0; ixdpy, win); - - snprintf(buffer, size, "%s", title); - - XFree(title); - } - - xdo_free(x); - - return result; -} - -int32_t get_active_window_class(char * buffer, int32_t size) { - xdo_t * x = xdo_new(NULL); - - if (!x) { - return -1; - } - - // Get the active window - Window win; - int ret = xdo_get_active_window(x, &win); - int result = 1; - if (ret) { - fprintf(stderr, "xdo_get_active_window reported an error\n"); - result = -2; - }else{ - XClassHint hint; - - if (XGetClassHint(x->xdpy, win, &hint)) { - snprintf(buffer, size, "%s", hint.res_class); - XFree(hint.res_name); - XFree(hint.res_class); - } - } - - xdo_free(x); - - return result; -} - -int32_t get_active_window_executable(char *buffer, int32_t size) { - xdo_t * x = xdo_new(NULL); - - if (!x) { - return -1; - } - - // Get the active window - Window win; - int ret = xdo_get_active_window(x, &win); - int result = 1; - if (ret) { - fprintf(stderr, "xdo_get_active_window reported an error\n"); - result = -2; - }else{ - // Get the window process PID - char *pid_raw = (char*)get_property(x->xdpy,win, XA_CARDINAL, "_NET_WM_PID", NULL); - if (pid_raw == NULL) { - result = -3; - }else{ - int pid = pid_raw[0] | pid_raw[1] << 8 | pid_raw[2] << 16 | pid_raw[3] << 24; - - // Get the executable path from it - char proc_path[250]; - snprintf(proc_path, 250, "/proc/%d/exe", pid); - - readlink(proc_path, buffer, size); - - XFree(pid_raw); - } - } - - xdo_free(x); - - return result; -} - -int32_t is_current_window_special() { - char class_buffer[250]; - int res = get_active_window_class(class_buffer, 250); - if (res > 0) { - if (strstr(class_buffer, "terminal") != NULL) { - return 1; - }else if (strstr(class_buffer, "URxvt") != NULL) { // urxvt terminal - return 4; - }else if (strstr(class_buffer, "XTerm") != NULL) { // XTerm and UXTerm - return 1; - }else if (strstr(class_buffer, "Termite") != NULL) { // Termite - return 1; - }else if (strstr(class_buffer, "konsole") != NULL) { // KDE Konsole - return 1; - }else if (strstr(class_buffer, "Terminator") != NULL) { // Terminator - return 1; - }else if (strstr(class_buffer, "stterm") != NULL) { // Simple terminal 3 - return 2; - }else if (strstr(class_buffer, "St") != NULL) { // Simple terminal - return 1; - }else if (strstr(class_buffer, "st") != NULL) { // Simple terminal 2 - return 1; - }else if (strstr(class_buffer, "Alacritty") != NULL) { // Alacritty terminal - return 1; - }else if (strstr(class_buffer, "Emacs") != NULL) { // Emacs - return 3; - }else if (strstr(class_buffer, "yakuake") != NULL) { // Yakuake terminal - return 1; - }else if (strstr(class_buffer, "Tilix") != NULL) { // Tilix terminal - return 1; - }else if (strstr(class_buffer, "kitty") != NULL) { // kitty terminal - return 1; - } - } - - return 0; -} - - diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h deleted file mode 100644 index 5102d0c..0000000 --- a/native/liblinuxbridge/bridge.h +++ /dev/null @@ -1,162 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#ifndef ESPANSO_BRIDGE_H -#define ESPANSO_BRIDGE_H - -#include - -extern void * context_instance; - -/* - * Check if the X11 context is available - */ -extern "C" int32_t check_x11(); - -/* - * Initialize the X11 context and parameters - */ -extern "C" int32_t initialize(void * context_instance); - -/* - * Start the event loop indefinitely. Blocking call. - */ -extern "C" int32_t eventloop(); - -/* - * Clean all the X11 resources allocated during the initialization. - */ -extern "C" void cleanup(); - -/* - * Called when a new keypress is made, the first argument is an char array, - * while the second is the size of the array. - */ -typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code); -extern KeypressCallback keypress_callback; - -/* - * Register the callback that will be called when a keypress was made - */ -extern "C" void register_keypress_callback(KeypressCallback callback); - -/* - * Called when a X11 error occurs - */ -typedef void (*X11ErrorCallback)(void * self, char error_code, char request_code, char minor_code); -extern X11ErrorCallback x11_error_callback; - -/* - * Register the callback that will be called when an X11 error occurs - */ -extern "C" void register_error_callback(X11ErrorCallback callback); - -/* - * Type the given string by simulating Key Presses - */ -extern "C" void send_string(const char * string); - -/* - * Type the given string by simulating Key Presses using a faster inject method - */ -extern "C" void fast_send_string(const char * string, int32_t delay); - -/* - * Send the backspace keypress, *count* times. - */ -extern "C" void delete_string(int32_t count); - -/* - * Send the backspace keypress, *count* times using a faster inject method - */ -extern "C" void fast_delete_string(int32_t count, int32_t delay); - -/* - * Send an Enter key press - */ -extern "C" void send_enter(); - -/* - * Send an Enter key press using a faster inject method - */ -extern "C" void fast_send_enter(); - -/* - * Send the left arrow keypress, *count* times. - */ -extern "C" void left_arrow(int32_t count); - -/* - * Send the left arrow keypress, *count* times using a faster inject method - */ -extern "C" void fast_left_arrow(int32_t count); - -/* - * Trigger normal paste ( Pressing CTRL+V ) - */ -extern "C" void trigger_paste(); - -/* - * Trigger terminal paste ( Pressing CTRL+SHIFT+V ) - */ -extern "C" void trigger_terminal_paste(); - -/* - * Trigger shift ins pasting( Pressing SHIFT+INS ) - */ -extern "C" void trigger_shift_ins_paste(); - -/* - * Trigger alt shift ins pasting( Pressing ALT+SHIFT+INS ) - */ -extern "C" void trigger_alt_shift_ins_paste(); - -/* - * Trigger CTRL+ALT+V pasting - */ -extern "C" void trigger_ctrl_alt_paste(); - -/* - * Trigger copy shortcut ( Pressing CTRL+C ) - */ -extern "C" void trigger_copy(); - -// SYSTEM MODULE - -/* - * Return the active windows's WM_NAME - */ -extern "C" int32_t get_active_window_name(char * buffer, int32_t size); - -/* - * Return the active windows's WM_CLASS - */ -extern "C" int32_t get_active_window_class(char * buffer, int32_t size); - -/* - * Return the active windows's executable path - */ -extern "C" int32_t get_active_window_executable(char * buffer, int32_t size); - -/* - * Return a value greater than 0 if the current window needs a special paste combination, 0 otherwise. - */ -extern "C" int32_t is_current_window_special(); - -#endif //ESPANSO_BRIDGE_H diff --git a/native/liblinuxbridge/fast_xdo.cpp b/native/liblinuxbridge/fast_xdo.cpp deleted file mode 100644 index 4308941..0000000 --- a/native/liblinuxbridge/fast_xdo.cpp +++ /dev/null @@ -1,245 +0,0 @@ -// -// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT -// and modified to use XSendEvent instead of XTestFakeKeyEvent. - -#include -#include -#include - -#include "fast_xdo.h" - -extern "C" { // Needed to avoid C++ compiler name mangling -#include -} - -void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) { - xk->display = xdo->xdpy; - xk->subwindow = None; - xk->time = CurrentTime; - xk->same_screen = True; - - /* Should we set these at all? */ - xk->x = xk->y = xk->x_root = xk->y_root = 1; -} - -void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, - int modstate, int is_press, useconds_t delay) { - /* Properly ensure the modstate is set by finding a key - * that activates each bit in the modifier state */ - int mask = modstate | key->modmask; - - /* Since key events have 'state' (shift, etc) in the event, we don't - * need to worry about key press ordering. */ - XKeyEvent xk; - fast_init_xkeyevent(xdo, &xk); - xk.window = window; - xk.keycode = key->code; - xk.state = mask | (key->group << 13); - xk.type = (is_press ? KeyPress : KeyRelease); - XSendEvent(xdo->xdpy, xk.window, True, 0, (XEvent *)&xk); - - /* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */ - XFlush(xdo->xdpy); - if (delay > 0) { - usleep(delay); - } -} - -int fast_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys, - int nkeys, int pressed, int *modifier, useconds_t delay) { - int i = 0; - int modstate = 0; - int keymapchanged = 0; - - /* Find an unused keycode in case we need to bind unmapped keysyms */ - KeySym *keysyms = NULL; - int keysyms_per_keycode = 0; - int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */ - keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low, - xdo->keycode_high - xdo->keycode_low, - &keysyms_per_keycode); - - /* Find a keycode that is unused for scratchspace */ - for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) { - int j = 0; - int key_is_empty = 1; - for (j = 0; j < keysyms_per_keycode; j++) { - /*char *symname;*/ - int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j; - /*symname = XKeysymToString(keysyms[symindex]);*/ - if (keysyms[symindex] != 0) { - key_is_empty = 0; - } else { - break; - } - } - if (key_is_empty) { - scratch_keycode = i; - break; - } - } - XFree(keysyms); - - /* Allow passing NULL for modifier in case we don't care about knowing - * the modifier map state after we finish */ - if (modifier == NULL) - modifier = &modstate; - - for (i = 0; i < nkeys; i++) { - if (keys[i].needs_binding == 1) { - KeySym keysym_list[] = { keys[i].symbol }; - //_xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode); - XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); - XSync(xdo->xdpy, False); - /* override the code in our current key to use the scratch_keycode */ - keys[i].code = scratch_keycode; - keymapchanged = 1; - } - - //fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n", - //keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier); - fast_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay); - - if (keys[i].needs_binding == 1) { - /* If we needed to make a new keymapping for this keystroke, we - * should sync with the server now, after the keypress, so that - * the next mapping or removal doesn't conflict. */ - XSync(xdo->xdpy, False); - } - - if (pressed) { - *modifier |= keys[i].modmask; - } else { - *modifier &= ~(keys[i].modmask); - } - } - - - if (keymapchanged) { - KeySym keysym_list[] = { 0 }; - //printf(xdo, "Reverting scratch keycode (sym %lu to %d)", - // keys[i].symbol, scratch_keycode); - XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); - } - - /* Necessary? */ - XFlush(xdo->xdpy); - return XDO_SUCCESS; -} - -KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key) { - int i = 0; - int len = xdo->charcodes_len; - - //printf("Finding symbol for key '%c'\n", key); - for (i = 0; i < len; i++) { - //printf(" => %c vs %c (%d)\n", - //key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key)); - if (xdo->charcodes[i].key == key) { - //printf(" => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol); - return xdo->charcodes[i].symbol; - } - } - - if (key >= 0x100) key += 0x01000000; - if (XKeysymToString(key)) return key; - return NoSymbol; -} - -void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) { - int i = 0; - int len = xdo->charcodes_len; - - key->code = 0; - key->symbol = keysym; - key->group = 0; - key->modmask = 0; - key->needs_binding = 1; - - for (i = 0; i < len; i++) { - if (xdo->charcodes[i].symbol == keysym) { - key->code = xdo->charcodes[i].code; - key->group = xdo->charcodes[i].group; - key->modmask = xdo->charcodes[i].modmask; - key->needs_binding = 0; - return; - } - } -} - -void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) { - KeySym keysym = fast_keysym_from_char(xdo, key->key); - fast_charcodemap_from_keysym(xdo, key, keysym); -} - -/* XXX: Return proper code if errors found */ -int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) { - - /* Since we're doing down/up, the delay should be based on the number - * of keys pressed (including shift). Since up/down is two calls, - * divide by two. */ - delay /= 2; - - /* XXX: Add error handling */ - //int nkeys = strlen(string); - //charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t)); - charcodemap_t key; - //int modifier = 0; - setlocale(LC_CTYPE,""); - mbstate_t ps = { 0 }; - ssize_t len; - while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) { - if (len == -1) { - fprintf(stderr, "Invalid multi-byte sequence encountered\n"); - return XDO_ERROR; - } - fast_charcodemap_from_char(xdo, &key); - if (key.code == 0 && key.symbol == NoSymbol) { - fprintf(stderr, "I don't what key produces '%lc', skipping.\n", - key.key); - continue; - } else { - //printf("Found key for %c\n", key.key); - //printf("code: %d\n", key.code); - //printf("sym: %s\n", XKeysymToString(key.symbol)); - } - - //printf(stderr, - //"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n", - //key.key, key.code, key.symbol, key.group, key.modmask, - //(key.needs_binding == 1) ? "needs binding" : "ok"); - - //_xdo_send_key(xdo, window, keycode, modstate, True, delay); - //_xdo_send_key(xdo, window, keycode, modstate, False, delay); - fast_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2); - key.needs_binding = 0; - fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2); - - XFlush(xdo->xdpy); - } /* walk string generating a keysequence */ - - //free(keys); - return XDO_SUCCESS; -} - -void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed) { - XKeyEvent xk; - xk.display = xdo->xdpy; - xk.window = window; - xk.root = XDefaultRootWindow(xdo->xdpy); - xk.subwindow = None; - xk.time = CurrentTime; - xk.x = 1; - xk.y = 1; - xk.x_root = 1; - xk.y_root = 1; - xk.same_screen = True; - xk.keycode = keycode; - xk.state = 0; - xk.type = (pressed ? KeyPress : KeyRelease); - - XEvent event; - event.xkey =xk; - - XSendEvent(xdo->xdpy, window, True, 0, &event); -} diff --git a/native/liblinuxbridge/fast_xdo.h b/native/liblinuxbridge/fast_xdo.h deleted file mode 100644 index a7de8a4..0000000 --- a/native/liblinuxbridge/fast_xdo.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT -// and modified to use XSendEvent instead of XTestFakeKeyEvent. - -#ifndef LIBLINUXBRIDGE_FAST_XDO_H -#define LIBLINUXBRIDGE_FAST_XDO_H - -extern "C" { // Needed to avoid C++ compiler name mangling -#include -} - -KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key); -void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key); -void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym); -void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk); -void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, - int modstate, int is_press, useconds_t delay); -int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay); -void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed); - -#endif //LIBLINUXBRIDGE_FAST_XDO_H diff --git a/native/libmacbridge/AppDelegate.h b/native/libmacbridge/AppDelegate.h deleted file mode 100644 index e37bb71..0000000 --- a/native/libmacbridge/AppDelegate.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#import -#import - -#include "bridge.h" - -@interface AppDelegate : NSObject { - @public NSStatusItem *myStatusItem; -} - -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification; -- (IBAction) statusIconClick: (id) sender; -- (IBAction) contextMenuClick: (id) sender; -- (void) updateIcon: (char *)iconPath; -- (void) setIcon: (char *)iconPath; - -@end \ No newline at end of file diff --git a/native/libmacbridge/AppDelegate.mm b/native/libmacbridge/AppDelegate.mm deleted file mode 100644 index 340f04a..0000000 --- a/native/libmacbridge/AppDelegate.mm +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#import "AppDelegate.h" - -@implementation AppDelegate - -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification -{ - // Setup status icon - if (show_icon) { - myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain]; - [self setIcon: icon_path]; - } - - // Setup key listener - [NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown) - handler:^(NSEvent *event){ - - if (event.type == NSEventTypeKeyDown - && event.keyCode != 0x33) { // Send backspace as a modifier - - const char *chars = [event.characters UTF8String]; - int len = event.characters.length; - - keypress_callback(context_instance, chars, len, 0, event.keyCode); - //NSLog(@"keydown: %@, %d", event.characters, event.keyCode); - }else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown) { - // Send the mouse button clicks as "other" events, used to improve word matches reliability - keypress_callback(context_instance, NULL, 0, 2, event.buttonNumber); - }else{ - // Because this event is triggered for both the press and release of a modifier, trigger the callback - // only on release - if (([event modifierFlags] & (NSEventModifierFlagShift | NSEventModifierFlagCommand | - NSEventModifierFlagControl | NSEventModifierFlagOption)) == 0) { - - keypress_callback(context_instance, NULL, 0, 1, event.keyCode); - } - - //NSLog(@"keydown: %d", event.keyCode); - } - }]; -} - -- (void) updateIcon: (char *)iconPath { - if (show_icon) { - [self setIcon: iconPath]; - } -} - -- (void) setIcon: (char *)iconPath { - if (show_icon) { - NSString *nsIconPath = [NSString stringWithUTF8String:iconPath]; - NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath]; - [statusImage setTemplate:YES]; - - [myStatusItem.button setImage:statusImage]; - [myStatusItem setHighlightMode:YES]; - [myStatusItem.button setAction:@selector(statusIconClick:)]; - [myStatusItem.button setTarget:self]; - } -} - -- (IBAction) statusIconClick: (id) sender { - icon_click_callback(context_instance); -} - -- (IBAction) contextMenuClick: (id) sender { - NSInteger item_id = [[sender valueForKey:@"tag"] integerValue]; - - context_menu_click_callback(context_instance, static_cast(item_id)); -} - -@end \ No newline at end of file diff --git a/native/libmacbridge/CMakeLists.txt b/native/libmacbridge/CMakeLists.txt deleted file mode 100644 index 78689a7..0000000 --- a/native/libmacbridge/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION 3.0) -project(libmacbridge) - -set (CMAKE_CXX_STANDARD 11) -set(CMAKE_C_FLAGS "-x objective-c") - -add_library(macbridge STATIC bridge.mm bridge.h AppDelegate.h AppDelegate.mm) - -install(TARGETS macbridge DESTINATION .) \ No newline at end of file diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h deleted file mode 100644 index a735cf8..0000000 --- a/native/libmacbridge/bridge.h +++ /dev/null @@ -1,189 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#ifndef ESPANSO_BRIDGE_H -#define ESPANSO_BRIDGE_H - -#include - -extern "C" { - -extern void * context_instance; -extern char * icon_path; -extern char * disabled_icon_path; -extern int32_t show_icon; - -/* -* Initialize the AppDelegate and check for accessibility permissions -*/ -int32_t initialize(void * context, const char * icon_path, const char * disabled_icon_path, int32_t show_icon); - -/* - * Start the event loop indefinitely. Blocking call. - */ -int32_t eventloop(); - -/* - * Initialize the application and start the headless eventloop, used for the espanso detect command - */ -int32_t headless_eventloop(); - -/* - * Called when a new keypress is made, the first argument is an char array, - * while the second is the size of the array. - */ -typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code); - -extern KeypressCallback keypress_callback; - -/* - * Register the callback that will be called when a keypress was made - */ -void register_keypress_callback(KeypressCallback callback); - -/* - * Type the given string by using the CGEventKeyboardSetUnicodeString call - */ -void send_string(const char * string); - -/* - * Send the Virtual Key press - */ -void send_vkey(int32_t vk); - -/* - * Send the Virtual Key press multiple times - */ -void send_multi_vkey(int32_t vk, int32_t count); - -/* - * Send the backspace keypress, *count* times. - */ -void delete_string(int32_t count); - -/* - * Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed - */ -int32_t are_modifiers_pressed(); - -/* - * Trigger normal paste ( Pressing CMD+V ) - */ -void trigger_paste(); - -/* - * Trigger normal copy ( Pressing CMD+C ) - */ -void trigger_copy(); - -// UI - -/* - * Called when the tray icon is clicked - */ -typedef void (*IconClickCallback)(void * self); -extern IconClickCallback icon_click_callback; -void register_icon_click_callback(IconClickCallback callback); - -// CONTEXT MENU - -typedef struct { - int32_t id; - int32_t type; - char name[100]; -} MenuItem; - -int32_t show_context_menu(MenuItem * items, int32_t count); - -/* - * Called when the context menu is clicked - */ -typedef void (*ContextMenuClickCallback)(void * self, int32_t id); -extern ContextMenuClickCallback context_menu_click_callback; -extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback); - -/* - * Update the tray icon status - */ -extern "C" void update_tray_icon(int32_t enabled); - -// SYSTEM - -/* - * Check if espanso is authorized to control accessibility features, needed to detect key presses. - * @return - */ -int32_t check_accessibility(); - -/* - * Prompt to authorize the accessibility features. - * @return - */ -int32_t prompt_accessibility(); - -/* - * Open Security & Privacy settings panel - * @return - */ -void open_settings_panel(); - -/* - * Return the active NSRunningApplication path - */ -int32_t get_active_app_bundle(char * buffer, int32_t size); - -/* - * Return the active NSRunningApplication bundle identifier - */ -int32_t get_active_app_identifier(char * buffer, int32_t size); - -// CLIPBOARD - -/* - * Return the clipboard text - */ -int32_t get_clipboard(char * buffer, int32_t size); - -/* - * Set the clipboard text - */ -int32_t set_clipboard(char * text); - -/* - * Set the clipboard image to the given file - */ -int32_t set_clipboard_image(char * path); - -/* - * Set the clipboard html - */ -int32_t set_clipboard_html(char * html, char * fallback); - -/* - * If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID. - */ -int32_t get_secure_input_process(int64_t *pid); - -/* - * Find the executable path corresponding to the given PID, return 0 if no process was found. - */ -int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size); - -}; -#endif //ESPANSO_BRIDGE_H diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm deleted file mode 100644 index 72fa20b..0000000 --- a/native/libmacbridge/bridge.mm +++ /dev/null @@ -1,434 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#include "bridge.h" - -#import -#include -#include "AppDelegate.h" -#include -#include -#include -extern "C" { - -} - -#include - -void * context_instance; -char * icon_path; -char * disabled_icon_path; -int32_t show_icon; -AppDelegate * delegate_ptr; - -KeypressCallback keypress_callback; -IconClickCallback icon_click_callback; -ContextMenuClickCallback context_menu_click_callback; - -int32_t initialize(void * context, const char * _icon_path, const char * _disabled_icon_path, int32_t _show_icon) { - context_instance = context; - icon_path = strdup(_icon_path); - disabled_icon_path = strdup(_disabled_icon_path); - show_icon = _show_icon; - - AppDelegate *delegate = [[AppDelegate alloc] init]; - delegate_ptr = delegate; - NSApplication * application = [NSApplication sharedApplication]; - [application setDelegate:delegate]; -} - -void register_keypress_callback(KeypressCallback callback) { - keypress_callback = callback; -} - -void register_icon_click_callback(IconClickCallback callback) { - icon_click_callback = callback; -} - -void register_context_menu_click_callback(ContextMenuClickCallback callback) { - context_menu_click_callback = callback; -} - - -int32_t eventloop() { - [NSApp run]; -} - -int32_t headless_eventloop() { - NSApplication * application = [NSApplication sharedApplication]; - [NSApp run]; - return 0; -} - -void update_tray_icon(int32_t enabled) { - dispatch_async(dispatch_get_main_queue(), ^(void) { - NSApplication * application = [NSApplication sharedApplication]; - char * iconPath = icon_path; - if (!enabled) { - iconPath = disabled_icon_path; - } - - [[application delegate] updateIcon: iconPath]; - }); -} - -void send_string(const char * string) { - char * stringCopy = strdup(string); - dispatch_async(dispatch_get_main_queue(), ^(void) { - // Convert the c string to a UniChar array as required by the CGEventKeyboardSetUnicodeString method - NSString *nsString = [NSString stringWithUTF8String:stringCopy]; - CFStringRef cfString = (__bridge CFStringRef) nsString; - std::vector buffer(nsString.length); - CFStringGetCharacters(cfString, CFRangeMake(0, nsString.length), buffer.data()); - - free(stringCopy); - - // Send the event - - // Check if the shift key is down, and if so, release it - // To see why: https://github.com/federico-terzi/espanso/issues/279 - if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, 0x38)) { - CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x38, false); - CGEventPost(kCGHIDEventTap, e2); - CFRelease(e2); - - usleep(2000); - } - - // Because of a bug ( or undocumented limit ) of the CGEventKeyboardSetUnicodeString method - // the string gets truncated after 20 characters, so we need to send multiple events. - - int i = 0; - while (i < buffer.size()) { - int chunk_size = 20; - if ((i+chunk_size) > buffer.size()) { - chunk_size = buffer.size() - i; - } - - UniChar * offset_buffer = buffer.data() + i; - CGEventRef e = CGEventCreateKeyboardEvent(NULL, 0x31, true); - CGEventKeyboardSetUnicodeString(e, chunk_size, offset_buffer); - CGEventPost(kCGHIDEventTap, e); - CFRelease(e); - - usleep(2000); - - // Some applications require an explicit release of the space key - // For more information: https://github.com/federico-terzi/espanso/issues/159 - CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false); - CGEventPost(kCGHIDEventTap, e2); - CFRelease(e2); - - usleep(2000); - - i += chunk_size; - } - }); -} - -void delete_string(int32_t count) { - send_multi_vkey(0x33, count); -} - -void send_vkey(int32_t vk) { - dispatch_async(dispatch_get_main_queue(), ^(void) { - CGEventRef keydown; - keydown = CGEventCreateKeyboardEvent(NULL, vk, true); - CGEventPost(kCGHIDEventTap, keydown); - CFRelease(keydown); - - usleep(500); - - CGEventRef keyup; - keyup = CGEventCreateKeyboardEvent(NULL, vk, false); - CGEventPost(kCGHIDEventTap, keyup); - CFRelease(keyup); - - usleep(500); - }); -} - -void send_multi_vkey(int32_t vk, int32_t count) { - dispatch_async(dispatch_get_main_queue(), ^(void) { - for (int i = 0; i < count; i++) { - CGEventRef keydown; - keydown = CGEventCreateKeyboardEvent(NULL, vk, true); - CGEventPost(kCGHIDEventTap, keydown); - CFRelease(keydown); - - usleep(500); - - CGEventRef keyup; - keyup = CGEventCreateKeyboardEvent(NULL, vk, false); - CGEventPost(kCGHIDEventTap, keyup); - CFRelease(keyup); - - usleep(500); - } - }); -} - -void trigger_paste() { - dispatch_async(dispatch_get_main_queue(), ^(void) { - CGEventRef keydown; - keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true); // CMD - CGEventPost(kCGHIDEventTap, keydown); - CFRelease(keydown); - - usleep(2000); - - CGEventRef keydown2; - keydown2 = CGEventCreateKeyboardEvent(NULL, 0x09, true); // V key - CGEventPost(kCGHIDEventTap, keydown2); - CFRelease(keydown2); - - usleep(2000); - - CGEventRef keyup; - keyup = CGEventCreateKeyboardEvent(NULL, 0x09, false); - CGEventPost(kCGHIDEventTap, keyup); - CFRelease(keyup); - - usleep(2000); - - CGEventRef keyup2; - keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false); // CMD - CGEventPost(kCGHIDEventTap, keyup2); - CFRelease(keyup2); - - usleep(2000); - }); -} - - -void trigger_copy() { - dispatch_async(dispatch_get_main_queue(), ^(void) { - CGEventRef keydown; - keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true); // CMD - CGEventPost(kCGHIDEventTap, keydown); - CFRelease(keydown); - - usleep(2000); - - CGEventRef keydown2; - keydown2 = CGEventCreateKeyboardEvent(NULL, 0x08, true); // C key - CGEventPost(kCGHIDEventTap, keydown2); - CFRelease(keydown2); - - usleep(2000); - - CGEventRef keyup; - keyup = CGEventCreateKeyboardEvent(NULL, 0x08, false); - CGEventPost(kCGHIDEventTap, keyup); - CFRelease(keyup); - - usleep(2000); - - CGEventRef keyup2; - keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false); // CMD - CGEventPost(kCGHIDEventTap, keyup2); - CFRelease(keyup2); - - usleep(2000); - }); -} - -int32_t are_modifiers_pressed() { - if ((NSEventModifierFlagControl | NSEventModifierFlagOption | - NSEventModifierFlagCommand | NSEventModifierFlagShift) & [NSEvent modifierFlags]) { - return 1; - } - return 0; -} - -int32_t get_active_app_bundle(char * buffer, int32_t size) { - NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *bundlePath = [frontApp bundleURL].path; - const char * path = [bundlePath UTF8String]; - - snprintf(buffer, size, "%s", path); - - [bundlePath release]; - - return 1; -} - -int32_t get_active_app_identifier(char * buffer, int32_t size) { - NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *bundleId = frontApp.bundleIdentifier; - const char * bundle = [bundleId UTF8String]; - - snprintf(buffer, size, "%s", bundle); - - [bundleId release]; - - return 1; -} - -int32_t get_clipboard(char * buffer, int32_t size) { - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - for (id element in pasteboard.pasteboardItems) { - NSString *string = [element stringForType: NSPasteboardTypeString]; - if (string != NULL) { - const char * text = [string UTF8String]; - snprintf(buffer, size, "%s", text); - - [string release]; - - return 1; - } - } - - return -1; -} - -int32_t set_clipboard(char * text) { - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - NSArray *array = @[NSPasteboardTypeString]; - [pasteboard declareTypes:array owner:nil]; - - NSString *nsText = [NSString stringWithUTF8String:text]; - [pasteboard setString:nsText forType:NSPasteboardTypeString]; -} - -int32_t set_clipboard_image(char *path) { - NSString *pathString = [NSString stringWithUTF8String:path]; - NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString]; - int result = 0; - - if (image != nil) { - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - [pasteboard clearContents]; - NSArray *copiedObjects = [NSArray arrayWithObject:image]; - [pasteboard writeObjects:copiedObjects]; - result = 1; - } - [image release]; - - return result; -} - -int32_t set_clipboard_html(char * html, char * fallback_text) { - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString]; - [pasteboard declareTypes:array owner:nil]; - - NSString *nsHtml = [NSString stringWithUTF8String:html]; - NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil]; - NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil]; - - NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length]) - documentAttributes:nil]; - - [pasteboard setData:rtf forType:NSRTFPboardType]; - - NSString *nsText = [NSString stringWithUTF8String:fallback_text]; - [pasteboard setString:nsText forType:NSPasteboardTypeString]; -} - - -// CONTEXT MENU - -int32_t show_context_menu(MenuItem * items, int32_t count) { - MenuItem * item_copy = (MenuItem*)malloc(sizeof(MenuItem)*count); - memcpy(item_copy, items, sizeof(MenuItem)*count); - int32_t count_copy = count; - - dispatch_async(dispatch_get_main_queue(), ^(void) { - - NSMenu *espansoMenu = [[NSMenu alloc] initWithTitle:@"Espanso"]; - - for (int i = 0; imyStatusItem popUpStatusItemMenu:espansoMenu]; - }); -} - -// 10.9+ only, see this url for compatibility: -// http://stackoverflow.com/questions/17693408/enable-access-for-assistive-devices-programmatically-on-10-9 -int32_t check_accessibility() { - NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @NO}; - return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts); -} - -int32_t prompt_accessibility() { - NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES}; - return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts); -} - -void open_settings_panel() { - NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; -} - -// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys -int32_t get_secure_input_process(int64_t *pid) { - NSArray *consoleUsersArray; - io_service_t rootService; - int32_t result = 0; - - if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0) - { - if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil) - { - if ([consoleUsersArray isKindOfClass:[NSArray class]]) // Be careful - ensure this really is an array - { - for (NSDictionary *consoleUserDict in consoleUsersArray) { - NSNumber *secureInputPID; - - if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil) - { - if ([secureInputPID isKindOfClass:[NSNumber class]]) - { - *pid = ((UInt64) [secureInputPID intValue]); - result = 1; - break; - } - } - } - } - - CFRelease((CFTypeRef)consoleUsersArray); - } - - IOObjectRelease((io_object_t) rootService); - } - - return result; -} - -int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size) { - int res = proc_pidpath((pid_t) pid, buff, buff_size); - if ( res <= 0 ) { - return 0; - } else { - return 1; - } -} \ No newline at end of file diff --git a/native/libwinbridge/CMakeLists.txt b/native/libwinbridge/CMakeLists.txt deleted file mode 100644 index f0ae960..0000000 --- a/native/libwinbridge/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -cmake_minimum_required(VERSION 3.0) -project(libwinbridge) - -set (CMAKE_CXX_STANDARD 14) - -add_library(winbridge STATIC bridge.cpp bridge.h) - -install(TARGETS winbridge DESTINATION .) \ No newline at end of file diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp deleted file mode 100644 index 11c9ab9..0000000 --- a/native/libwinbridge/bridge.cpp +++ /dev/null @@ -1,934 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#include "bridge.h" -#include -#include -#include -#include -#include -#include - -#define UNICODE - -#ifdef __MINGW32__ -# ifndef WINVER -# define WINVER 0x0606 -# endif -# define STRSAFE_NO_DEPRECATE -#endif - -#include -#include -#include -#include - -#pragma comment( lib, "gdiplus.lib" ) -#include -#include - -// How many milliseconds must pass between keystrokes to refresh the keyboard layout -const long refreshKeyboardLayoutInterval = 2000; - -void * manager_instance; -int32_t show_icon; - -// Keyboard listening - -DWORD lastKeyboardPressTick = 0; -HKL currentKeyboardLayout; -HWND window; -const wchar_t* const winclass = L"Espanso"; - - - -// UI - -#define APPWM_ICON_CLICK (WM_APP + 1) -#define APPWM_NOTIFICATION_POPUP (WM_APP + 2) -#define APPWM_NOTIFICATION_CLOSE (WM_APP + 3) -#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 4) - -const wchar_t* const notification_winclass = L"EspansoNotification"; -HWND nw = NULL; -HWND hwnd_st_u = NULL; -HBITMAP g_espanso_bmp = NULL; -HICON g_espanso_ico = NULL; -HICON g_espanso_red_ico = NULL; -NOTIFYICONDATA nid = {}; - -UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); - -// Callbacks - -KeypressCallback keypress_callback = NULL; -IconClickCallback icon_click_callback = NULL; -ContextMenuClickCallback context_menu_click_callback = NULL; - -void register_keypress_callback(KeypressCallback callback) { - keypress_callback = callback; -} - -void register_icon_click_callback(IconClickCallback callback) { - icon_click_callback = callback; -} - -void register_context_menu_click_callback(ContextMenuClickCallback callback) { - context_menu_click_callback = callback; -} - -/* - * Message handler procedure for the windows - */ -LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) -{ - HDC hdcStatic = NULL; - - switch (msg) - { - case WM_DESTROY: - std::cout << "\ndestroying window\n"; - PostQuitMessage(0); - DeleteObject(g_espanso_bmp); - DeleteObject(g_espanso_ico); - return 0L; - case WM_COMMAND: // Click on the tray icon context menu - { - UINT idItem = (UINT)LOWORD(wp); - UINT flags = (UINT)HIWORD(wp); - - if (flags == 0) { - context_menu_click_callback(manager_instance, (int32_t)idItem); - } - - break; - } - case APPWM_NOTIFICATION_POPUP: // Request to show a notification - { - std::unique_ptr ptr(reinterpret_cast(wp)); - - SetWindowText(hwnd_st_u, L" "); // Clear the previous text - SetWindowText(hwnd_st_u, ptr.get()); - - // Show the window - ShowWindow(nw, SW_SHOWNOACTIVATE); - break; - } - case APPWM_NOTIFICATION_CLOSE: // Request to close a notification - { - // Hide the window - ShowWindow(nw, SW_HIDE); - break; - } - case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu - { - HMENU hPopupMenu = CreatePopupMenu(); - - // Create the menu - - int32_t count = static_cast(lp); - std::unique_ptr items(reinterpret_cast(wp)); - - for (int i = 0; i lpb(dwSize); - - // Request the Raw input data - if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize, - sizeof(RAWINPUTHEADER)) != dwSize) { - return 0; - } - - // Convert the input data - RAWINPUT* raw = reinterpret_cast(lpb.data()); - // Make sure it's a keyboard type event, relative to a key press. - if (raw->header.dwType == RIM_TYPEKEYBOARD) - { - // We only want KEY UP AND KEY DOWN events - if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP && - raw->data.keyboard.Message != WM_SYSKEYDOWN) { - return 0; - } - - // The alt key sends a SYSKEYDOWN instead of KEYDOWN event - int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN || - raw->data.keyboard.Message == WM_SYSKEYDOWN; - - DWORD currentTick = GetTickCount(); - - // If enough time has passed between the last keypress and now, refresh the keyboard layout - if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval) { - - // Because keyboard layouts on windows are Window-specific, to get the current - // layout we need to get the foreground window and get its layout. - - HWND hwnd = GetForegroundWindow(); - if (hwnd) { - DWORD threadID = GetWindowThreadProcessId(hwnd, NULL); - HKL newKeyboardLayout = GetKeyboardLayout(threadID); - - // It's not always valid, so update the current value only if available. - if (newKeyboardLayout != 0) { - currentKeyboardLayout = newKeyboardLayout; - } - } - - lastKeyboardPressTick = currentTick; - } - - // Get keyboard state ( necessary to decode the associated Unicode char ) - std::vector lpKeyState(256); - if (GetKeyboardState(lpKeyState.data())) { - // Convert the virtual key to an unicode char - std::array buffer; - - // This flag is needed to avoid chaning the keyboard state for some layouts. - // Refer to issue: https://github.com/federico-terzi/espanso/issues/86 - UINT flags = 1 << 2; - - int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), flags, currentKeyboardLayout); - - //std::cout << result << " " << buffer[0] << " " << raw->data.keyboard.VKey << std::endl; - - // We need to call the callback in two different ways based on the type of key - // The only modifier we use that has a result > 0 is the BACKSPACE, so we have to consider it. - if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) { - keypress_callback(manager_instance, reinterpret_cast(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, 0, is_key_down); - }else{ - //std::cout << raw->data.keyboard.MakeCode << " " << raw->data.keyboard.Flags << std::endl; - int variant = 0; - if (raw->data.keyboard.VKey == VK_SHIFT) { - // To discriminate between the left and right shift, we need to employ a workaround. - // See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput - if (raw->data.keyboard.MakeCode == 42) { // Left shift - variant = LEFT_VARIANT; - }if (raw->data.keyboard.MakeCode == 54) { // Right shift - variant = RIGHT_VARIANT; - } - }else{ - // Also the ALT and CTRL key are special cases - // Check out the previous Stackoverflow question for more information - if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU) { - if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0) { - variant = RIGHT_VARIANT; - }else{ - variant = LEFT_VARIANT; - } - } - } - - keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, variant, is_key_down); - } - } - }else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse input, registered as "other" events. Needed to improve the reliability of word matches - { - if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN)) != 0) { - //std::cout << "mouse down" << std::endl; - keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0, 0); - } - } - - return 0; - } - default: - if (msg == WM_TASKBARCREATED) { // Explorer crashed, recreate the icon - if (show_icon) { - Shell_NotifyIcon(NIM_ADD, &nid); - } - } - return DefWindowProc(window, msg, wp, lp); - } -} - -int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t _show_icon) { - manager_instance = self; - show_icon = _show_icon; - - // Load the images - g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); - g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE); - g_espanso_red_ico = (HICON)LoadImage(NULL, red_ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE); - - // Make the notification capable of handling different screen definitions - SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); - - // Initialize the default keyboard layout - currentKeyboardLayout = GetKeyboardLayout(0); - - // Initialize the Worker window - - // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa - WNDCLASSEX wndclass = { - sizeof(WNDCLASSEX), // cbSize: Size of this structure - 0, // style: Class styles - window_procedure, // lpfnWndProc: Pointer to the window procedure - 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure - 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. - GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. - NULL, // hIcon: A handle to the class icon. - LoadCursor(0,IDC_ARROW), // hCursor: A handle to the class cursor. - NULL, // hbrBackground: A handle to the class background brush. - NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu - winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. - NULL // hIconSm: A handle to a small icon that is associated with the window class. - }; - - // Notification Window - - // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa - WNDCLASSEX notificationwndclass = { - sizeof(WNDCLASSEX), // cbSize: Size of this structure - 0, // style: Class styles - window_procedure, // lpfnWndProc: Pointer to the window procedure - 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure - 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. - GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. - NULL, // hIcon: A handle to the class icon. - LoadCursor(0,IDC_ARROW), // hCursor: A handle to the class cursor. - NULL, // hbrBackground: A handle to the class background brush. - NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu - notification_winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. - NULL // hIconSm: A handle to a small icon that is associated with the window class. - }; - - if (RegisterClassEx(&wndclass) && RegisterClassEx(¬ificationwndclass)) - { - // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw - window = CreateWindowEx( - 0, // dwExStyle: The extended window style of the window being created. - winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass - L"Espanso Worker Window", // lpWindowName: The window name. - WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created. - CW_USEDEFAULT, // X: The initial horizontal position of the window. - CW_USEDEFAULT, // Y: The initial vertical position of the window. - 100, // nWidth: The width, in device units, of the window. - 100, // nHeight: The height, in device units, of the window. - NULL, // hWndParent: handle to the parent or owner window of the window being created. - NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. - GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. - NULL // lpParam: Pointer to a value to be passed to the window - ); - - // Register raw inputs - RAWINPUTDEVICE Rid[2]; - - Rid[0].usUsagePage = 0x01; - Rid[0].usUsage = 0x06; - Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID keyboard and also ignores legacy keyboard messages - Rid[0].hwndTarget = window; - - Rid[1].usUsagePage = 0x01; - Rid[1].usUsage = 0x02; - Rid[1].dwFlags = RIDEV_INPUTSINK; // adds HID mouse and also ignores legacy mouse messages - Rid[1].hwndTarget = window; - - if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) { // Something went wrong, error. - return -1; - } - - // Initialize the notification window - nw = CreateWindowEx( - WS_EX_TOOLWINDOW | WS_EX_TOPMOST, // dwExStyle: The extended window style of the window being created. - notification_winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass - L"Espanso Notification", // lpWindowName: The window name. - WS_POPUPWINDOW, // dwStyle: The style of the window being created. - CW_USEDEFAULT, // X: The initial horizontal position of the window. - CW_USEDEFAULT, // Y: The initial vertical position of the window. - 300, // nWidth: The width, in device units, of the window. - 100, // nHeight: The height, in device units, of the window. - NULL, // hWndParent: handle to the parent or owner window of the window being created. - NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. - GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. - NULL // lpParam: Pointer to a value to be passed to the window - ); - - if (nw) - { - int x, w, y, h; - y = 40; h = 30; - x = 100; w = 180; - hwnd_st_u = CreateWindowEx(0, L"static", L"ST_U", - WS_CHILD | WS_VISIBLE | WS_TABSTOP | SS_CENTER, - x, y, w, h, - nw, (HMENU)(501), - (HINSTANCE)GetWindowLong(nw, GWLP_HINSTANCE), NULL); - - SetWindowText(hwnd_st_u, L"Loading..."); - - int posX = GetSystemMetrics(SM_CXSCREEN) - 350; - int posY = GetSystemMetrics(SM_CYSCREEN) - 200; - - SetWindowPos(nw, HWND_TOP, posX, posY, 0, 0, SWP_NOSIZE); - - // Hide the window - ShowWindow(nw, SW_HIDE); - - // Setup the icon in the notification space - - SendMessage(nw, WM_SETICON, ICON_BIG, (LPARAM)g_espanso_ico); - SendMessage(nw, WM_SETICON, ICON_SMALL, (LPARAM)g_espanso_ico); - - //Notification - nid.cbSize = sizeof(nid); - nid.hWnd = nw; - nid.uID = 1; - nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; - nid.uCallbackMessage = APPWM_ICON_CLICK; - nid.hIcon = g_espanso_ico; - StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), L"espanso"); - - // Show the notification. - if (show_icon) { - Shell_NotifyIcon(NIM_ADD, &nid); - } - } - }else{ - // Something went wrong, error. - return -1; - } - - return 1; -} - -void eventloop() { - if (window) - { - // Hide the window - ShowWindow(window, SW_HIDE); - - // Enter the Event loop - MSG msg; - while (GetMessage(&msg, 0, 0, 0)) DispatchMessage(&msg); - } - - // Something went wrong, this should have been an infinite loop. -} - -void update_tray_icon(int32_t enabled) { - if (enabled) { - nid.hIcon = g_espanso_ico; - }else{ - nid.hIcon = g_espanso_red_ico; - } - - // Update the icon - if (show_icon) { - Shell_NotifyIcon(NIM_MODIFY, &nid); - } -} - -/* - * Type the given string simulating keyboard presses. - */ -void send_string(const wchar_t * string) { - std::wstring msg = string; - - std::vector vec; - for (auto ch : msg) - { - INPUT input = { 0 }; - input.type = INPUT_KEYBOARD; - input.ki.dwFlags = KEYEVENTF_UNICODE; - input.ki.wScan = ch; - vec.push_back(input); - - input.ki.dwFlags |= KEYEVENTF_KEYUP; - vec.push_back(input); - } - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); -} - -/* - * Send the backspace keypress, *count* times. - */ -void delete_string(int32_t count, int32_t delay) { - if (delay != 0) { - send_multi_vkey_with_delay(VK_BACK, count, delay); - }else{ - send_multi_vkey(VK_BACK, count); - } -} - -void send_vkey(int32_t vk) { - std::vector vec; - - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = vk; - input.ki.dwFlags = 0; // 0 for key press - vec.push_back(input); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - vec.push_back(input); - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); -} - -void send_multi_vkey(int32_t vk, int32_t count) { - std::vector vec; - - for (int i = 0; i < count; i++) { - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = vk; - input.ki.dwFlags = 0; // 0 for key press - vec.push_back(input); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - vec.push_back(input); - } - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); -} - -void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay) { - for (int i = 0; i < count; i++) { - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = vk; - input.ki.dwFlags = 0; // 0 for key press - SendInput(1, &input, sizeof(INPUT)); - - Sleep(delay); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - SendInput(1, &input, sizeof(INPUT)); - - Sleep(delay); - } -} - - -void trigger_shift_paste() { - std::vector vec; - - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = VK_CONTROL; - input.ki.dwFlags = 0; // 0 for key press - vec.push_back(input); - - input.ki.wVk = VK_SHIFT; // SHIFT KEY - vec.push_back(input); - - input.ki.wVk = 0x56; // V KEY - vec.push_back(input); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - vec.push_back(input); - - input.ki.wVk = VK_SHIFT; // SHIFT KEY - vec.push_back(input); - - input.ki.wVk = VK_CONTROL; - vec.push_back(input); - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); -} - -void trigger_paste() { - std::vector vec; - - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = VK_CONTROL; - input.ki.dwFlags = 0; // 0 for key press - vec.push_back(input); - - input.ki.wVk = 0x56; // V KEY - vec.push_back(input); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - vec.push_back(input); - - input.ki.wVk = VK_CONTROL; - vec.push_back(input); - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); -} - -void trigger_copy() { - std::vector vec; - - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = VK_CONTROL; - input.ki.dwFlags = 0; // 0 for key press - vec.push_back(input); - - input.ki.wVk = 0x43; // C KEY - vec.push_back(input); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - vec.push_back(input); - - input.ki.wVk = VK_CONTROL; - vec.push_back(input); - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); -} - -int32_t are_modifiers_pressed() { - short ctrl_pressed = GetAsyncKeyState(VK_CONTROL); - short enter_pressed = GetAsyncKeyState(VK_RETURN); - short alt_pressed = GetAsyncKeyState(VK_MENU); - short shift_pressed = GetAsyncKeyState(VK_SHIFT); - short meta_pressed = GetAsyncKeyState(VK_LWIN); - short rmeta_pressed = GetAsyncKeyState(VK_RWIN); - if (((ctrl_pressed & 0x8000) + - (enter_pressed & 0x8000) + - (alt_pressed & 0x8000) + - (shift_pressed & 0x8000) + - (meta_pressed & 0x8000) + - (rmeta_pressed & 0x8000)) != 0) { - return 1; - } - - return 0; -} - - -// SYSTEM - -int32_t get_active_window_name(wchar_t * buffer, int32_t size) { - HWND hwnd = GetForegroundWindow(); - - return GetWindowText(hwnd, buffer, size); -} - -int32_t get_active_window_executable(wchar_t * buffer, int32_t size) { - HWND hwnd = GetForegroundWindow(); - - // Extract the window PID - DWORD windowPid; - GetWindowThreadProcessId(hwnd, &windowPid); - - DWORD dsize = (DWORD) size; - - // Extract the process executable file path - HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid); - int res = QueryFullProcessImageNameW(process, 0, buffer, &dsize); - CloseHandle(process); - - return res; -} - -// Notifications - -int32_t show_notification(wchar_t * message) { - if (nw != NULL) { - wchar_t * buffer = new wchar_t[100]; - swprintf(buffer, 100, L"%ls", message); - - PostMessage(nw, APPWM_NOTIFICATION_POPUP, reinterpret_cast(buffer), 0); - return 1; - } - - return -1; -} - -void close_notification() { - if (nw != NULL) { - PostMessage(nw, APPWM_NOTIFICATION_CLOSE, 0, 0); - } -} - -int32_t show_context_menu(MenuItem * items, int32_t count) { - if (nw != NULL) { - MenuItem * items_buffer = new MenuItem[count]; - memcpy(items_buffer, items, sizeof(MenuItem)*count); - - PostMessage(nw, APPWM_SHOW_CONTEXT_MENU, reinterpret_cast(items_buffer), static_cast(count)); - return 1; - } - - return -1; -} - -void cleanup_ui() { - Shell_NotifyIcon(NIM_DELETE, &nid); -} - -// SYSTEM - -int32_t start_daemon_process() { - wchar_t cmd[MAX_PATH]; - swprintf(cmd, MAX_PATH, L"espanso.exe daemon"); - - // Get current espanso directory - TCHAR espansoFilePath[MAX_PATH]; - GetModuleFileName(NULL, espansoFilePath, MAX_PATH); - - STARTUPINFO si = { sizeof(si) }; - PROCESS_INFORMATION pi; - - // Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw - BOOL res = CreateProcess( - espansoFilePath, - cmd, - NULL, - NULL, - FALSE, - DETACHED_PROCESS | CREATE_NO_WINDOW, - NULL, - NULL, - &si, - &pi - ); - - if (!res) { - return -1; - } - - return 1; -} - - -int32_t start_process(wchar_t * _cmd) { - wchar_t cmd[MAX_PATH]; - swprintf(cmd, MAX_PATH, _cmd); - - STARTUPINFO si = { sizeof(si) }; - PROCESS_INFORMATION pi; - - // Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw - BOOL res = CreateProcess( - NULL, - cmd, - NULL, - NULL, - FALSE, - DETACHED_PROCESS, - NULL, - NULL, - &si, - &pi - ); - - if (!res) { - return -1; - } - - return 1; -} - -// CLIPBOARD - -int32_t set_clipboard(wchar_t *text) { - int32_t result = 0; - const size_t len = wcslen(text) + 1; - HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t)); - memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t)); - GlobalUnlock(hMem); - if (!OpenClipboard(NULL)) { - return -1; - } - EmptyClipboard(); - if (!SetClipboardData(CF_UNICODETEXT, hMem)) { - result = -2; - } - CloseClipboard(); - return result; -} - -int32_t get_clipboard(wchar_t *buffer, int32_t size) { - int32_t result = 1; - if (!OpenClipboard(NULL)) { - return -1; - } - - // Get handle of clipboard object for ANSI text - HANDLE hData = GetClipboardData(CF_UNICODETEXT); - if (!hData) { - result = -2; - }else{ - HGLOBAL hMem = GlobalLock(hData); - if (!hMem) { - result = -3; - }else{ - GlobalUnlock(hMem); - swprintf(buffer, size, L"%s", hMem); - } - } - - CloseClipboard(); - return result; -} - -int32_t set_clipboard_image(wchar_t *path) { - bool result = false; - - Gdiplus::GdiplusStartupInput gdiplusStartupInput; - ULONG_PTR gdiplusToken; - Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); - - Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path); - if (gdibmp) - { - HBITMAP hbitmap; - gdibmp->GetHBITMAP(0, &hbitmap); - if (OpenClipboard(NULL)) - { - EmptyClipboard(); - DIBSECTION ds; - if (GetObject(hbitmap, sizeof(DIBSECTION), &ds)) - { - HDC hdc = GetDC(HWND_DESKTOP); - //create compatible bitmap (get DDB from DIB) - HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT, - ds.dsBm.bmBits, (BITMAPINFO*)&ds.dsBmih, DIB_RGB_COLORS); - ReleaseDC(HWND_DESKTOP, hdc); - SetClipboardData(CF_BITMAP, hbitmap_ddb); - DeleteObject(hbitmap_ddb); - result = true; - } - CloseClipboard(); - } - - //cleanup: - DeleteObject(hbitmap); - delete gdibmp; - } - - Gdiplus::GdiplusShutdown(gdiplusToken); - - return result; -} - -// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard -int32_t set_clipboard_html(char * html, wchar_t * text_fallback) { - // Get clipboard id for HTML format - static int cfid = 0; - if(!cfid) { - cfid = RegisterClipboardFormat(L"HTML Format"); - } - - int32_t result = 0; - const size_t html_len = strlen(html) + 1; - HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char)); - memcpy(GlobalLock(hMem), html, html_len * sizeof(char)); - GlobalUnlock(hMem); - - const size_t fallback_len = wcslen(text_fallback) + 1; - HGLOBAL hMemFallback = GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t)); - memcpy(GlobalLock(hMemFallback), text_fallback, fallback_len * sizeof(wchar_t)); - GlobalUnlock(hMemFallback); - - if (!OpenClipboard(NULL)) { - return -1; - } - EmptyClipboard(); - if (!SetClipboardData(cfid, hMem)) { - result = -2; - } - - if (!SetClipboardData(CF_UNICODETEXT, hMemFallback)) { - result = -3; - } - CloseClipboard(); - GlobalFree(hMem); - return result; -} diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h deleted file mode 100644 index fadcaeb..0000000 --- a/native/libwinbridge/bridge.h +++ /dev/null @@ -1,193 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#ifndef ESPANSO_BRIDGE_H -#define ESPANSO_BRIDGE_H - -#include -#include - -// SYSTEM - -extern "C" int32_t start_daemon_process(); - -extern void * manager_instance; - -/* - * Initialize the Windows parameters - * return: 1 if OK, -1 otherwise. - */ -extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t show_icon); - -#define LEFT_VARIANT 1 -#define RIGHT_VARIANT 2 - -/* - * Called when a new keypress is made, the first argument is an int array, - * while the second is the size of the array. - */ -typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t variant, int32_t is_key_down); -extern KeypressCallback keypress_callback; - -/* - * Register the callback that will be called when a keypress was made - */ -extern "C" void register_keypress_callback(KeypressCallback callback); - -/* - * Start the event loop indefinitely. Blocking call. - */ -extern "C" void eventloop(); - -// Keyboard Manager - -/* - * Type the given string by simulating Key Presses - */ -extern "C" void send_string(const wchar_t * string); - -/* - * Send the given Virtual Key press - */ -extern "C" void send_vkey(int32_t vk); - -/* - * Send the given Virtual Key press multiple times - */ -extern "C" void send_multi_vkey(int32_t vk, int32_t count); - -/* - * Send the given Virtual Key press multiple times adding a delay between each keypress - */ -extern "C" void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay); - -/* - * Send the backspace keypress, *count* times. - */ -extern "C" void delete_string(int32_t count, int32_t delay); - -/* - * Send the Paste keyboard shortcut (CTRL+V) - */ -extern "C" void trigger_paste(); - -/* - * Send the Paste keyboard shortcut (CTRL+SHIFT+V) - */ -extern "C" void trigger_shift_paste(); - -/* - * Send the copy keyboard shortcut (CTRL+C) - */ -extern "C" void trigger_copy(); - -/* - * Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed - */ -extern "C" int32_t are_modifiers_pressed(); - -// Detect current application commands - -/* - * Return the active windows's title - */ -extern "C" int32_t get_active_window_name(wchar_t * buffer, int32_t size); - -/* - * Return the active windows's executable path - */ -extern "C" int32_t get_active_window_executable(wchar_t * buffer, int32_t size); - -// UI - -/* - * Called when the tray icon is clicked - */ -typedef void (*IconClickCallback)(void * self); -extern IconClickCallback icon_click_callback; -extern "C" void register_icon_click_callback(IconClickCallback callback); - -// CONTEXT MENU - -typedef struct { - int32_t id; - int32_t type; - wchar_t name[100]; -} MenuItem; - -extern "C" int32_t show_context_menu(MenuItem * items, int32_t count); - -/* - * Called when the context menu is clicked - */ -typedef void (*ContextMenuClickCallback)(void * self, int32_t id); -extern ContextMenuClickCallback context_menu_click_callback; -extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback); - -/* - * Hide the tray icon - */ -extern "C" void cleanup_ui(); - -// NOTIFICATION - -/* - * Show a window containing the notification. - */ -extern "C" int32_t show_notification(wchar_t * message); - -/* - * Close the notification if present - */ -extern "C" void close_notification(); - -/* - * Update the tray icon status - */ -extern "C" void update_tray_icon(int32_t enabled); - -// CLIPBOARD - -/* - * Return the clipboard text - */ -extern "C" int32_t get_clipboard(wchar_t * buffer, int32_t size); - -/* - * Set the clipboard text - */ -extern "C" int32_t set_clipboard(wchar_t * text); - -/* - * Set the clipboard image to the given path - */ -extern "C" int32_t set_clipboard_image(wchar_t * path); - -/* - * Set clipboard HTML. Notice how in this case, text is not a wide char but instead - * uses the UTF8 encoding. - * Also set the text fallback, in case some applications don't support HTML clipboard. - */ -extern "C" int32_t set_clipboard_html(char * html, wchar_t * text_fallback); - -// PROCESSES - -extern "C" int32_t start_process(wchar_t * cmd); - -#endif //ESPANSO_BRIDGE_H \ No newline at end of file diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj deleted file mode 100644 index a3fa5be..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj +++ /dev/null @@ -1,312 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - B6F9DF16232283F8005233EB /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = B6F9DF15232283F8005233EB /* AppDelegate.m */; }; - B6F9DF18232283F8005233EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B6F9DF17232283F8005233EB /* Assets.xcassets */; }; - B6F9DF1E232283F8005233EB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = B6F9DF1D232283F8005233EB /* main.m */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EspansoNotifyHelper.app; sourceTree = BUILT_PRODUCTS_DIR; }; - B6F9DF14232283F8005233EB /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - B6F9DF15232283F8005233EB /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - B6F9DF17232283F8005233EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - B6F9DF1C232283F8005233EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B6F9DF1D232283F8005233EB /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - B6F9DF1F232283F8005233EB /* EspansoNotifyHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EspansoNotifyHelper.entitlements; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B6F9DF0E232283F8005233EB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B6F9DF08232283F8005233EB = { - isa = PBXGroup; - children = ( - B6F9DF13232283F8005233EB /* EspansoNotifyHelper */, - B6F9DF12232283F8005233EB /* Products */, - ); - sourceTree = ""; - }; - B6F9DF12232283F8005233EB /* Products */ = { - isa = PBXGroup; - children = ( - B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */, - ); - name = Products; - sourceTree = ""; - }; - B6F9DF13232283F8005233EB /* EspansoNotifyHelper */ = { - isa = PBXGroup; - children = ( - B6F9DF14232283F8005233EB /* AppDelegate.h */, - B6F9DF15232283F8005233EB /* AppDelegate.m */, - B6F9DF17232283F8005233EB /* Assets.xcassets */, - B6F9DF1C232283F8005233EB /* Info.plist */, - B6F9DF1D232283F8005233EB /* main.m */, - B6F9DF1F232283F8005233EB /* EspansoNotifyHelper.entitlements */, - ); - path = EspansoNotifyHelper; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - B6F9DF10232283F8005233EB /* EspansoNotifyHelper */ = { - isa = PBXNativeTarget; - buildConfigurationList = B6F9DF22232283F8005233EB /* Build configuration list for PBXNativeTarget "EspansoNotifyHelper" */; - buildPhases = ( - B6F9DF0D232283F8005233EB /* Sources */, - B6F9DF0E232283F8005233EB /* Frameworks */, - B6F9DF0F232283F8005233EB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = EspansoNotifyHelper; - productName = EspansoNotifyHelper; - productReference = B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B6F9DF09232283F8005233EB /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1010; - ORGANIZATIONNAME = "Federico Terzi"; - TargetAttributes = { - B6F9DF10232283F8005233EB = { - CreatedOnToolsVersion = 10.1; - }; - }; - }; - buildConfigurationList = B6F9DF0C232283F8005233EB /* Build configuration list for PBXProject "EspansoNotifyHelper" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = B6F9DF08232283F8005233EB; - productRefGroup = B6F9DF12232283F8005233EB /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B6F9DF10232283F8005233EB /* EspansoNotifyHelper */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - B6F9DF0F232283F8005233EB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6F9DF18232283F8005233EB /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - B6F9DF0D232283F8005233EB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6F9DF1E232283F8005233EB /* main.m in Sources */, - B6F9DF16232283F8005233EB /* AppDelegate.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - B6F9DF20232283F8005233EB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Mac Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - }; - name = Debug; - }; - B6F9DF21232283F8005233EB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Mac Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - }; - name = Release; - }; - B6F9DF23232283F8005233EB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = EspansoNotifyHelper/EspansoNotifyHelper.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = N69XJWRM3X; - INFOPLIST_FILE = EspansoNotifyHelper/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.federicoterzi.EspansoNotifyHelper; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - B6F9DF24232283F8005233EB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = EspansoNotifyHelper/EspansoNotifyHelper.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = N69XJWRM3X; - INFOPLIST_FILE = EspansoNotifyHelper/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.federicoterzi.EspansoNotifyHelper; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B6F9DF0C232283F8005233EB /* Build configuration list for PBXProject "EspansoNotifyHelper" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B6F9DF20232283F8005233EB /* Debug */, - B6F9DF21232283F8005233EB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B6F9DF22232283F8005233EB /* Build configuration list for PBXNativeTarget "EspansoNotifyHelper" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B6F9DF23232283F8005233EB /* Debug */, - B6F9DF24232283F8005233EB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B6F9DF09232283F8005233EB /* Project object */; -} diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index b73ee34..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 84373c4..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme deleted file mode 100644 index 69a385d..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist b/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 396e625..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - SchemeUserState - - EspansoNotifyHelper.xcscheme_^#shared#^_ - - orderHint - 0 - - - SuppressBuildableAutocreation - - B6F9DF10232283F8005233EB - - primary - - - - - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h b/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h deleted file mode 100644 index dbe09df..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#import - -@interface AppDelegate : NSObject - - -@end - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m b/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m deleted file mode 100644 index b076bcb..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#import "AppDelegate.h" - -@interface AppDelegate () - -@property (weak) IBOutlet NSWindow *window; -@end - -@implementation AppDelegate - -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self]; - - NSArray *args = [[NSProcessInfo processInfo] arguments]; - - NSString *title = @"Title"; - NSString *desc = @"Description"; - double delay = 1.5; - - if ([args count] > 3) { - title = args[1]; - desc = args[2]; - delay = [args[3] doubleValue]; - } - - NSUserNotification *notification = [[NSUserNotification alloc] init]; - notification.title = title; - notification.informativeText = desc; - notification.soundName = nil; - - [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; - - [[NSUserNotificationCenter defaultUserNotificationCenter] performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:delay]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - NSRunningApplication *app = [NSRunningApplication currentApplication]; - [app terminate]; - }); -} - - -- (void)applicationWillTerminate:(NSNotification *)aNotification { - // Insert code here to tear down your application -} - -- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification{ - return YES; -} - - -@end diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index ce3f357..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "icongreen-16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "icongreen-32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "icongreen-32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "icongreen-64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "icongreen-128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "icongreen-256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "icongreen-256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "icongreen-512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "icongreen-512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "icongreen-1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png deleted file mode 100644 index 3f2555a..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png deleted file mode 100644 index 181c5e8..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png deleted file mode 100644 index 355addc..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png deleted file mode 100644 index 7423ef9..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png deleted file mode 100644 index 520331d..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png deleted file mode 100644 index 4bf1365..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png deleted file mode 100644 index e5809ef..0000000 Binary files a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png and /dev/null differ diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json b/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements b/other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements deleted file mode 100644 index f2ef3ae..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist b/other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist deleted file mode 100644 index 6b44964..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist +++ /dev/null @@ -1,36 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSApplicationCategoryType - public.app-category.utilities - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - Copyright © 2019 Federico Terzi. All rights reserved. - NSMainNibFile - MainMenu - LSUIElement - - NSPrincipalClass - NSApplication - - diff --git a/other/EspansoNotifyHelper/EspansoNotifyHelper/main.m b/other/EspansoNotifyHelper/EspansoNotifyHelper/main.m deleted file mode 100644 index 68731bb..0000000 --- a/other/EspansoNotifyHelper/EspansoNotifyHelper/main.m +++ /dev/null @@ -1,30 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#import -#import "AppDelegate.h" - -int main(int argc, const char * argv[]) { - AppDelegate *delegate = [[AppDelegate alloc] init]; - NSApplication * application = [NSApplication sharedApplication]; - [application setDelegate:delegate]; - [NSApp run]; - - return 0; -} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6f2e075 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 \ No newline at end of file diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs deleted file mode 100644 index 74562ea..0000000 --- a/src/bridge/linux.rs +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::os::raw::{c_char, c_void}; - -#[allow(improper_ctypes)] -#[link(name = "linuxbridge", kind = "static")] -extern "C" { - pub fn check_x11() -> i32; - pub fn initialize(s: *const c_void) -> i32; - pub fn eventloop(); - pub fn cleanup(); - - // System - pub fn get_active_window_name(buffer: *mut c_char, size: i32) -> i32; - pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32; - pub fn get_active_window_executable(buffer: *mut c_char, size: i32) -> i32; - pub fn is_current_window_special() -> i32; - pub fn register_error_callback( - cb: extern "C" fn( - _self: *mut c_void, - error_code: c_char, - request_code: c_char, - minor_code: c_char, - ), - ); - - // Keyboard - pub fn register_keypress_callback( - cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32), - ); - - pub fn send_string(string: *const c_char); - pub fn delete_string(count: i32); - pub fn left_arrow(count: i32); - pub fn send_enter(); - pub fn trigger_paste(); - pub fn trigger_terminal_paste(); - pub fn trigger_shift_ins_paste(); - pub fn trigger_alt_shift_ins_paste(); - pub fn trigger_ctrl_alt_paste(); - pub fn trigger_copy(); - - pub fn fast_send_string(string: *const c_char, delay: i32); - pub fn fast_delete_string(count: i32, delay: i32); - pub fn fast_left_arrow(count: i32); - pub fn fast_send_enter(); -} diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs deleted file mode 100644 index fd6dc44..0000000 --- a/src/bridge/macos.rs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::os::raw::{c_char, c_void}; - -#[repr(C)] -pub struct MacMenuItem { - pub item_id: i32, - pub item_type: i32, - pub item_name: [c_char; 100], -} - -#[allow(improper_ctypes)] -#[link(name = "macbridge", kind = "static")] -extern "C" { - pub fn initialize( - s: *const c_void, - icon_path: *const c_char, - disabled_icon_path: *const c_char, - show_icon: i32, - ); - pub fn eventloop(); - pub fn headless_eventloop(); - - // System - pub fn check_accessibility() -> i32; - pub fn prompt_accessibility() -> i32; - pub fn open_settings_panel(); - pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32; - pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32; - pub fn get_secure_input_process(pid: *mut i64) -> i32; - pub fn get_path_from_pid(pid: i64, buffer: *mut c_char, size: i32) -> i32; - - // Clipboard - pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32; - pub fn set_clipboard(text: *const c_char) -> i32; - pub fn set_clipboard_image(path: *const c_char) -> i32; - pub fn set_clipboard_html(html: *const c_char, text_fallback: *const c_char) -> i32; - - // UI - pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void)); - pub fn show_context_menu(items: *const MacMenuItem, count: i32) -> i32; - pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32)); - pub fn update_tray_icon(enabled: i32); - - // Keyboard - pub fn register_keypress_callback( - cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32), - ); - - pub fn send_string(string: *const c_char); - pub fn send_vkey(vk: i32); - pub fn send_multi_vkey(vk: i32, count: i32); - pub fn delete_string(count: i32); - pub fn trigger_paste(); - pub fn trigger_copy(); - pub fn are_modifiers_pressed() -> i32; -} diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs deleted file mode 100644 index 470dbfd..0000000 --- a/src/bridge/windows.rs +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::os::raw::{c_char, c_void}; - -#[repr(C)] -pub struct WindowsMenuItem { - pub item_id: i32, - pub item_type: i32, - pub item_name: [u16; 100], -} - -#[allow(improper_ctypes)] -#[link(name = "winbridge", kind = "static")] -extern "C" { - pub fn start_daemon_process() -> i32; - pub fn initialize( - s: *const c_void, - ico_path: *const u16, - red_ico_path: *const u16, - bmp_path: *const u16, - show_icon: i32, - ) -> i32; - - // SYSTEM - pub fn get_active_window_name(buffer: *mut u16, size: i32) -> i32; - pub fn get_active_window_executable(buffer: *mut u16, size: i32) -> i32; - - // UI - pub fn show_notification(message: *const u16) -> i32; - pub fn close_notification(); - pub fn show_context_menu(items: *const WindowsMenuItem, count: i32) -> i32; - pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void)); - pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32)); - pub fn cleanup_ui(); - pub fn update_tray_icon(enabled: i32); - - // CLIPBOARD - pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32; - pub fn set_clipboard(payload: *const u16) -> i32; - pub fn set_clipboard_image(path: *const u16) -> i32; - pub fn set_clipboard_html(html: *const c_char, text_fallback: *const u16) -> i32; - - // KEYBOARD - pub fn register_keypress_callback( - cb: extern "C" fn(_self: *mut c_void, *const u16, i32, i32, i32, i32, i32), - ); - - pub fn eventloop(); - pub fn send_string(string: *const u16); - pub fn send_vkey(vk: i32); - pub fn send_multi_vkey(vk: i32, count: i32); - pub fn delete_string(count: i32, delay: i32); - pub fn trigger_paste(); - pub fn trigger_shift_paste(); - pub fn trigger_copy(); - pub fn are_modifiers_pressed() -> i32; - - // PROCESSES - - pub fn start_process(cmd: *const u16) -> i32; -} diff --git a/src/check.rs b/src/check.rs deleted file mode 100644 index 414efbc..0000000 --- a/src/check.rs +++ /dev/null @@ -1,71 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -// This functions are used to check if the required dependencies and conditions are satisfied -// before starting espanso - -#[cfg(target_os = "linux")] -pub fn check_preconditions() -> bool { - use std::process::Command; - - let mut result = true; - - // Make sure notify-send is installed - let status = Command::new("notify-send").arg("-v").output(); - if status.is_err() { - println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it."); - result = false; - } - - // Make sure xclip is installed - let status = Command::new("xclip").arg("-version").output(); - if status.is_err() { - println!( - "Error: 'xclip' command is needed for espanso to work correctly, please install it." - ); - result = false; - } - - result -} - -#[cfg(target_os = "macos")] -pub fn check_preconditions() -> bool { - // Make sure no app is currently using secure input. - let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application(); - - if let Some((app_name, process)) = secure_input_app { - eprintln!("WARNING: An application is currently using SecureInput and might prevent espanso from working correctly."); - eprintln!(); - eprintln!("APP: {}", app_name); - eprintln!("PROC: {}", process); - eprintln!(); - eprintln!("Please close it or disable SecureInput for that application (most apps that use it have a"); - eprintln!("setting to disable it)."); - eprintln!("Until then, espanso might not work as expected."); - } - - true -} - -#[cfg(target_os = "windows")] -pub fn check_preconditions() -> bool { - // Nothing needed on windows - true -} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index f2fc91b..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::ConfigSet; -use crate::matcher::{Match, MatchContentType}; -use serde::Serialize; - -pub fn list_matches(config_set: ConfigSet, onlytriggers: bool, preserve_newlines: bool) { - let matches = filter_matches(config_set); - - for m in matches { - for trigger in m.triggers.iter() { - if onlytriggers { - println!("{}", trigger); - } else { - match m.content { - MatchContentType::Text(ref text) => { - let replace = if preserve_newlines { - text.replace.to_owned() - } else { - text.replace.replace("\n", " ") - }; - println!("{} - {}", trigger, replace) - } - MatchContentType::Image(_) => { - // Skip image matches for now - } - } - } - } - } -} - -#[derive(Debug, Serialize)] -struct JsonMatchEntry { - triggers: Vec, - replace: String, -} - -pub fn list_matches_as_json(config_set: ConfigSet) { - let matches = filter_matches(config_set); - - let mut entries = Vec::new(); - - for m in matches { - match m.content { - MatchContentType::Text(ref text) => entries.push(JsonMatchEntry { - triggers: m.triggers, - replace: text.replace.clone(), - }), - MatchContentType::Image(_) => { - // Skip image matches for now - } - } - } - - let output = serde_json::to_string(&entries); - - println!("{}", output.unwrap_or_default()) -} - -fn filter_matches(config_set: ConfigSet) -> Vec { - let mut output = Vec::new(); - output.extend(config_set.default.matches); - - // TODO: consider specific matches by class, title or exe path - // for specific in config_set.specific { - // output.extend(specific.matches) - // } - output -} diff --git a/src/clipboard/linux.rs b/src/clipboard/linux.rs deleted file mode 100644 index d051d02..0000000 --- a/src/clipboard/linux.rs +++ /dev/null @@ -1,123 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use log::error; -use std::io::Write; -use std::path::Path; -use std::process::{Command, Stdio}; - -pub struct LinuxClipboardManager {} - -impl super::ClipboardManager for LinuxClipboardManager { - fn get_clipboard(&self) -> Option { - let res = Command::new("xclip").args(&["-o", "-sel", "clip"]).output(); - - if let Ok(output) = res { - if output.status.success() { - let s = String::from_utf8_lossy(&output.stdout); - return Some((*s).to_owned()); - } - } - - None - } - - fn set_clipboard(&self, payload: &str) { - let res = Command::new("xclip") - .args(&["-sel", "clip"]) - .stdin(Stdio::piped()) - .spawn(); - - if let Ok(mut child) = res { - let stdin = child.stdin.as_mut(); - - if let Some(output) = stdin { - let res = output.write_all(payload.as_bytes()); - - if let Err(e) = res { - error!("Could not set clipboard: {}", e); - } - - let res = child.wait(); - - if let Err(e) = res { - error!("Could not set clipboard: {}", e); - } - } - } - } - - fn set_clipboard_image(&self, image_path: &Path) { - let extension = image_path.extension(); - let mime = match extension { - Some(ext) => { - let ext = ext.to_string_lossy().to_lowercase(); - match ext.as_ref() { - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "svg" => "image/svg", - _ => "image/png", - } - } - None => "image/png", - }; - - let image_path = image_path.to_string_lossy().into_owned(); - - let res = Command::new("xclip") - .args(&["-selection", "clipboard", "-t", mime, "-i", &image_path]) - .spawn(); - - if let Err(e) = res { - error!("Could not set image clipboard: {}", e); - } - } - - fn set_clipboard_html(&self, html: &str) { - let res = Command::new("xclip") - .args(&["-sel", "clip", "-t", "text/html"]) - .stdin(Stdio::piped()) - .spawn(); - - if let Ok(mut child) = res { - let stdin = child.stdin.as_mut(); - - if let Some(output) = stdin { - let res = output.write_all(html.as_bytes()); - - if let Err(e) = res { - error!("Could not set clipboard html: {}", e); - } - - let res = child.wait(); - - if let Err(e) = res { - error!("Could not set clipboard html: {}", e); - } - } - } - } -} - -impl LinuxClipboardManager { - pub fn new() -> LinuxClipboardManager { - LinuxClipboardManager {} - } -} diff --git a/src/clipboard/macos.rs b/src/clipboard/macos.rs deleted file mode 100644 index a6ef7e5..0000000 --- a/src/clipboard/macos.rs +++ /dev/null @@ -1,86 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::macos::*; -use log::{error, warn}; -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use std::path::Path; - -pub struct MacClipboardManager {} - -impl super::ClipboardManager for MacClipboardManager { - fn get_clipboard(&self) -> Option { - unsafe { - let mut buffer: [c_char; 2000] = [0; 2000]; - let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - - let string = c_string.to_str(); - if let Ok(string) = string { - return Some((*string).to_owned()); - } - } - } - - None - } - - fn set_clipboard(&self, payload: &str) { - let res = CString::new(payload); - if let Ok(cstr) = res { - unsafe { - set_clipboard(cstr.as_ptr()); - } - } - } - - fn set_clipboard_image(&self, image_path: &Path) { - let path_string = image_path.to_string_lossy().into_owned(); - let res = CString::new(path_string); - if let Ok(path) = res { - unsafe { - let result = set_clipboard_image(path.as_ptr()); - if result != 1 { - warn!("Couldn't set clipboard for image: {:?}", image_path) - } - } - } - } - - fn set_clipboard_html(&self, html: &str) { - // Render the text fallback for those applications that don't support HTML clipboard - let decorator = html2text::render::text_renderer::TrivialDecorator::new(); - let text_fallback = - html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator); - unsafe { - let payload_c = CString::new(html).expect("unable to create CString for html content"); - let payload_fallback_c = CString::new(text_fallback).unwrap(); - set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr()); - } - } -} - -impl MacClipboardManager { - pub fn new() -> MacClipboardManager { - MacClipboardManager {} - } -} diff --git a/src/clipboard/mod.rs b/src/clipboard/mod.rs deleted file mode 100644 index a282c89..0000000 --- a/src/clipboard/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::path::Path; - -#[cfg(target_os = "windows")] -mod windows; - -#[cfg(target_os = "linux")] -mod linux; - -#[cfg(target_os = "macos")] -mod macos; - -pub trait ClipboardManager { - fn get_clipboard(&self) -> Option; - fn set_clipboard(&self, payload: &str); - fn set_clipboard_image(&self, image_path: &Path); - fn set_clipboard_html(&self, html: &str); -} - -// LINUX IMPLEMENTATION -#[cfg(target_os = "linux")] -pub fn get_manager() -> impl ClipboardManager { - linux::LinuxClipboardManager::new() -} - -// WINDOWS IMPLEMENTATION -#[cfg(target_os = "windows")] -pub fn get_manager() -> impl ClipboardManager { - windows::WindowsClipboardManager::new() -} - -// MAC IMPLEMENTATION -#[cfg(target_os = "macos")] -pub fn get_manager() -> impl ClipboardManager { - macos::MacClipboardManager::new() -} diff --git a/src/clipboard/windows.rs b/src/clipboard/windows.rs deleted file mode 100644 index 2341bcf..0000000 --- a/src/clipboard/windows.rs +++ /dev/null @@ -1,119 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::windows::{ - get_clipboard, set_clipboard, set_clipboard_html, set_clipboard_image, -}; -use std::{ffi::CString, path::Path}; -use widestring::U16CString; - -pub struct WindowsClipboardManager {} - -impl WindowsClipboardManager { - pub fn new() -> WindowsClipboardManager { - WindowsClipboardManager {} - } -} - -impl super::ClipboardManager for WindowsClipboardManager { - fn get_clipboard(&self) -> Option { - unsafe { - let mut buffer: [u16; 2000] = [0; 2000]; - let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = U16CString::from_ptr_str(buffer.as_ptr()); - - let string = c_string.to_string_lossy(); - return Some((*string).to_owned()); - } - } - - None - } - - fn set_clipboard(&self, payload: &str) { - unsafe { - let payload_c = U16CString::from_str(payload).unwrap(); - set_clipboard(payload_c.as_ptr()); - } - } - - fn set_clipboard_image(&self, image_path: &Path) { - let path_string = image_path.to_string_lossy().into_owned(); - unsafe { - let payload_c = U16CString::from_str(path_string).unwrap(); - set_clipboard_image(payload_c.as_ptr()); - } - } - - fn set_clipboard_html(&self, html: &str) { - // In order to set the HTML clipboard, we have to create a prefix with a specific format - // For more information, look here: - // https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format - // https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard - let mut tokens = Vec::new(); - tokens.push("Version:0.9"); - tokens.push("StartHTML:<"); - tokens.push("EndHTML:<"); - tokens.push("StartFragment:<"); - tokens.push("EndFragment:<"); - tokens.push(""); - tokens.push(""); - let content = format!("{}", html); - tokens.push(&content); - tokens.push(""); - tokens.push(""); - - let mut render = tokens.join("\r\n"); - - // Now replace the placeholders with the actual positions - render = render.replace( - "<", - &format!("{:0>8}", render.find("").unwrap_or_default()), - ); - render = render.replace("<", &format!("{:0>8}", render.len())); - render = render.replace( - "<", - &format!( - "{:0>8}", - render.find("").unwrap_or_default() - + "".len() - ), - ); - render = render.replace( - "<", - &format!( - "{:0>8}", - render.find("").unwrap_or_default() - ), - ); - - // Render the text fallback for those applications that don't support HTML clipboard - let decorator = html2text::render::text_renderer::TrivialDecorator::new(); - let text_fallback = - html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator); - unsafe { - let payload_c = - CString::new(render).expect("unable to create CString for html content"); - let payload_fallback_c = U16CString::from_str(text_fallback).unwrap(); - set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr()); - } - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index 7e4d1c0..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,1937 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -extern crate dirs; - -use crate::event::KeyModifier; -use crate::keyboard::PasteShortcut; -use crate::matcher::{Match, MatchVariable}; -use log::error; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::error::Error; -use std::fmt; -use std::fs; -use std::fs::{create_dir_all, File}; -use std::io::Read; -use std::path::{Path, PathBuf}; -use walkdir::{DirEntry, WalkDir}; - -pub(crate) mod runtime; - -const DEFAULT_CONFIG_FILE_CONTENT: &str = include_str!("../res/config.yml"); - -pub const DEFAULT_CONFIG_FILE_NAME: &str = "default.yml"; -pub const USER_CONFIGS_FOLDER_NAME: &str = "user"; - -// Default values for primitives -fn default_name() -> String { - "default".to_owned() -} -fn default_parent() -> String { - "self".to_owned() -} -fn default_filter_title() -> String { - "".to_owned() -} -fn default_filter_class() -> String { - "".to_owned() -} -fn default_filter_exec() -> String { - "".to_owned() -} -fn default_log_level() -> i32 { - 0 -} -fn default_conflict_check() -> bool { - false -} -fn default_ipc_server_port() -> i32 { - 34982 -} -fn default_worker_ipc_server_port() -> i32 { - 34983 -} -fn default_use_system_agent() -> bool { - true -} -fn default_config_caching_interval() -> i32 { - 800 -} -fn default_word_separators() -> Vec { - vec![' ', ',', '.', '?', '!', '\r', '\n', 22u8 as char] -} -fn default_toggle_interval() -> u32 { - 230 -} -fn default_toggle_key() -> KeyModifier { - KeyModifier::ALT -} -fn default_preserve_clipboard() -> bool { - true -} -fn default_passive_match_regex() -> String { - "(?P:\\p{L}+)(/(?P.*)/)?".to_owned() -} -fn default_passive_arg_delimiter() -> char { - '/' -} -fn default_passive_arg_escape() -> char { - '\\' -} -fn default_passive_delay() -> u64 { - 100 -} -fn default_passive_key() -> KeyModifier { - KeyModifier::OFF -} -fn default_enable_passive() -> bool { - false -} -fn default_enable_active() -> bool { - true -} -fn default_backspace_limit() -> i32 { - 3 -} -fn default_backspace_delay() -> i32 { - 0 -} -fn default_inject_delay() -> i32 { - 0 -} -fn default_restore_clipboard_delay() -> i32 { - 300 -} -fn default_exclude_default_entries() -> bool { - false -} -fn default_secure_input_watcher_enabled() -> bool { - true -} -fn default_secure_input_notification() -> bool { - true -} -fn default_show_notifications() -> bool { - true -} -fn default_auto_restart() -> bool { - true -} -fn default_undo_backspace() -> bool { - true -} -fn default_show_icon() -> bool { - true -} -fn default_fast_inject() -> bool { - true -} -fn default_secure_input_watcher_interval() -> i32 { - 5000 -} -fn default_matches() -> Vec { - Vec::new() -} -fn default_global_vars() -> Vec { - Vec::new() -} -fn default_modulo_path() -> Option { - None -} -fn default_post_inject_delay() -> u64 { - 100 -} - -fn default_wait_for_modifiers_release() -> bool { - false -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Configs { - #[serde(default = "default_name")] - pub name: String, - - #[serde(default = "default_parent")] - pub parent: String, - - #[serde(default = "default_filter_title")] - pub filter_title: String, - - #[serde(default = "default_filter_class")] - pub filter_class: String, - - #[serde(default = "default_filter_exec")] - pub filter_exec: String, - - #[serde(default = "default_log_level")] - pub log_level: i32, - - #[serde(default = "default_conflict_check")] - pub conflict_check: bool, - - #[serde(default = "default_ipc_server_port")] - pub ipc_server_port: i32, - - #[serde(default = "default_worker_ipc_server_port")] - pub worker_ipc_server_port: i32, - - #[serde(default = "default_use_system_agent")] - pub use_system_agent: bool, - - #[serde(default = "default_config_caching_interval")] - pub config_caching_interval: i32, - - #[serde(default = "default_word_separators")] - pub word_separators: Vec, // TODO: add parsing test - - #[serde(default = "default_toggle_key")] - pub toggle_key: KeyModifier, - - #[serde(default = "default_toggle_interval")] - pub toggle_interval: u32, - - #[serde(default = "default_preserve_clipboard")] - pub preserve_clipboard: bool, - - #[serde(default = "default_passive_match_regex")] - pub passive_match_regex: String, - - #[serde(default = "default_passive_arg_delimiter")] - pub passive_arg_delimiter: char, - - #[serde(default = "default_passive_arg_escape")] - pub passive_arg_escape: char, - - #[serde(default = "default_passive_key")] - pub passive_key: KeyModifier, - - #[serde(default = "default_passive_delay")] - pub passive_delay: u64, - - #[serde(default = "default_enable_passive")] - pub enable_passive: bool, - - #[serde(default = "default_enable_active")] - pub enable_active: bool, - - #[serde(default = "default_undo_backspace")] - pub undo_backspace: bool, - - #[serde(default)] - pub paste_shortcut: PasteShortcut, - - #[serde(default = "default_backspace_limit")] - pub backspace_limit: i32, - - #[serde(default = "default_restore_clipboard_delay")] - pub restore_clipboard_delay: i32, - - #[serde(default = "default_secure_input_watcher_enabled")] - pub secure_input_watcher_enabled: bool, - - #[serde(default = "default_secure_input_watcher_interval")] - pub secure_input_watcher_interval: i32, - - #[serde(default = "default_post_inject_delay")] - pub post_inject_delay: u64, - - #[serde(default = "default_secure_input_notification")] - pub secure_input_notification: bool, - - #[serde(default)] - pub backend: BackendType, - - #[serde(default = "default_exclude_default_entries")] - pub exclude_default_entries: bool, - - #[serde(default = "default_show_notifications")] - pub show_notifications: bool, - - #[serde(default = "default_show_icon")] - pub show_icon: bool, - - #[serde(default = "default_fast_inject")] - pub fast_inject: bool, - - #[serde(default = "default_backspace_delay")] - pub backspace_delay: i32, - - #[serde(default = "default_inject_delay")] - pub inject_delay: i32, - - #[serde(default = "default_auto_restart")] - pub auto_restart: bool, - - #[serde(default = "default_matches")] - pub matches: Vec, - - #[serde(default = "default_global_vars")] - pub global_vars: Vec, - - #[serde(default = "default_modulo_path")] - pub modulo_path: Option, - - #[serde(default = "default_wait_for_modifiers_release")] - pub wait_for_modifiers_release: bool, -} - -// Macro used to validate config fields -#[macro_export] -macro_rules! validate_field { - ($result:expr, $field:expr, $def_value:expr) => { - if $field != $def_value { - let mut field_name = stringify!($field); - if field_name.starts_with("self.") { - field_name = &field_name[5..]; // Remove the 'self.' prefix - } - error!("Validation error, parameter '{}' is reserved and can be only used in the default.yml config file", field_name); - $result = false; - } - }; -} - -impl Configs { - /* - * Validate the Config instance. - * It makes sure that user defined config instances do not define - * attributes reserved to the default config. - */ - fn validate_user_defined_config(&self) -> bool { - let mut result = true; - - validate_field!( - result, - self.config_caching_interval, - default_config_caching_interval() - ); - validate_field!(result, self.log_level, default_log_level()); - validate_field!(result, self.conflict_check, default_conflict_check()); - validate_field!(result, self.toggle_key, default_toggle_key()); - validate_field!(result, self.toggle_interval, default_toggle_interval()); - validate_field!(result, self.backspace_limit, default_backspace_limit()); - validate_field!(result, self.ipc_server_port, default_ipc_server_port()); - validate_field!(result, self.use_system_agent, default_use_system_agent()); - validate_field!( - result, - self.preserve_clipboard, - default_preserve_clipboard() - ); - validate_field!( - result, - self.passive_match_regex, - default_passive_match_regex() - ); - validate_field!( - result, - self.passive_arg_delimiter, - default_passive_arg_delimiter() - ); - validate_field!( - result, - self.passive_arg_escape, - default_passive_arg_escape() - ); - validate_field!(result, self.passive_key, default_passive_key()); - validate_field!( - result, - self.restore_clipboard_delay, - default_restore_clipboard_delay() - ); - validate_field!( - result, - self.secure_input_watcher_enabled, - default_secure_input_watcher_enabled() - ); - validate_field!( - result, - self.secure_input_watcher_interval, - default_secure_input_watcher_interval() - ); - validate_field!( - result, - self.secure_input_notification, - default_secure_input_notification() - ); - validate_field!( - result, - self.show_notifications, - default_show_notifications() - ); - validate_field!(result, self.show_icon, default_show_icon()); - - result - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum BackendType { - Inject, - Clipboard, - - // On Linux systems there is a long standing issue with text injection (which - // in general is better than Clipboard copy/pasting) that prevents certain - // apps from correctly handling special characters (such as emojis or accented letters) - // when injected. For this reason, espanso initially defaulted on the Clipboard - // backend on Linux, as it was the most reliable (working in 99% of cases), - // even though it was less efficient and with a few inconveniences (for example, the - // previous clipboard content being overwritten). - // The Auto backend tries to take it a step further, by automatically determining - // when an injection is possible (only ascii characters in the replacement), and falling - // back to the Clipboard backend otherwise. - // Should only be used on Linux systems. - Auto, -} -impl Default for BackendType { - // The default backend varies based on the operating system. - // On Windows and macOS, the Inject backend is working great and should - // be preferred as it doesn't override the clipboard. - // On the other hand, on linux it has many problems due to the bugs - // of the libxdo used. For this reason, Clipboard will be the default - // backend on Linux from version v0.3.0 - - #[cfg(not(target_os = "linux"))] - fn default() -> Self { - BackendType::Inject - } - - #[cfg(target_os = "linux")] - fn default() -> Self { - BackendType::Auto - } -} - -impl Configs { - fn load_config(path: &Path) -> Result { - let file_res = File::open(path); - if let Ok(mut file) = file_res { - let mut contents = String::new(); - let res = file.read_to_string(&mut contents); - - if res.is_err() { - return Err(ConfigLoadError::UnableToReadFile); - } - - let config_res = serde_yaml::from_str(&contents); - - match config_res { - Ok(config) => Ok(config), - Err(e) => Err(ConfigLoadError::InvalidYAML(path.to_owned(), e.to_string())), - } - } else { - eprintln!("Error: Cannot load file {:?}", path); - Err(ConfigLoadError::FileNotFound) - } - } - - fn merge_overwrite(&mut self, new_config: Configs) { - // Merge matches - let mut merged_matches = new_config.matches; - let mut match_trigger_set = HashSet::new(); - merged_matches.iter().for_each(|m| { - match_trigger_set.extend(m.triggers.clone()); - }); - let parent_matches: Vec = self - .matches - .iter() - .filter(|&m| { - !m.triggers - .iter() - .any(|trigger| match_trigger_set.contains(trigger)) - }) - .cloned() - .collect(); - - merged_matches.extend(parent_matches); - self.matches = merged_matches; - - // Merge global variables - let mut merged_global_vars = new_config.global_vars; - let mut vars_name_set = HashSet::new(); - merged_global_vars.iter().for_each(|m| { - vars_name_set.insert(m.name.clone()); - }); - let parent_vars: Vec = self - .global_vars - .iter() - .filter(|&m| !vars_name_set.contains(&m.name)) - .cloned() - .collect(); - - merged_global_vars.extend(parent_vars); - self.global_vars = merged_global_vars; - } - - fn merge_no_overwrite(&mut self, default: &Configs) { - // Merge matches - let mut match_trigger_set = HashSet::new(); - self.matches.iter().for_each(|m| { - match_trigger_set.extend(m.triggers.clone()); - }); - let default_matches: Vec = default - .matches - .iter() - .filter(|&m| { - !m.triggers - .iter() - .any(|trigger| match_trigger_set.contains(trigger)) - }) - .cloned() - .collect(); - - self.matches.extend(default_matches); - - // Merge global variables - let mut vars_name_set = HashSet::new(); - self.global_vars.iter().for_each(|m| { - vars_name_set.insert(m.name.clone()); - }); - let default_vars: Vec = default - .global_vars - .iter() - .filter(|&m| !vars_name_set.contains(&m.name)) - .cloned() - .collect(); - - self.global_vars.extend(default_vars); - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ConfigSet { - pub default: Configs, - pub specific: Vec, -} - -impl ConfigSet { - pub fn load(config_dir: &Path, package_dir: &Path) -> Result { - if !config_dir.is_dir() { - return Err(ConfigLoadError::InvalidConfigDirectory); - } - - // Load default configuration - let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME); - let default = Configs::load_config(default_file.as_path())?; - - // Check that a compatible backend is used, otherwise warn the user - if cfg!(not(target_os = "linux")) && default.backend == BackendType::Auto { - eprintln!("Warning: Using Auto backend is only supported on Linux, falling back to Inject backend."); - } - - // Analyze which config files have to be loaded - - let mut target_files = Vec::new(); - - let specific_dir = config_dir.join(USER_CONFIGS_FOLDER_NAME); - if specific_dir.exists() { - let dir_entry = WalkDir::new(specific_dir); - target_files.extend(dir_entry); - } - - let package_files = if package_dir.exists() { - let dir_entry = WalkDir::new(package_dir); - dir_entry.into_iter().collect() - } else { - vec![] - }; - - // Load the user defined config files - - let mut name_set = HashSet::new(); - let mut children_map: HashMap> = HashMap::new(); - let mut package_map: HashMap> = HashMap::new(); - let mut root_configs = Vec::new(); - root_configs.push(default); - - let mut file_loader = |entry: walkdir::Result, - dest_map: &mut HashMap>| - -> Result<(), ConfigLoadError> { - match entry { - Ok(entry) => { - let path = entry.path(); - - // Skip non-yaml config files - if path - .extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - != "yml" - { - return Ok(()); - } - - // Skip hidden files - if path - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .starts_with(".") - { - return Ok(()); - } - - let mut config = Configs::load_config(&path)?; - - // Make sure the config does not contain reserved fields - if !config.validate_user_defined_config() { - return Err(ConfigLoadError::InvalidParameter(path.to_owned())); - } - - // No name specified, defaulting to the path name - if config.name == "default" { - config.name = path.to_str().unwrap_or_default().to_owned(); - } - - if name_set.contains(&config.name) { - return Err(ConfigLoadError::NameDuplicate(path.to_owned())); - } - - name_set.insert(config.name.clone()); - - if config.parent == "self" { - // No parent, root config - root_configs.push(config); - } else { - // Children config - let children_vec = dest_map.entry(config.parent.clone()).or_default(); - children_vec.push(config); - } - } - Err(e) => { - eprintln!("Warning: Unable to read config file: {}", e); - } - } - - Ok(()) - }; - - // Load the default and user specific configs - for entry in target_files { - file_loader(entry, &mut children_map)?; - } - - // Load the package related configs - for entry in package_files { - file_loader(entry, &mut package_map)?; - } - - // Merge the children config files - let mut configs_without_packages = Vec::new(); - for root_config in root_configs { - let config = ConfigSet::reduce_configs(root_config, &children_map, true); - configs_without_packages.push(config); - } - - // Merge package files - // Note: we need two different steps as the packages have a lower priority - // than configs. - let mut configs = Vec::new(); - for root_config in configs_without_packages { - let config = ConfigSet::reduce_configs(root_config, &package_map, false); - configs.push(config); - } - - // Separate default from specific - let default = configs.get(0).unwrap().clone(); - let mut specific = (&configs[1..]).to_vec().clone(); - - // Add default entries to specific configs when needed - for config in specific.iter_mut() { - if !config.exclude_default_entries { - config.merge_no_overwrite(&default); - } - } - - // Check if some triggers are conflicting with each other - // For more information, see: https://github.com/federico-terzi/espanso/issues/135 - if default.conflict_check { - let has_conflicts = Self::has_conflicts(&default, &specific); - if has_conflicts { - eprintln!("Warning: some triggers had conflicts and may not behave as intended"); - eprintln!( - "To turn off this check, add \"conflict_check: false\" in the configuration" - ); - } - } - - Ok(ConfigSet { default, specific }) - } - - fn reduce_configs( - target: Configs, - children_map: &HashMap>, - higher_priority: bool, - ) -> Configs { - if children_map.contains_key(&target.name) { - let mut target = target; - for children in children_map.get(&target.name).unwrap() { - let children = - Self::reduce_configs(children.clone(), children_map, higher_priority); - if higher_priority { - target.merge_overwrite(children); - } else { - target.merge_no_overwrite(&children); - } - } - target - } else { - target - } - } - - pub fn load_default() -> Result { - // Configuration related - - let config_dir = crate::context::get_config_dir(); - - let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME); - - // If config file does not exist, create one from template - if !default_file.exists() { - let result = fs::write(&default_file, DEFAULT_CONFIG_FILE_CONTENT); - if result.is_err() { - return Err(ConfigLoadError::UnableToCreateDefaultConfig); - } - } - - // Create auxiliary directories - - let user_config_dir = config_dir.join(USER_CONFIGS_FOLDER_NAME); - if !user_config_dir.exists() { - let res = create_dir_all(user_config_dir.as_path()); - if res.is_err() { - return Err(ConfigLoadError::UnableToCreateDefaultConfig); - } - } - - // Packages - - let package_dir = crate::context::get_package_dir(); - let res = create_dir_all(package_dir.as_path()); - if res.is_err() { - return Err(ConfigLoadError::UnableToCreateDefaultConfig); // TODO: change error type - } - - return ConfigSet::load(config_dir.as_path(), package_dir.as_path()); - } - - fn has_conflicts(default: &Configs, specific: &Vec) -> bool { - let mut sorted_triggers: Vec = default - .matches - .iter() - .flat_map(|t| t.triggers.clone()) - .collect(); - sorted_triggers.sort(); - - let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers); - - for s in specific.iter() { - let mut specific_triggers: Vec = - s.matches.iter().flat_map(|t| t.triggers.clone()).collect(); - specific_triggers.sort(); - has_conflicts |= Self::list_has_conflicts(&specific_triggers); - } - - has_conflicts - } - - fn list_has_conflicts(sorted_list: &Vec) -> bool { - if sorted_list.len() <= 1 { - return false; - } - - let mut has_conflicts = false; - - for (i, item) in sorted_list.iter().skip(1).enumerate() { - let previous = &sorted_list[i]; - if item.starts_with(previous) { - has_conflicts = true; - eprintln!( - "Warning: trigger '{}' is conflicting with '{}' and may not behave as intended", - item, previous - ); - } - } - - has_conflicts - } -} - -pub trait ConfigManager<'a> { - fn active_config(&'a self) -> &'a Configs; - fn default_config(&'a self) -> &'a Configs; - fn matches(&'a self) -> &'a Vec; -} - -// Error handling -#[derive(Debug, PartialEq)] -pub enum ConfigLoadError { - FileNotFound, - UnableToReadFile, - InvalidYAML(PathBuf, String), - InvalidConfigDirectory, - InvalidParameter(PathBuf), - NameDuplicate(PathBuf), - UnableToCreateDefaultConfig, -} - -impl fmt::Display for ConfigLoadError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ConfigLoadError::FileNotFound => write!(f, "File not found"), - ConfigLoadError::UnableToReadFile => write!(f, "Unable to read config file"), - ConfigLoadError::InvalidYAML(path, e) => write!(f, "Error parsing YAML file '{}', invalid syntax: {}", path.to_str().unwrap_or_default(), e), - ConfigLoadError::InvalidConfigDirectory => write!(f, "Invalid config directory"), - ConfigLoadError::InvalidParameter(path) => write!(f, "Invalid parameter in '{}', use of reserved parameters in used defined configs is not permitted", path.to_str().unwrap_or_default()), - ConfigLoadError::NameDuplicate(path) => write!(f, "Found duplicate 'name' in '{}', please use different names", path.to_str().unwrap_or_default()), - ConfigLoadError::UnableToCreateDefaultConfig => write!(f, "Could not generate default config file"), - } - } -} - -impl Error for ConfigLoadError { - fn description(&self) -> &str { - match self { - ConfigLoadError::FileNotFound => "File not found", - ConfigLoadError::UnableToReadFile => "Unable to read config file", - ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax", - ConfigLoadError::InvalidConfigDirectory => "Invalid config directory", - ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in user defined configs is not permitted", - ConfigLoadError::NameDuplicate(_) => "Found duplicate 'name' in some configurations, please use different names", - ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file", - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::matcher::MatchContentType; - use std::io::Write; - use tempfile::{NamedTempFile, TempDir}; - - const TEST_WORKING_CONFIG_FILE: &str = include_str!("../res/test/working_config.yml"); - const TEST_CONFIG_FILE_WITH_BAD_YAML: &str = - include_str!("../res/test/config_with_bad_yaml.yml"); - - // Test Configs - - fn create_tmp_file(string: &str) -> NamedTempFile { - let file = NamedTempFile::new().unwrap(); - file.as_file().write_all(string.as_bytes()).unwrap(); - file - } - - fn variant_eq(a: &T, b: &T) -> bool { - std::mem::discriminant(a) == std::mem::discriminant(b) - } - - #[test] - fn test_config_file_not_found() { - let config = Configs::load_config(Path::new("invalid/path")); - assert_eq!(config.is_err(), true); - assert_eq!(config.unwrap_err(), ConfigLoadError::FileNotFound); - } - - #[test] - fn test_config_file_with_bad_yaml_syntax() { - let broken_config_file = create_tmp_file(TEST_CONFIG_FILE_WITH_BAD_YAML); - let config = Configs::load_config(broken_config_file.path()); - match config { - Ok(_) => assert!(false), - Err(e) => { - match e { - ConfigLoadError::InvalidYAML(p, _) => { - assert_eq!(p, broken_config_file.path().to_owned()) - } - _ => assert!(false), - } - assert!(true); - } - } - } - - #[test] - fn test_validate_field_macro() { - let mut result = true; - - validate_field!(result, 3, 3); - assert_eq!(result, true); - - validate_field!(result, 10, 3); - assert_eq!(result, false); - - validate_field!(result, 3, 3); - assert_eq!(result, false); - } - - #[test] - fn test_user_defined_config_does_not_have_reserved_fields() { - let working_config_file = create_tmp_file( - r###" - - backend: Clipboard - - "###, - ); - let config = Configs::load_config(working_config_file.path()); - assert_eq!(config.unwrap().validate_user_defined_config(), true); - } - - #[test] - fn test_user_defined_config_has_reserved_fields_config_caching_interval() { - let working_config_file = create_tmp_file( - r###" - - # This should not happen in an app-specific config - config_caching_interval: 100 - - "###, - ); - let config = Configs::load_config(working_config_file.path()); - assert_eq!(config.unwrap().validate_user_defined_config(), false); - } - - #[test] - fn test_user_defined_config_has_reserved_fields_toggle_key() { - let working_config_file = create_tmp_file( - r###" - - # This should not happen in an app-specific config - toggle_key: CTRL - - "###, - ); - let config = Configs::load_config(working_config_file.path()); - assert_eq!(config.unwrap().validate_user_defined_config(), false); - } - - #[test] - fn test_user_defined_config_has_reserved_fields_toggle_interval() { - let working_config_file = create_tmp_file( - r###" - - # This should not happen in an app-specific config - toggle_interval: 1000 - - "###, - ); - let config = Configs::load_config(working_config_file.path()); - assert_eq!(config.unwrap().validate_user_defined_config(), false); - } - - #[test] - fn test_user_defined_config_has_reserved_fields_backspace_limit() { - let working_config_file = create_tmp_file( - r###" - - # This should not happen in an app-specific config - backspace_limit: 10 - - "###, - ); - let config = Configs::load_config(working_config_file.path()); - assert_eq!(config.unwrap().validate_user_defined_config(), false); - } - - #[test] - fn test_config_loaded_correctly() { - let working_config_file = create_tmp_file(TEST_WORKING_CONFIG_FILE); - let config = Configs::load_config(working_config_file.path()); - assert_eq!(config.is_ok(), true); - } - - // Test ConfigSet - - pub fn create_temp_espanso_directories() -> (TempDir, TempDir) { - create_temp_espanso_directories_with_default_content(DEFAULT_CONFIG_FILE_CONTENT) - } - - pub fn create_temp_espanso_directories_with_default_content( - default_content: &str, - ) -> (TempDir, TempDir) { - let data_dir = TempDir::new().expect("unable to create data directory"); - let package_dir = TempDir::new().expect("unable to create package directory"); - - let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME); - fs::write(default_path, default_content).unwrap(); - - (data_dir, package_dir) - } - - pub fn create_temp_file_in_dir(tmp_dir: &PathBuf, name: &str, content: &str) -> PathBuf { - let user_defined_path = tmp_dir.join(name); - let user_defined_path_copy = user_defined_path.clone(); - fs::write(user_defined_path, content).unwrap(); - - user_defined_path_copy - } - - pub fn create_user_config_file(tmp_dir: &Path, name: &str, content: &str) -> PathBuf { - let user_config_dir = tmp_dir.join(USER_CONFIGS_FOLDER_NAME); - if !user_config_dir.exists() { - create_dir_all(&user_config_dir).unwrap(); - } - - create_temp_file_in_dir(&user_config_dir, name, content) - } - - pub fn create_package_file( - package_data_dir: &Path, - package_name: &str, - filename: &str, - content: &str, - ) -> PathBuf { - let package_dir = package_data_dir.join(package_name); - if !package_dir.exists() { - create_dir_all(&package_dir).unwrap(); - } - - create_temp_file_in_dir(&package_dir, filename, content) - } - - #[test] - fn test_config_set_default_content_should_work_correctly() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - } - - #[test] - fn test_config_set_load_fail_bad_directory() { - let config_set = ConfigSet::load(Path::new("invalid/path"), Path::new("invalid/path")); - assert_eq!(config_set.is_err(), true); - assert_eq!( - config_set.unwrap_err(), - ConfigLoadError::InvalidConfigDirectory - ); - } - - #[test] - fn test_config_set_missing_default_file() { - let data_dir = TempDir::new().expect("unable to create temp directory"); - let package_dir = TempDir::new().expect("unable to create package directory"); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert_eq!(config_set.is_err(), true); - assert_eq!(config_set.unwrap_err(), ConfigLoadError::FileNotFound); - } - - #[test] - fn test_config_set_invalid_yaml_syntax() { - let (data_dir, package_dir) = - create_temp_espanso_directories_with_default_content(TEST_CONFIG_FILE_WITH_BAD_YAML); - let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - match config_set { - Ok(_) => assert!(false), - Err(e) => { - match e { - ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, default_path), - _ => assert!(false), - } - assert!(true); - } - } - } - - #[test] - fn test_config_set_specific_file_with_reserved_fields() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - let user_defined_path = create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - config_caching_interval: 10000 - "###, - ); - let user_defined_path_copy = user_defined_path.clone(); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_err()); - assert_eq!( - config_set.unwrap_err(), - ConfigLoadError::InvalidParameter(user_defined_path_copy) - ) - } - - #[test] - fn test_config_set_specific_file_missing_name_auto_generated() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - let user_defined_path = create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - backend: Clipboard - "###, - ); - let user_defined_path_copy = user_defined_path.clone(); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - assert_eq!( - config_set.unwrap().specific[0].name, - user_defined_path_copy.to_str().unwrap_or_default() - ) - } - - #[test] - fn test_config_set_specific_file_duplicate_name() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: specific1 - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific2.yml", - r###" - name: specific1 - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_err()); - assert!(variant_eq( - &config_set.unwrap_err(), - &ConfigLoadError::NameDuplicate(PathBuf::new()) - )) - } - - #[test] - fn test_user_defined_config_set_merge_with_parent_matches() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ":lol" - replace: "LOL" - - trigger: ":yess" - replace: "Bob" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific1.yml", - r###" - name: specific1 - - matches: - - trigger: "hello" - replace: "newstring" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.default.matches.len(), 2); - assert_eq!(config_set.specific[0].matches.len(), 3); - - assert!(config_set.specific[0] - .matches - .iter() - .find(|x| x.triggers[0] == "hello") - .is_some()); - assert!(config_set.specific[0] - .matches - .iter() - .find(|x| x.triggers[0] == ":lol") - .is_some()); - assert!(config_set.specific[0] - .matches - .iter() - .find(|x| x.triggers[0] == ":yess") - .is_some()); - } - - #[test] - fn test_user_defined_config_set_merge_with_parent_matches_child_priority() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ":lol" - replace: "LOL" - - trigger: ":yess" - replace: "Bob" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific2.yml", - r###" - name: specific1 - - matches: - - trigger: ":lol" - replace: "newstring" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.default.matches.len(), 2); - assert_eq!(config_set.specific[0].matches.len(), 2); - - assert!(config_set.specific[0] - .matches - .iter() - .find(|x| { - if let MatchContentType::Text(content) = &x.content { - x.triggers[0] == ":lol" && content.replace == "newstring" - } else { - false - } - }) - .is_some()); - assert!(config_set.specific[0] - .matches - .iter() - .find(|x| x.triggers[0] == ":yess") - .is_some()); - } - - #[test] - fn test_user_defined_config_set_exclude_merge_with_parent_matches() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ":lol" - replace: "LOL" - - trigger: ":yess" - replace: "Bob" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific2.yml", - r###" - name: specific1 - - exclude_default_entries: true - - matches: - - trigger: "hello" - replace: "newstring" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.default.matches.len(), 2); - assert_eq!(config_set.specific[0].matches.len(), 1); - - assert!(config_set.specific[0] - .matches - .iter() - .find(|x| { - if let MatchContentType::Text(content) = &x.content { - x.triggers[0] == "hello" && content.replace == "newstring" - } else { - false - } - }) - .is_some()); - } - - #[test] - fn test_only_yaml_files_are_loaded_from_config() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ":lol" - replace: "LOL" - - trigger: ":yess" - replace: "Bob" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.zzz", - r###" - name: specific1 - - exclude_default_entries: true - - matches: - - trigger: "hello" - replace: "newstring" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - } - - #[test] - fn test_hidden_files_are_ignored() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ":lol" - replace: "LOL" - - trigger: ":yess" - replace: "Bob" - "###, - ); - - create_user_config_file( - data_dir.path(), - ".specific.yml", - r###" - name: specific1 - - exclude_default_entries: true - - matches: - - trigger: "hello" - replace: "newstring" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - } - - #[test] - fn test_config_set_no_parent_configs_works_correctly() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: specific1 - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific2.yml", - r###" - name: specific2 - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 2); - } - - #[test] - fn test_config_set_default_parent_works_correctly() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - parent: default - - matches: - - trigger: "hello" - replace: "world" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - assert_eq!(config_set.default.matches.len(), 2); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hasta")); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hello")); - } - - #[test] - fn test_config_set_no_parent_should_not_merge() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - matches: - - trigger: "hello" - replace: "world" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 1); - assert_eq!(config_set.default.matches.len(), 1); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hasta")); - assert!(!config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hello")); - assert!(config_set.specific[0] - .matches - .iter() - .any(|m| m.triggers[0] == "hello")); - } - - #[test] - fn test_config_set_default_nested_parent_works_correctly() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: custom1 - parent: default - - matches: - - trigger: "hello" - replace: "world" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific2.yml", - r###" - parent: custom1 - - matches: - - trigger: "super" - replace: "mario" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - assert_eq!(config_set.default.matches.len(), 3); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hasta")); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hello")); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "super")); - } - - #[test] - fn test_config_set_parent_merge_children_priority_should_be_higher() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - parent: default - - matches: - - trigger: "hasta" - replace: "world" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - assert_eq!(config_set.default.matches.len(), 1); - assert!(config_set.default.matches.iter().any(|m| { - if let MatchContentType::Text(content) = &m.content { - m.triggers[0] == "hasta" && content.replace == "world" - } else { - false - } - })); - } - - #[test] - fn test_config_set_package_configs_default_merge() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_package_file( - package_dir.path(), - "package1", - "package.yml", - r###" - parent: default - - matches: - - trigger: "harry" - replace: "potter" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - assert_eq!(config_set.default.matches.len(), 2); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hasta")); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "harry")); - } - - #[test] - fn test_config_set_package_configs_lower_priority_than_user() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_package_file( - package_dir.path(), - "package1", - "package.yml", - r###" - parent: default - - matches: - - trigger: "hasta" - replace: "potter" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - assert_eq!(config_set.default.matches.len(), 1); - if let MatchContentType::Text(content) = config_set.default.matches[0].content.clone() { - assert_eq!(config_set.default.matches[0].triggers[0], "hasta"); - assert_eq!(content.replace, "Hasta la vista") - } else { - panic!("invalid content"); - } - } - - #[test] - fn test_config_set_package_configs_without_merge() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_package_file( - package_dir.path(), - "package1", - "package.yml", - r###" - matches: - - trigger: "harry" - replace: "potter" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 1); - assert_eq!(config_set.default.matches.len(), 1); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hasta")); - assert!(config_set.specific[0] - .matches - .iter() - .any(|m| m.triggers[0] == "harry")); - } - - #[test] - fn test_config_set_package_configs_multiple_files() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: hasta - replace: Hasta la vista - "###, - ); - - create_package_file( - package_dir.path(), - "package1", - "package.yml", - r###" - name: package1 - - matches: - - trigger: "harry" - replace: "potter" - "###, - ); - - create_package_file( - package_dir.path(), - "package1", - "addon.yml", - r###" - parent: package1 - - matches: - - trigger: "ron" - replace: "weasley" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 1); - assert_eq!(config_set.default.matches.len(), 1); - assert!(config_set - .default - .matches - .iter() - .any(|m| m.triggers[0] == "hasta")); - assert!(config_set.specific[0] - .matches - .iter() - .any(|m| m.triggers[0] == "harry")); - assert!(config_set.specific[0] - .matches - .iter() - .any(|m| m.triggers[0] == "ron")); - } - - #[test] - fn test_list_has_conflict_no_conflict() { - assert_eq!( - ConfigSet::list_has_conflicts(&vec!(":ab".to_owned(), ":bc".to_owned())), - false - ); - } - - #[test] - fn test_list_has_conflict_conflict() { - let mut list = vec!["ac".to_owned(), "ab".to_owned(), "abc".to_owned()]; - list.sort(); - assert_eq!(ConfigSet::list_has_conflicts(&list), true); - } - - #[test] - fn test_has_conflict_no_conflict() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ac - replace: Hasta la vista - - trigger: bc - replace: Jon - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: specific1 - - matches: - - trigger: "hello" - replace: "world" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!( - ConfigSet::has_conflicts(&config_set.default, &config_set.specific), - false - ); - } - - #[test] - fn test_has_conflict_conflict_in_default() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ac - replace: Hasta la vista - - trigger: bc - replace: Jon - - trigger: acb - replace: Error - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: specific1 - - matches: - - trigger: "hello" - replace: "world" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!( - ConfigSet::has_conflicts(&config_set.default, &config_set.specific), - true - ); - } - - #[test] - fn test_has_conflict_conflict_in_specific_and_default() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ac - replace: Hasta la vista - - trigger: bc - replace: Jon - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: specific1 - - matches: - - trigger: "bcd" - replace: "Conflict" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!( - ConfigSet::has_conflicts(&config_set.default, &config_set.specific), - true - ); - } - - #[test] - fn test_has_conflict_no_conflict_in_specific_and_specific() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - matches: - - trigger: ac - replace: Hasta la vista - - trigger: bc - replace: Jon - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - name: specific1 - - matches: - - trigger: "bad" - replace: "Conflict" - "###, - ); - create_user_config_file( - data_dir.path(), - "specific2.yml", - r###" - name: specific2 - - matches: - - trigger: "badass" - replace: "Conflict" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!( - ConfigSet::has_conflicts(&config_set.default, &config_set.specific), - false - ); - } - - #[test] - fn test_config_set_specific_inherits_default_global_vars() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - global_vars: - - name: testvar - type: date - params: - format: "%m" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - global_vars: - - name: specificvar - type: date - params: - format: "%m" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 1); - assert_eq!(config_set.default.global_vars.len(), 1); - assert_eq!(config_set.specific[0].global_vars.len(), 2); - assert!(config_set.specific[0] - .global_vars - .iter() - .any(|m| m.name == "testvar")); - assert!(config_set.specific[0] - .global_vars - .iter() - .any(|m| m.name == "specificvar")); - } - - #[test] - fn test_config_set_default_get_variables_from_specific() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - global_vars: - - name: testvar - type: date - params: - format: "%m" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - parent: default - global_vars: - - name: specificvar - type: date - params: - format: "%m" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 0); - assert_eq!(config_set.default.global_vars.len(), 2); - assert!(config_set - .default - .global_vars - .iter() - .any(|m| m.name == "testvar")); - assert!(config_set - .default - .global_vars - .iter() - .any(|m| m.name == "specificvar")); - } - - #[test] - fn test_config_set_specific_dont_inherits_default_global_vars_when_exclude_is_on() { - let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( - r###" - global_vars: - - name: testvar - type: date - params: - format: "%m" - "###, - ); - - create_user_config_file( - data_dir.path(), - "specific.yml", - r###" - exclude_default_entries: true - - global_vars: - - name: specificvar - type: date - params: - format: "%m" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); - assert_eq!(config_set.specific.len(), 1); - assert_eq!(config_set.default.global_vars.len(), 1); - assert_eq!(config_set.specific[0].global_vars.len(), 1); - assert!(config_set.specific[0] - .global_vars - .iter() - .any(|m| m.name == "specificvar")); - } -} diff --git a/src/config/runtime.rs b/src/config/runtime.rs deleted file mode 100644 index 577897c..0000000 --- a/src/config/runtime.rs +++ /dev/null @@ -1,555 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::{ConfigSet, Configs}; -use crate::matcher::Match; -use crate::system::SystemManager; -use log::{debug, warn}; -use regex::Regex; -use std::cell::RefCell; -use std::time::SystemTime; - -pub struct RuntimeConfigManager<'a, S: SystemManager> { - set: ConfigSet, - - // Filter regexps - title_regexps: Vec>, - class_regexps: Vec>, - exec_regexps: Vec>, - - system_manager: S, - - // Cache - last_config_update: RefCell, - last_config: RefCell>, -} - -impl<'a, S: SystemManager> RuntimeConfigManager<'a, S> { - pub fn new<'b>(set: ConfigSet, system_manager: S) -> RuntimeConfigManager<'b, S> { - // Compile all the regexps - let title_regexps = set.specific.iter().map( - |config| { - if config.filter_title.is_empty() { - None - }else{ - let res = Regex::new(&config.filter_title); - if let Ok(regex) = res { - Some(regex) - }else{ - warn!("Invalid regex in 'filter_title' field of configuration {}, ignoring it...", config.name); - None - } - } - } - ).collect(); - - let class_regexps = set.specific.iter().map( - |config| { - if config.filter_class.is_empty() { - None - }else{ - let res = Regex::new(&config.filter_class); - if let Ok(regex) = res { - Some(regex) - }else{ - warn!("Invalid regex in 'filter_class' field of configuration {}, ignoring it...", config.name); - None - } - } - } - ).collect(); - - let exec_regexps = set.specific.iter().map( - |config| { - if config.filter_exec.is_empty() { - None - }else{ - let res = Regex::new(&config.filter_exec); - if let Ok(regex) = res { - Some(regex) - }else{ - warn!("Invalid regex in 'filter_exec' field of configuration {}, ignoring it...", config.name); - None - } - } - } - ).collect(); - - let last_config_update = RefCell::new(SystemTime::now()); - let last_config = RefCell::new(None); - - RuntimeConfigManager { - set, - title_regexps, - class_regexps, - exec_regexps, - system_manager, - last_config_update, - last_config, - } - } - - fn calculate_active_config(&'a self) -> &'a Configs { - // TODO: optimize performance by avoiding some of these checks if no Configs use the filters - - debug!("Requested config for window:"); - - let active_title = self.system_manager.get_current_window_title(); - - if let Some(title) = active_title { - debug!("=> Title: '{}'", title); - - for (i, regex) in self.title_regexps.iter().enumerate() { - if let Some(regex) = regex { - if regex.is_match(&title) { - debug!( - "Matched 'filter_title' for '{}' config, using custom settings.", - self.set.specific[i].name - ); - - return &self.set.specific[i]; - } - } - } - } - - let active_executable = self.system_manager.get_current_window_executable(); - - if let Some(executable) = active_executable { - debug!("=> Executable: '{}'", executable); - - for (i, regex) in self.exec_regexps.iter().enumerate() { - if let Some(regex) = regex { - if regex.is_match(&executable) { - debug!( - "Matched 'filter_exec' for '{}' config, using custom settings.", - self.set.specific[i].name - ); - - return &self.set.specific[i]; - } - } - } - } - - let active_class = self.system_manager.get_current_window_class(); - - if let Some(class) = active_class { - debug!("=> Class: '{}'", class); - - for (i, regex) in self.class_regexps.iter().enumerate() { - if let Some(regex) = regex { - if regex.is_match(&class) { - debug!( - "Matched 'filter_class' for '{}' config, using custom settings.", - self.set.specific[i].name - ); - - return &self.set.specific[i]; - } - } - } - } - - // No matches, return the default mapping - debug!("No matches for custom configs, using default settings."); - &self.set.default - } -} - -impl<'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a, S> { - fn active_config(&'a self) -> &'a Configs { - let mut last_config_update = self.last_config_update.borrow_mut(); - if let Ok(elapsed) = (*last_config_update).elapsed() { - *last_config_update = SystemTime::now(); - - if elapsed.as_millis() < self.set.default.config_caching_interval as u128 { - let last_config = self.last_config.borrow(); - if let Some(cached_config) = *last_config { - debug!("Using cached config"); - return cached_config; - } - } - } - - let config = self.calculate_active_config(); - - let mut last_config = self.last_config.borrow_mut(); - *last_config = Some(config); - - config - } - - fn default_config(&'a self) -> &'a Configs { - &self.set.default - } - - fn matches(&'a self) -> &'a Vec { - &self.active_config().matches - } -} - -// TESTS - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::tests::{create_temp_espanso_directories, create_user_config_file}; - use crate::config::ConfigManager; - - struct DummySystemManager { - title: RefCell, - class: RefCell, - exec: RefCell, - } - impl SystemManager for DummySystemManager { - fn get_current_window_title(&self) -> Option { - Some(self.title.borrow().clone()) - } - fn get_current_window_class(&self) -> Option { - Some(self.class.borrow().clone()) - } - fn get_current_window_executable(&self) -> Option { - Some(self.exec.borrow().clone()) - } - } - impl DummySystemManager { - pub fn new_custom(title: &str, class: &str, exec: &str) -> DummySystemManager { - DummySystemManager { - title: RefCell::new(title.to_owned()), - class: RefCell::new(class.to_owned()), - exec: RefCell::new(exec.to_owned()), - } - } - - pub fn new() -> DummySystemManager { - DummySystemManager::new_custom("title", "class", "exec") - } - - pub fn change(&self, title: &str, class: &str, exec: &str) { - *self.title.borrow_mut() = title.to_owned(); - *self.class.borrow_mut() = class.to_owned(); - *self.exec.borrow_mut() = exec.to_owned(); - } - } - - #[test] - fn test_runtime_constructor_regex_load_correctly() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: myname1 - filter_exec: "Title" - "###, - ); - - create_user_config_file( - &data_dir.path(), - "specific2.yml", - r###" - name: myname2 - filter_title: "Yeah" - filter_class: "Car" - "###, - ); - - create_user_config_file( - &data_dir.path(), - "specific3.yml", - r###" - name: myname3 - filter_title: "Nice" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = DummySystemManager::new(); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - let sp1index = config_manager - .set - .specific - .iter() - .position(|x| x.name == "myname1") - .unwrap(); - let sp2index = config_manager - .set - .specific - .iter() - .position(|x| x.name == "myname2") - .unwrap(); - let sp3index = config_manager - .set - .specific - .iter() - .position(|x| x.name == "myname3") - .unwrap(); - - assert_eq!(config_manager.exec_regexps.len(), 3); - assert_eq!(config_manager.title_regexps.len(), 3); - assert_eq!(config_manager.class_regexps.len(), 3); - - assert!(config_manager.class_regexps[sp1index].is_none()); - assert!(config_manager.class_regexps[sp2index].is_some()); - assert!(config_manager.class_regexps[sp3index].is_none()); - - assert!(config_manager.title_regexps[sp1index].is_none()); - assert!(config_manager.title_regexps[sp2index].is_some()); - assert!(config_manager.title_regexps[sp3index].is_some()); - - assert!(config_manager.exec_regexps[sp1index].is_some()); - assert!(config_manager.exec_regexps[sp2index].is_none()); - assert!(config_manager.exec_regexps[sp3index].is_none()); - } - - #[test] - fn test_runtime_constructor_malformed_regexes_are_ignored() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: myname1 - filter_exec: "[`-_]" - "###, - ); - - create_user_config_file( - &data_dir.path(), - "specific2.yml", - r###" - name: myname2 - filter_title: "[`-_]" - filter_class: "Car" - "###, - ); - - create_user_config_file( - &data_dir.path(), - "specific3.yml", - r###" - name: myname3 - filter_title: "Nice" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = DummySystemManager::new(); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - let sp1index = config_manager - .set - .specific - .iter() - .position(|x| x.name == "myname1") - .unwrap(); - let sp2index = config_manager - .set - .specific - .iter() - .position(|x| x.name == "myname2") - .unwrap(); - let sp3index = config_manager - .set - .specific - .iter() - .position(|x| x.name == "myname3") - .unwrap(); - - assert_eq!(config_manager.exec_regexps.len(), 3); - assert_eq!(config_manager.title_regexps.len(), 3); - assert_eq!(config_manager.class_regexps.len(), 3); - - assert!(config_manager.class_regexps[sp1index].is_none()); - assert!(config_manager.class_regexps[sp2index].is_some()); - assert!(config_manager.class_regexps[sp3index].is_none()); - - assert!(config_manager.title_regexps[sp1index].is_none()); - assert!(config_manager.title_regexps[sp2index].is_none()); - assert!(config_manager.title_regexps[sp3index].is_some()); - - assert!(config_manager.exec_regexps[sp1index].is_none()); - assert!(config_manager.exec_regexps[sp2index].is_none()); - assert!(config_manager.exec_regexps[sp3index].is_none()); - } - - #[test] - fn test_runtime_calculate_active_config_specific_title_match() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: chrome - filter_title: "Chrome" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = - DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - assert_eq!(config_manager.calculate_active_config().name, "chrome"); - } - - #[test] - fn test_runtime_calculate_active_config_specific_class_match() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: chrome - filter_class: "Chrome" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = - DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - assert_eq!(config_manager.calculate_active_config().name, "chrome"); - } - - #[test] - fn test_runtime_calculate_active_config_specific_exec_match() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: chrome - filter_exec: "chrome.exe" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = - DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - assert_eq!(config_manager.calculate_active_config().name, "chrome"); - } - - #[test] - fn test_runtime_calculate_active_config_specific_multi_filter_match() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: chrome - filter_class: Browser - filter_exec: "firefox.exe" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = - DummySystemManager::new_custom("Google Chrome", "Browser", "C:\\Path\\chrome.exe"); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - assert_eq!(config_manager.calculate_active_config().name, "chrome"); - } - - #[test] - fn test_runtime_calculate_active_config_no_match() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: firefox - filter_title: "Firefox" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = - DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - assert_eq!(config_manager.calculate_active_config().name, "default"); - } - - #[test] - fn test_runtime_active_config_cache() { - let (data_dir, package_dir) = create_temp_espanso_directories(); - - create_user_config_file( - &data_dir.path(), - "specific.yml", - r###" - name: firefox - filter_title: "Firefox" - "###, - ); - - let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); - assert!(config_set.is_ok()); - - let dummy_system_manager = - DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); - - let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); - - assert_eq!(config_manager.active_config().name, "default"); - assert_eq!(config_manager.calculate_active_config().name, "default"); - - config_manager - .system_manager - .change("Firefox", "Browser", "C\\Path\\firefox.exe"); - - // Active config should have changed, but not cached one - assert_eq!(config_manager.calculate_active_config().name, "firefox"); - assert_eq!(config_manager.active_config().name, "default"); - } -} diff --git a/src/context/linux.rs b/src/context/linux.rs deleted file mode 100644 index 2d0fe54..0000000 --- a/src/context/linux.rs +++ /dev/null @@ -1,170 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::linux::*; -use crate::config::Configs; -use crate::event::KeyModifier::*; -use crate::event::*; -use log::{debug, error, warn}; -use std::ffi::CStr; -use std::os::raw::{c_char, c_void}; -use std::process::exit; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Acquire; -use std::sync::mpsc::Sender; -use std::sync::Arc; - -#[repr(C)] -pub struct LinuxContext { - pub send_channel: Sender, - is_injecting: Arc, -} - -impl LinuxContext { - pub fn new( - _: Configs, - send_channel: Sender, - is_injecting: Arc, - ) -> Box { - // Check if the X11 context is available - let x11_available = unsafe { check_x11() }; - - if x11_available < 0 { - error!("Error, can't connect to X11 context"); - std::process::exit(100); - } - - let context = Box::new(LinuxContext { - send_channel, - is_injecting, - }); - - unsafe { - let context_ptr = &*context as *const LinuxContext as *const c_void; - - register_keypress_callback(keypress_callback); - register_error_callback(error_callback); - - let res = initialize(context_ptr); - if res <= 0 { - error!("Could not initialize linux context, error: {}", res); - exit(10); - } - } - - context - } -} - -impl super::Context for LinuxContext { - fn eventloop(&self) { - unsafe { - eventloop(); - } - } -} - -impl Drop for LinuxContext { - fn drop(&mut self) { - unsafe { - cleanup(); - } - } -} - -// Native bridge code - -extern "C" fn keypress_callback( - _self: *mut c_void, - raw_buffer: *const u8, - _len: i32, - event_type: i32, - key_code: i32, -) { - unsafe { - let _self = _self as *mut LinuxContext; - - // If espanso is currently injecting text, we should avoid processing - // external events, as it could happen that espanso reinterpret its - // own input. - if (*_self).is_injecting.load(Acquire) { - debug!("Input ignored while espanso is injecting text..."); - return; - } - - if event_type == 0 { - // Char event - // Convert the received buffer to a string - let c_str = CStr::from_ptr(raw_buffer as *const c_char); - let char_str = c_str.to_str(); - - // Send the char through the channel - match char_str { - Ok(char_str) => { - let event = Event::Key(KeyEvent::Char(char_str.to_owned())); - (*_self).send_channel.send(event).unwrap(); - } - Err(e) => { - debug!("Unable to receive char: {}", e); - } - } - } else if event_type == 1 { - // Modifier event - - let modifier: Option = match key_code { - 133 => Some(LEFT_META), - 134 => Some(RIGHT_META), - 50 => Some(LEFT_SHIFT), - 62 => Some(RIGHT_SHIFT), - 64 => Some(LEFT_ALT), - 108 => Some(RIGHT_ALT), - 37 => Some(LEFT_CTRL), - 105 => Some(RIGHT_CTRL), - 22 => Some(BACKSPACE), - 66 => Some(CAPS_LOCK), - _ => None, - }; - - if let Some(modifier) = modifier { - let event = Event::Key(KeyEvent::Modifier(modifier)); - (*_self).send_channel.send(event).unwrap(); - } else { - // Not one of the default modifiers, send an "other" event - let event = Event::Key(KeyEvent::Other); - (*_self).send_channel.send(event).unwrap(); - } - } else { - // Other type of event - let event = Event::Key(KeyEvent::Other); - (*_self).send_channel.send(event).unwrap(); - } - } -} - -extern "C" fn error_callback( - _self: *mut c_void, - error_code: c_char, - request_code: c_char, - minor_code: c_char, -) { - warn!( - "X11 reported an error code: {}, request_code: {} and minor_code: {}", - error_code, request_code, minor_code - ); -} diff --git a/src/context/macos.rs b/src/context/macos.rs deleted file mode 100644 index 284e0c7..0000000 --- a/src/context/macos.rs +++ /dev/null @@ -1,275 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::macos::*; -use crate::config::Configs; -use crate::event::KeyModifier::*; -use crate::event::{ActionType, Event, KeyEvent, KeyModifier, SystemEvent}; -use crate::system::macos::MacSystemManager; -use log::{debug, error, info}; -use std::cell::RefCell; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_void}; -use std::process::exit; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Acquire; -use std::sync::mpsc::Sender; -use std::sync::Arc; -use std::{fs, thread}; - -const STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icon.png"); -const DISABLED_STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icondisabled.png"); - -pub struct MacContext { - pub send_channel: Sender, - is_injecting: Arc, - secure_input_watcher_enabled: bool, - secure_input_watcher_interval: i32, -} - -impl MacContext { - pub fn new( - config: Configs, - send_channel: Sender, - is_injecting: Arc, - ) -> Box { - // Check accessibility - unsafe { - let res = prompt_accessibility(); - - if res == 0 { - error!("Accessibility must be enabled to make espanso work on MacOS."); - error!( - "Please allow espanso in the Security & Privacy panel, then restart espanso." - ); - error!("For more information: https://espanso.org/install/mac/"); - exit(1); - } - } - - let context = Box::new(MacContext { - send_channel, - is_injecting, - secure_input_watcher_enabled: config.secure_input_watcher_enabled, - secure_input_watcher_interval: config.secure_input_watcher_interval, - }); - - // Initialize the status icon path - let espanso_dir = super::get_data_dir(); - let status_icon_target = espanso_dir.join("icon.png"); - let disabled_status_icon_target = espanso_dir.join("icondisabled.png"); - - if status_icon_target.exists() { - info!("Status icon already initialized, skipping."); - } else { - fs::write(&status_icon_target, STATUS_ICON_BINARY).unwrap_or_else(|e| { - error!( - "Error copying the Status Icon to the espanso data directory: {}", - e - ); - }); - } - - if disabled_status_icon_target.exists() { - info!("Status icon (disabled) already initialized, skipping."); - } else { - fs::write(&disabled_status_icon_target, DISABLED_STATUS_ICON_BINARY).unwrap_or_else( - |e| { - error!( - "Error copying the Status Icon (disabled) to the espanso data directory: {}", - e - ); - }, - ); - } - - unsafe { - let context_ptr = &*context as *const MacContext as *const c_void; - - register_keypress_callback(keypress_callback); - register_icon_click_callback(icon_click_callback); - register_context_menu_click_callback(context_menu_click_callback); - - let status_icon_path = - CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default(); - let disabled_status_icon_path = - CString::new(disabled_status_icon_target.to_str().unwrap_or_default()) - .unwrap_or_default(); - let show_icon = if config.show_icon { 1 } else { 0 }; - - initialize( - context_ptr, - status_icon_path.as_ptr(), - disabled_status_icon_path.as_ptr(), - show_icon, - ); - } - - context - } - - fn start_secure_input_watcher(&self) { - let send_channel = self.send_channel.clone(); - let secure_input_watcher_interval = self.secure_input_watcher_interval as u64; - - let secure_input_watcher = thread::Builder::new().name("secure_input_watcher".to_string()).spawn(move || { - let mut last_secure_input_pid: Option = None; - loop { - let pid = MacSystemManager::get_secure_input_pid(); - - if let Some(pid) = pid { // Some application is currently on SecureInput - let should_notify = if let Some(old_pid) = last_secure_input_pid { // We already detected a SecureInput app - if old_pid != pid { // The old app is different from the current one, we should take action - true - }else{ // We already notified this application before - false - } - }else{ // First time we see this SecureInput app, we should take action - true - }; - - if should_notify { - let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application(); - - if let Some((app_name, path)) = secure_input_app { - let event = Event::System(SystemEvent::SecureInputEnabled(app_name, path)); - send_channel.send(event); - } - } - - last_secure_input_pid = Some(pid); - }else{ // No app is currently keeping SecureInput - if let Some(old_pid) = last_secure_input_pid { // If there was an app with SecureInput, notify that is now free - let event = Event::System(SystemEvent::SecureInputDisabled); - send_channel.send(event); - } - - last_secure_input_pid = None - } - - thread::sleep(std::time::Duration::from_millis(secure_input_watcher_interval)); - } - }); - } -} - -pub fn update_icon(enabled: bool) { - unsafe { - crate::bridge::macos::update_tray_icon(if enabled { 1 } else { 0 }); - } -} - -impl super::Context for MacContext { - fn eventloop(&self) { - // Start the SecureInput watcher thread - if self.secure_input_watcher_enabled { - self.start_secure_input_watcher(); - } - - unsafe { - eventloop(); - } - } -} - -// Native bridge code - -extern "C" fn keypress_callback( - _self: *mut c_void, - raw_buffer: *const u8, - len: i32, - event_type: i32, - key_code: i32, -) { - unsafe { - let _self = _self as *mut MacContext; - - // If espanso is currently injecting text, we should avoid processing - // external events, as it could happen that espanso reinterpret its - // own input. - if (*_self).is_injecting.load(Acquire) { - debug!("Input ignored while espanso is injecting text..."); - return; - } - - if event_type == 0 { - // Char event - // Convert the received buffer to a string - let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); - let char_str = c_str.to_str(); - - // Send the char through the channel - match char_str { - Ok(char_str) => { - let event = Event::Key(KeyEvent::Char(char_str.to_owned())); - (*_self).send_channel.send(event).unwrap(); - } - Err(e) => { - error!("Unable to receive char: {}", e); - } - } - } else if event_type == 1 { - // Modifier event - let modifier: Option = match key_code { - 0x37 => Some(LEFT_META), - 0x36 => Some(RIGHT_META), - 0x38 => Some(LEFT_SHIFT), - 0x3C => Some(RIGHT_SHIFT), - 0x3A => Some(LEFT_ALT), - 0x3D => Some(RIGHT_ALT), - 0x3B => Some(LEFT_CTRL), - 0x3E => Some(RIGHT_CTRL), - 0x33 => Some(BACKSPACE), - 0x39 => Some(CAPS_LOCK), - _ => None, - }; - - if let Some(modifier) = modifier { - let event = Event::Key(KeyEvent::Modifier(modifier)); - (*_self).send_channel.send(event).unwrap(); - } else { - // Not one of the default modifiers, send an "other" event - let event = Event::Key(KeyEvent::Other); - (*_self).send_channel.send(event).unwrap(); - } - } else { - // Other type of event - let event = Event::Key(KeyEvent::Other); - (*_self).send_channel.send(event).unwrap(); - } - } -} - -extern "C" fn icon_click_callback(_self: *mut c_void) { - unsafe { - let _self = _self as *mut MacContext; - - let event = Event::Action(ActionType::IconClick); - (*_self).send_channel.send(event).unwrap(); - } -} - -extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) { - unsafe { - let _self = _self as *mut MacContext; - - let event = Event::Action(ActionType::from(id)); - (*_self).send_channel.send(event).unwrap(); - } -} diff --git a/src/context/mod.rs b/src/context/mod.rs deleted file mode 100644 index 0f95483..0000000 --- a/src/context/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#[cfg(target_os = "windows")] -mod windows; - -#[cfg(target_os = "linux")] -mod linux; - -#[cfg(target_os = "macos")] -pub(crate) mod macos; - -use crate::config::Configs; -use crate::event::Event; -use std::fs::create_dir_all; -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; -use std::sync::mpsc::Sender; -use std::sync::{Arc, Once}; - -pub trait Context { - fn eventloop(&self); -} - -// MAC IMPLEMENTATION -#[cfg(target_os = "macos")] -pub fn new( - config: Configs, - send_channel: Sender, - is_injecting: Arc, -) -> Box { - macos::MacContext::new(config, send_channel, is_injecting) -} - -#[cfg(target_os = "macos")] -pub fn update_icon(enabled: bool) { - macos::update_icon(enabled); -} - -#[cfg(target_os = "macos")] -pub fn get_icon_path() -> Option { - None -} - -// LINUX IMPLEMENTATION -#[cfg(target_os = "linux")] -pub fn new( - config: Configs, - send_channel: Sender, - is_injecting: Arc, -) -> Box { - linux::LinuxContext::new(config, send_channel, is_injecting) -} - -#[cfg(target_os = "linux")] -pub fn update_icon(enabled: bool) { - // No icon on Linux -} - -#[cfg(target_os = "linux")] -pub fn get_icon_path() -> Option { - None -} - -// WINDOWS IMPLEMENTATION -#[cfg(target_os = "windows")] -pub fn new( - config: Configs, - send_channel: Sender, - is_injecting: Arc, -) -> Box { - windows::WindowsContext::new(config, send_channel, is_injecting) -} - -#[cfg(target_os = "windows")] -pub fn update_icon(enabled: bool) { - windows::update_icon(enabled); -} - -#[cfg(target_os = "windows")] -pub fn get_icon_path() -> Option { - Some(windows::get_icon_path(&get_data_dir())) -} - -// espanso directories - -static WARING_INIT: Once = Once::new(); - -pub fn get_data_dir() -> PathBuf { - let data_dir = dirs::data_local_dir().expect("Can't obtain data_local_dir(), terminating."); - let espanso_dir = data_dir.join("espanso"); - create_dir_all(&espanso_dir).expect("Error creating espanso data directory"); - espanso_dir -} - -pub fn get_config_dir() -> PathBuf { - // Portable mode check - // Get the espanso executable path - let espanso_exe_path = std::env::current_exe().expect("Could not get espanso executable path"); - let exe_dir = espanso_exe_path.parent(); - if let Some(parent) = exe_dir { - let config_dir = parent.join(".espanso"); - if config_dir.exists() { - println!( - "PORTABLE MODE, using config folder: '{}'", - config_dir.to_string_lossy() - ); - return config_dir; - } - } - - // For compatibility purposes, check if the $HOME/.espanso directory is available - let home_dir = dirs::home_dir().expect("Can't obtain the user home directory, terminating."); - let legacy_espanso_dir = home_dir.join(".espanso"); - if legacy_espanso_dir.exists() { - // Avoid printing the warning multiple times with std::sync::Once - WARING_INIT.call_once(|| { - eprintln!("WARNING: using legacy espanso config location in $HOME/.espanso is DEPRECATED"); - eprintln!("Starting from espanso v0.3.0, espanso config location is changed."); - eprintln!("Please check out the documentation to find out more: https://espanso.org/docs/configuration/"); - eprintln!() - }); - - return legacy_espanso_dir; - } - - // Check for $HOME/.config/espanso location - let home_config_dir = home_dir.join(".config"); - let config_espanso_dir = home_config_dir.join("espanso"); - if config_espanso_dir.exists() { - return config_espanso_dir; - } - - // New config location, from version v0.3.0 - // Refer to issue #73 for more information: https://github.com/federico-terzi/espanso/issues/73 - let config_dir = dirs::config_dir().expect("Can't obtain config_dir(), terminating."); - let espanso_dir = config_dir.join("espanso"); - create_dir_all(&espanso_dir).expect("Error creating espanso config directory"); - espanso_dir -} - -const PACKAGES_FOLDER_NAME: &str = "packages"; - -pub fn get_package_dir() -> PathBuf { - // Deprecated $HOME/.espanso/packages directory compatibility check - let config_dir = get_config_dir(); - let legacy_package_dir = config_dir.join(PACKAGES_FOLDER_NAME); - if legacy_package_dir.exists() { - return legacy_package_dir; - } - - // New package location, starting from version v0.3.0 - let data_dir = get_data_dir(); - let package_dir = data_dir.join(PACKAGES_FOLDER_NAME); - create_dir_all(&package_dir).expect("Error creating espanso packages directory"); - package_dir -} diff --git a/src/context/windows.rs b/src/context/windows.rs deleted file mode 100644 index f871f30..0000000 --- a/src/context/windows.rs +++ /dev/null @@ -1,252 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::windows::*; -use crate::config::Configs; -use crate::event::KeyModifier::*; -use crate::event::{ActionType, Event, KeyEvent, KeyModifier}; -use log::{debug, error, info}; -use std::ffi::c_void; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Acquire; -use std::sync::mpsc::Sender; -use std::sync::Arc; -use widestring::{U16CStr, U16CString}; - -const BMP_BINARY: &[u8] = include_bytes!("../res/win/espanso.bmp"); -const ICO_BINARY: &[u8] = include_bytes!("../res/win/espanso.ico"); -const RED_ICO_BINARY: &[u8] = include_bytes!("../res/win/espansored.ico"); - -pub struct WindowsContext { - send_channel: Sender, - is_injecting: Arc, -} - -impl WindowsContext { - pub fn new( - config: Configs, - send_channel: Sender, - is_injecting: Arc, - ) -> Box { - // Initialize image resources - - let espanso_dir = super::get_data_dir(); - - info!( - "Initializing Espanso resources in {}", - espanso_dir.as_path().display() - ); - - let espanso_bmp_image = espanso_dir.join("espansoicon.bmp"); - if espanso_bmp_image.exists() { - info!("BMP already initialized, skipping."); - } else { - fs::write(&espanso_bmp_image, BMP_BINARY).expect("Unable to write windows bmp file"); - - info!( - "Extracted bmp icon to: {}", - espanso_bmp_image.to_str().unwrap_or("error") - ); - } - - let espanso_ico_image = get_icon_path(&espanso_dir); - if espanso_ico_image.exists() { - info!("ICO already initialized, skipping."); - } else { - fs::write(&espanso_ico_image, ICO_BINARY).expect("Unable to write windows ico file"); - - info!( - "Extracted 'ico' icon to: {}", - espanso_ico_image.to_str().unwrap_or("error") - ); - } - - let espanso_red_ico_image = espanso_dir.join("espansored.ico"); - if espanso_red_ico_image.exists() { - info!("red ICO already initialized, skipping."); - } else { - fs::write(&espanso_red_ico_image, RED_ICO_BINARY) - .expect("Unable to write windows ico file"); - - info!( - "Extracted 'red ico' icon to: {}", - espanso_red_ico_image.to_str().unwrap_or("error") - ); - } - - let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default(); - let ico_icon = espanso_ico_image.to_str().unwrap_or_default(); - let red_ico_icon = espanso_red_ico_image.to_str().unwrap_or_default(); - - let send_channel = send_channel; - - let context = Box::new(WindowsContext { - send_channel, - is_injecting, - }); - - unsafe { - let context_ptr = &*context as *const WindowsContext as *const c_void; - - // Register callbacks - register_keypress_callback(keypress_callback); - register_icon_click_callback(icon_click_callback); - register_context_menu_click_callback(context_menu_click_callback); - - let ico_file_c = U16CString::from_str(ico_icon).unwrap(); - let red_ico_file_c = U16CString::from_str(red_ico_icon).unwrap(); - let bmp_file_c = U16CString::from_str(bmp_icon).unwrap(); - - let show_icon = if config.show_icon { 1 } else { 0 }; - - // Initialize the windows - let res = initialize( - context_ptr, - ico_file_c.as_ptr(), - red_ico_file_c.as_ptr(), - bmp_file_c.as_ptr(), - show_icon, - ); - if res != 1 { - panic!("Can't initialize Windows context") - } - } - - context - } -} - -impl super::Context for WindowsContext { - fn eventloop(&self) { - unsafe { - eventloop(); - } - } -} - -pub fn get_icon_path(espanso_dir: &Path) -> PathBuf { - espanso_dir.join("espanso.ico") -} - -// Native bridge code - -pub fn update_icon(enabled: bool) { - unsafe { - crate::bridge::windows::update_tray_icon(if enabled { 1 } else { 0 }); - } -} - -extern "C" fn keypress_callback( - _self: *mut c_void, - raw_buffer: *const u16, - len: i32, - event_type: i32, - key_code: i32, - variant: i32, - is_key_down: i32, -) { - unsafe { - let _self = _self as *mut WindowsContext; - - // If espanso is currently injecting text, we should avoid processing - // external events, as it could happen that espanso reinterpret its - // own input. - if (*_self).is_injecting.load(Acquire) { - debug!("Input ignored while espanso is injecting text..."); - return; - } - - if event_type == 0 { - // Char event - if is_key_down != 0 { - // KEY DOWN EVENT - // Convert the received buffer to a string - let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); - let c_string = U16CStr::from_slice_with_nul(buffer); - - if let Ok(c_string) = c_string { - let string = c_string.to_string(); - - // Send the char through the channel - match string { - Ok(string) => { - let event = Event::Key(KeyEvent::Char(string)); - (*_self).send_channel.send(event).unwrap(); - } - Err(e) => { - error!("Unable to receive char: {}", e); - } - } - } else { - error!("unable to decode widechar"); - } - } - } else if event_type == 1 { - // Modifier event - if is_key_down == 0 { - let modifier: Option = match (key_code, variant) { - (0x5B, _) => Some(LEFT_META), - (0x5C, _) => Some(RIGHT_META), - (0x10, 1) => Some(LEFT_SHIFT), - (0x10, 2) => Some(RIGHT_SHIFT), - (0x12, 1) => Some(LEFT_ALT), - (0x12, 2) => Some(RIGHT_ALT), - (0x11, 1) => Some(LEFT_CTRL), - (0x11, 2) => Some(RIGHT_CTRL), - (0x08, _) => Some(BACKSPACE), - (0x14, _) => Some(CAPS_LOCK), - _ => None, - }; - - if let Some(modifier) = modifier { - let event = Event::Key(KeyEvent::Modifier(modifier)); - (*_self).send_channel.send(event).unwrap(); - } else { - // Not one of the default modifiers, send an "other" event - let event = Event::Key(KeyEvent::Other); - (*_self).send_channel.send(event).unwrap(); - } - } - } else { - // Other type of event - let event = Event::Key(KeyEvent::Other); - (*_self).send_channel.send(event).unwrap(); - } - } -} - -extern "C" fn icon_click_callback(_self: *mut c_void) { - unsafe { - let _self = _self as *mut WindowsContext; - - let event = Event::Action(ActionType::IconClick); - (*_self).send_channel.send(event).unwrap(); - } -} - -extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) { - unsafe { - let _self = _self as *mut WindowsContext; - - let event = Event::Action(ActionType::from(id)); - (*_self).send_channel.send(event).unwrap(); - } -} diff --git a/src/edit.rs b/src/edit.rs deleted file mode 100644 index fc1e802..0000000 --- a/src/edit.rs +++ /dev/null @@ -1,76 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::path::Path; - -#[cfg(target_os = "linux")] -fn default_editor() -> String { - "/bin/nano".to_owned() -} -#[cfg(target_os = "macos")] -fn default_editor() -> String { - "/usr/bin/nano".to_owned() -} -#[cfg(target_os = "windows")] -fn default_editor() -> String { - "C:\\Windows\\System32\\notepad.exe".to_owned() -} - -pub fn open_editor(file_path: &Path) -> bool { - use std::process::Command; - - // Check if another editor is defined in the environment variables - let editor_var = std::env::var_os("EDITOR"); - let visual_var = std::env::var_os("VISUAL"); - - // Prioritize the editors specified by the environment variable, use the default one - let editor: String = if let Some(editor_var) = editor_var { - editor_var.to_string_lossy().to_string() - } else if let Some(visual_var) = visual_var { - visual_var.to_string_lossy().to_string() - } else { - default_editor() - }; - - // Start the editor and wait for its termination - let status = if cfg!(target_os = "windows") { - Command::new(&editor).arg(file_path).spawn() - } else { - // On Unix, spawn the editor using the shell so that it can - // accept parameters. See issue #245 - Command::new("/bin/bash") - .arg("-c") - .arg(format!("{} '{}'", editor, file_path.to_string_lossy())) - .spawn() - }; - - if let Ok(mut child) = status { - // Wait for the user to edit the configuration - let result = child.wait(); - - if let Ok(exit_status) = result { - exit_status.success() - } else { - false - } - } else { - println!("Error: could not start editor at: {}", &editor); - false - } -} diff --git a/src/engine.rs b/src/engine.rs deleted file mode 100644 index 81e4fa5..0000000 --- a/src/engine.rs +++ /dev/null @@ -1,545 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::clipboard::ClipboardManager; -use crate::config::BackendType; -use crate::config::{ConfigManager, Configs}; -use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver}; -use crate::keyboard::KeyboardManager; -use crate::matcher::{Match, MatchReceiver}; -use crate::protocol::{send_command_or_warn, IPCCommand, Service}; -use crate::render::{RenderResult, Renderer}; -use crate::{ - guard::InjectGuard, - ui::{MenuItem, MenuItemType, UIManager}, -}; -use log::{debug, error, info, warn}; -use regex::Regex; -use std::cell::RefCell; -use std::process::exit; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Release; -use std::sync::Arc; - -pub struct Engine< - 'a, - S: KeyboardManager, - C: ClipboardManager, - M: ConfigManager<'a>, - U: UIManager, - R: Renderer, -> { - keyboard_manager: &'a S, - clipboard_manager: &'a C, - config_manager: &'a M, - ui_manager: &'a U, - renderer: &'a R, - is_injecting: Arc, - - enabled: RefCell, - // Trigger string and injected text len pair - last_expansion_data: RefCell>, -} - -impl< - 'a, - S: KeyboardManager, - C: ClipboardManager, - M: ConfigManager<'a>, - U: UIManager, - R: Renderer, - > Engine<'a, S, C, M, U, R> -{ - pub fn new( - keyboard_manager: &'a S, - clipboard_manager: &'a C, - config_manager: &'a M, - ui_manager: &'a U, - renderer: &'a R, - is_injecting: Arc, - ) -> Engine<'a, S, C, M, U, R> { - let enabled = RefCell::new(true); - let last_expansion_data = RefCell::new(None); - - Engine { - keyboard_manager, - clipboard_manager, - config_manager, - ui_manager, - renderer, - is_injecting, - enabled, - last_expansion_data, - } - } - - fn build_menu(&self) -> Vec { - let mut menu = Vec::new(); - - let enabled = self.enabled.borrow(); - let toggle_text = if *enabled { "Disable" } else { "Enable" }.to_owned(); - menu.push(MenuItem { - item_type: MenuItemType::Button, - item_name: toggle_text, - item_id: ActionType::Toggle as i32, - }); - - menu.push(MenuItem { - item_type: MenuItemType::Separator, - item_name: "".to_owned(), - item_id: 998, - }); - - menu.push(MenuItem { - item_type: MenuItemType::Button, - item_name: "Reload configs".to_owned(), - item_id: ActionType::RestartWorker as i32, - }); - - menu.push(MenuItem { - item_type: MenuItemType::Separator, - item_name: "".to_owned(), - item_id: 999, - }); - - menu.push(MenuItem { - item_type: MenuItemType::Button, - item_name: "Exit espanso".to_owned(), - item_id: ActionType::Exit as i32, - }); - - menu - } - - fn return_content_if_preserve_clipboard_is_enabled(&self) -> Option { - // If the preserve_clipboard option is enabled, first save the current - // clipboard content in order to restore it later. - if self.config_manager.default_config().preserve_clipboard { - match self.clipboard_manager.get_clipboard() { - Some(clipboard) => Some(clipboard), - None => None, - } - } else { - None - } - } - - fn find_match_by_trigger(&self, trigger: &str) -> Option { - let config = self.config_manager.active_config(); - - if let Some(m) = config - .matches - .iter() - .find(|m| m.triggers.iter().any(|t| t == trigger)) - { - Some(m.clone()) - } else { - None - } - } - - fn inject_text( - &self, - config: &Configs, - target_string: &str, - force_clipboard: bool, - is_html: bool, - ) { - let backend = if force_clipboard || is_html { - &BackendType::Clipboard - } else if config.backend == BackendType::Auto { - if cfg!(target_os = "linux") { - let all_ascii = target_string.chars().all(|c| c.is_ascii()); - if all_ascii { - debug!("All elements of the replacement are ascii, using Inject backend"); - &BackendType::Inject - } else { - debug!("There are non-ascii characters, using Clipboard backend"); - &BackendType::Clipboard - } - } else { - &BackendType::Inject - } - } else { - &config.backend - }; - - match backend { - BackendType::Inject => { - // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = target_string.split('\n'); - - for (i, split) in splits.enumerate() { - if i > 0 { - self.keyboard_manager.send_enter(&config); - } - - self.keyboard_manager.send_string(&config, split); - } - } - BackendType::Clipboard => { - if !is_html { - self.clipboard_manager.set_clipboard(&target_string); - } else { - self.clipboard_manager.set_clipboard_html(&target_string); - } - - self.keyboard_manager.trigger_paste(&config); - } - _ => { - error!("Unsupported backend type evaluation."); - return; - } - } - } - - fn inject_match( - &self, - m: &Match, - trailing_separator: Option, - trigger_offset: usize, - skip_delete: bool, - ) -> Option<(String, i32)> { - let config = self.config_manager.active_config(); - - if !config.enable_active { - return None; - } - - // Block espanso from reinterpreting its own actions - let _inject_guard = InjectGuard::new(self.is_injecting.clone(), &config); - - let char_count = if trailing_separator.is_none() { - m.triggers[trigger_offset].chars().count() as i32 - } else { - m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator - }; - - // If configured to do so, wait until the modifier keys are released (or timeout) so - // that we avoid unwanted interactions. As an example, see: - // https://github.com/federico-terzi/espanso/issues/470 - if config.wait_for_modifiers_release { - crate::keyboard::wait_for_modifiers_release(); - } - - if !skip_delete { - self.keyboard_manager.delete_string(&config, char_count); - } - - let mut previous_clipboard_content: Option = None; - - let rendered = self - .renderer - .render_match(m, trigger_offset, config, vec![]); - - let mut expansion_data: Option<(String, i32)> = None; - - match rendered { - RenderResult::Text(mut target_string) => { - // If a trailing separator was counted in the match, add it back to the target string - if let Some(trailing_separator) = trailing_separator { - if trailing_separator == '\r' { - // If the trailing separator is a carriage return, - target_string.push('\n'); // convert it to new line - } else { - target_string.push(trailing_separator); - } - } - - // Convert Windows style newlines into unix styles - target_string = target_string.replace("\r\n", "\n"); - - // Calculate cursor rewind moves if a Cursor Hint is present - let index = target_string.find("$|$"); - let cursor_rewind = if let Some(index) = index { - // Convert the byte index to a char index - let char_str = &target_string[0..index]; - let char_index = char_str.chars().count(); - let total_size = target_string.chars().count(); - - // Remove the $|$ placeholder - target_string = target_string.replace("$|$", ""); - - // Calculate the amount of rewind moves needed (LEFT ARROW). - // Subtract also 3, equal to the number of chars of the placeholder "$|$" - let moves = (total_size - char_index - 3) as i32; - Some(moves) - } else { - None - }; - - // If the preserve_clipboard option is enabled, save the current - // clipboard content to restore it later. - previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); - - self.inject_text(&config, &target_string, m.force_clipboard, m.is_html); - - // Disallow undo backspace if cursor positioning is used or text is HTML - if cursor_rewind.is_none() && !m.is_html { - expansion_data = Some(( - m.triggers[trigger_offset].clone(), - target_string.chars().count() as i32, - )); - } - - if let Some(moves) = cursor_rewind { - // Simulate left arrow key presses to bring the cursor into the desired position - self.keyboard_manager.move_cursor_left(&config, moves); - } - } - RenderResult::Image(image_path) => { - // If the preserve_clipboard option is enabled, save the current - // clipboard content to restore it later. - previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); - - self.clipboard_manager.set_clipboard_image(&image_path); - self.keyboard_manager.trigger_paste(&config); - } - RenderResult::Error => { - error!("Could not render match: {}", m.triggers[trigger_offset]); - } - } - - // Restore previous clipboard content - if let Some(previous_clipboard_content) = previous_clipboard_content { - // Sometimes an expansion gets overwritten before pasting by the previous content - // A delay is needed to mitigate the problem - std::thread::sleep(std::time::Duration::from_millis( - config.restore_clipboard_delay as u64, - )); - - self.clipboard_manager - .set_clipboard(&previous_clipboard_content); - } - - expansion_data - } -} - -lazy_static! { - static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap(); -} - -impl< - 'a, - S: KeyboardManager, - C: ClipboardManager, - M: ConfigManager<'a>, - U: UIManager, - R: Renderer, - > MatchReceiver for Engine<'a, S, C, M, U, R> -{ - fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize) { - let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false); - let mut last_expansion_data = self.last_expansion_data.borrow_mut(); - (*last_expansion_data) = expansion_data; - } - - fn on_undo(&self) { - let config = self.config_manager.active_config(); - - if !config.undo_backspace { - return; - } - - // Block espanso from reinterpreting its own actions - let _inject_guard = InjectGuard::new(self.is_injecting.clone(), &config); - - let last_expansion_data = self.last_expansion_data.borrow(); - if let Some(ref last_expansion_data) = *last_expansion_data { - let (trigger_string, injected_text_len) = last_expansion_data; - // Delete the previously injected text, minus one character as it has been consumed by the backspace - self.keyboard_manager - .delete_string(&config, *injected_text_len - 1); - // Restore previous text - self.inject_text(&config, trigger_string, false, false); - } - } - - fn on_enable_update(&self, status: bool) { - let message = if status { - "espanso enabled" - } else { - "espanso disabled" - }; - - info!("Toggled: {}", message); - - let mut enabled_ref = self.enabled.borrow_mut(); - *enabled_ref = status; - - let config = self.config_manager.default_config(); - - if config.show_notifications { - self.ui_manager.notify(message); - } - - // Update the icon on supported OSes. - crate::context::update_icon(status); - } - - fn on_passive(&self) { - let config = self.config_manager.active_config(); - - if !config.enable_passive { - return; - } - - // Block espanso from reinterpreting its own actions - self.is_injecting.store(true, Release); - - // In order to avoid pasting previous clipboard contents, we need to check if - // a new clipboard was effectively copied. - // See issue: https://github.com/federico-terzi/espanso/issues/213 - let previous_clipboard = self.clipboard_manager.get_clipboard().unwrap_or_default(); - - // Sleep for a while, giving time to effectively copy the text - std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); - - // Clear the clipboard, for new-content detection later - self.clipboard_manager.set_clipboard(""); - - // Sleep for a while, giving time to effectively copy the text - std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); - - // Trigger a copy shortcut to transfer the content of the selection to the clipboard - self.keyboard_manager.trigger_copy(&config); - - // Sleep for a while, giving time to effectively copy the text - std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); - - // Then get the text from the clipboard and render the match output - let clipboard = self.clipboard_manager.get_clipboard(); - - if let Some(clipboard) = clipboard { - // Don't expand empty clipboards, as usually they are the result of an empty passive selection - if clipboard.trim().is_empty() { - info!("Avoiding passive expansion, as the user didn't select anything"); - } else { - info!("Passive mode activated"); - - // Restore original clipboard in case it's used during render - self.clipboard_manager.set_clipboard(&previous_clipboard); - - let rendered = self.renderer.render_passive(&clipboard, &config); - - match rendered { - RenderResult::Text(payload) => { - // Paste back the result in the field - self.clipboard_manager.set_clipboard(&payload); - - std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); - self.keyboard_manager.trigger_paste(&config); - } - _ => warn!("Cannot expand passive match"), - } - } - } - - std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); - - // Restore original clipboard - self.clipboard_manager.set_clipboard(&previous_clipboard); - - // Re-allow espanso to interpret actions - self.is_injecting.store(false, Release); - } -} - -impl< - 'a, - S: KeyboardManager, - C: ClipboardManager, - M: ConfigManager<'a>, - U: UIManager, - R: Renderer, - > ActionEventReceiver for Engine<'a, S, C, M, U, R> -{ - fn on_action_event(&self, e: ActionType) { - let config = self.config_manager.default_config(); - match e { - ActionType::IconClick => { - self.ui_manager.show_menu(self.build_menu()); - } - ActionType::ExitWorker => { - info!("terminating worker process"); - self.ui_manager.cleanup(); - exit(0); - } - ActionType::Exit => { - send_command_or_warn(Service::Daemon, config.clone(), IPCCommand::exit()); - } - ActionType::RestartWorker => { - send_command_or_warn( - Service::Daemon, - config.clone(), - IPCCommand::restart_worker(), - ); - } - _ => {} - } - } -} - -impl< - 'a, - S: KeyboardManager, - C: ClipboardManager, - M: ConfigManager<'a>, - U: UIManager, - R: Renderer, - > SystemEventReceiver for Engine<'a, S, C, M, U, R> -{ - fn on_system_event(&self, e: SystemEvent) { - match e { - // MacOS specific - SystemEvent::SecureInputEnabled(app_name, path) => { - info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path); - - let config = self.config_manager.default_config(); - if config.secure_input_notification && config.show_notifications { - self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000); - } - - crate::context::update_icon(false); - } - SystemEvent::SecureInputDisabled => { - info!("SecureInput has been disabled."); - - let is_enabled = self.enabled.borrow(); - crate::context::update_icon(*is_enabled); - } - SystemEvent::NotifyRequest(message) => { - let config = self.config_manager.default_config(); - if config.show_notifications { - self.ui_manager.notify(&message); - } - } - SystemEvent::Trigger(trigger) => { - let m = self.find_match_by_trigger(&trigger); - match m { - Some(m) => { - self.inject_match(&m, None, 0, true); - } - None => warn!("No match found with trigger: {}", trigger), - } - } - } - } -} diff --git a/src/event/manager.rs b/src/event/manager.rs deleted file mode 100644 index 5bafec4..0000000 --- a/src/event/manager.rs +++ /dev/null @@ -1,75 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::event::{ActionEventReceiver, Event, KeyEventReceiver, SystemEventReceiver}; -use std::sync::mpsc::Receiver; - -pub trait EventManager { - fn eventloop(&self); -} - -pub struct DefaultEventManager<'a> { - receive_channel: Receiver, - key_receivers: Vec<&'a dyn KeyEventReceiver>, - action_receivers: Vec<&'a dyn ActionEventReceiver>, - system_receivers: Vec<&'a dyn SystemEventReceiver>, -} - -impl<'a> DefaultEventManager<'a> { - pub fn new( - receive_channel: Receiver, - key_receivers: Vec<&'a dyn KeyEventReceiver>, - action_receivers: Vec<&'a dyn ActionEventReceiver>, - system_receivers: Vec<&'a dyn SystemEventReceiver>, - ) -> DefaultEventManager<'a> { - DefaultEventManager { - receive_channel, - key_receivers, - action_receivers, - system_receivers, - } - } -} - -impl<'a> EventManager for DefaultEventManager<'a> { - fn eventloop(&self) { - loop { - match self.receive_channel.recv() { - Ok(event) => match event { - Event::Key(key_event) => { - self.key_receivers - .iter() - .for_each(move |&receiver| receiver.on_key_event(key_event.clone())); - } - Event::Action(action_event) => { - self.action_receivers - .iter() - .for_each(|&receiver| receiver.on_action_event(action_event.clone())); - } - Event::System(system_event) => { - self.system_receivers.iter().for_each(move |&receiver| { - receiver.on_system_event(system_event.clone()) - }); - } - }, - Err(e) => panic!("Broken event channel {}", e), - } - } - } -} diff --git a/src/event/mod.rs b/src/event/mod.rs deleted file mode 100644 index a85f317..0000000 --- a/src/event/mod.rs +++ /dev/null @@ -1,211 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -pub(crate) mod manager; - -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub enum Event { - Action(ActionType), - Key(KeyEvent), - System(SystemEvent), -} - -#[derive(Debug, Clone)] -pub enum ActionType { - Noop = 0, - Toggle = 1, - Exit = 2, - IconClick = 3, - Enable = 4, - Disable = 5, - RestartWorker = 6, - ExitWorker = 7, -} - -impl From for ActionType { - fn from(id: i32) -> Self { - match id { - 1 => ActionType::Toggle, - 2 => ActionType::Exit, - 3 => ActionType::IconClick, - 4 => ActionType::Enable, - 5 => ActionType::Disable, - 6 => ActionType::RestartWorker, - 7 => ActionType::ExitWorker, - _ => ActionType::Noop, - } - } -} - -#[derive(Debug, Clone)] -pub enum KeyEvent { - Char(String), - Modifier(KeyModifier), - Other, -} - -#[allow(non_camel_case_types)] -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum KeyModifier { - CTRL, - SHIFT, - ALT, - META, - BACKSPACE, - OFF, - - // These are specific variants of the ones above. See issue: #117 - // https://github.com/federico-terzi/espanso/issues/117 - LEFT_CTRL, - RIGHT_CTRL, - LEFT_ALT, - RIGHT_ALT, - LEFT_META, - RIGHT_META, - LEFT_SHIFT, - RIGHT_SHIFT, - - // Special cases, should not be used in config - CAPS_LOCK, -} - -impl KeyModifier { - /// This function is used to compare KeyModifiers, considering the relations between - /// the generic modifier and the specific left/right variant - /// For example, CTRL will match with CTRL, LEFT_CTRL and RIGHT_CTRL; - /// but LEFT_CTRL will only match will LEFT_CTRL - pub fn shallow_equals(current: &KeyModifier, config: &KeyModifier) -> bool { - use KeyModifier::*; - - match config { - KeyModifier::CTRL => { - current == &LEFT_CTRL || current == &RIGHT_CTRL || current == &CTRL - } - KeyModifier::SHIFT => { - current == &LEFT_SHIFT || current == &RIGHT_SHIFT || current == &SHIFT - } - KeyModifier::ALT => current == &LEFT_ALT || current == &RIGHT_ALT || current == &ALT, - KeyModifier::META => { - current == &LEFT_META || current == &RIGHT_META || current == &META - } - KeyModifier::BACKSPACE => current == &BACKSPACE, - KeyModifier::LEFT_CTRL => current == &LEFT_CTRL, - KeyModifier::RIGHT_CTRL => current == &RIGHT_CTRL, - KeyModifier::LEFT_ALT => current == &LEFT_ALT, - KeyModifier::RIGHT_ALT => current == &RIGHT_ALT, - KeyModifier::LEFT_META => current == &LEFT_META, - KeyModifier::RIGHT_META => current == &RIGHT_META, - KeyModifier::LEFT_SHIFT => current == &LEFT_SHIFT, - KeyModifier::RIGHT_SHIFT => current == &RIGHT_SHIFT, - _ => false, - } - } -} - -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub enum SystemEvent { - // MacOS specific - SecureInputEnabled(String, String), // AppName, App Path - SecureInputDisabled, - - // Notification - NotifyRequest(String), - - // Trigger an expansion from IPC - Trigger(String), -} - -// Receivers - -pub trait KeyEventReceiver { - fn on_key_event(&self, e: KeyEvent); -} - -pub trait ActionEventReceiver { - fn on_action_event(&self, e: ActionType); -} - -pub trait SystemEventReceiver { - fn on_system_event(&self, e: SystemEvent); -} - -// TESTS - -#[cfg(test)] -mod tests { - use super::KeyModifier::*; - use super::*; - - #[test] - fn test_shallow_equals_ctrl() { - assert!(KeyModifier::shallow_equals(&CTRL, &CTRL)); - assert!(KeyModifier::shallow_equals(&LEFT_CTRL, &CTRL)); - assert!(KeyModifier::shallow_equals(&RIGHT_CTRL, &CTRL)); - - assert!(!KeyModifier::shallow_equals(&CTRL, &LEFT_CTRL)); - assert!(!KeyModifier::shallow_equals(&CTRL, &RIGHT_CTRL)); - } - - #[test] - fn test_shallow_equals_shift() { - assert!(KeyModifier::shallow_equals(&SHIFT, &SHIFT)); - assert!(KeyModifier::shallow_equals(&LEFT_SHIFT, &SHIFT)); - assert!(KeyModifier::shallow_equals(&RIGHT_SHIFT, &SHIFT)); - - assert!(!KeyModifier::shallow_equals(&SHIFT, &LEFT_SHIFT)); - assert!(!KeyModifier::shallow_equals(&SHIFT, &RIGHT_SHIFT)); - } - - #[test] - fn test_shallow_equals_alt() { - assert!(KeyModifier::shallow_equals(&ALT, &ALT)); - assert!(KeyModifier::shallow_equals(&LEFT_ALT, &ALT)); - assert!(KeyModifier::shallow_equals(&RIGHT_ALT, &ALT)); - - assert!(!KeyModifier::shallow_equals(&ALT, &LEFT_ALT)); - assert!(!KeyModifier::shallow_equals(&ALT, &RIGHT_ALT)); - } - - #[test] - fn test_shallow_equals_meta() { - assert!(KeyModifier::shallow_equals(&META, &META)); - assert!(KeyModifier::shallow_equals(&LEFT_META, &META)); - assert!(KeyModifier::shallow_equals(&RIGHT_META, &META)); - - assert!(!KeyModifier::shallow_equals(&META, &LEFT_META)); - assert!(!KeyModifier::shallow_equals(&META, &RIGHT_META)); - } - - #[test] - fn test_shallow_equals_backspace() { - assert!(KeyModifier::shallow_equals(&BACKSPACE, &BACKSPACE)); - } - - #[test] - fn test_shallow_equals_off() { - assert!(!KeyModifier::shallow_equals(&OFF, &CTRL)); - assert!(!KeyModifier::shallow_equals(&OFF, &ALT)); - assert!(!KeyModifier::shallow_equals(&OFF, &META)); - assert!(!KeyModifier::shallow_equals(&OFF, &SHIFT)); - } -} diff --git a/src/extension/clipboard.rs b/src/extension/clipboard.rs deleted file mode 100644 index d82112d..0000000 --- a/src/extension/clipboard.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::clipboard::ClipboardManager; -use crate::extension::ExtensionResult; -use serde_yaml::Mapping; -use std::collections::HashMap; - -use super::ExtensionOut; - -pub struct ClipboardExtension { - clipboard_manager: Box, -} - -impl ClipboardExtension { - pub fn new(clipboard_manager: Box) -> ClipboardExtension { - ClipboardExtension { clipboard_manager } - } -} - -impl super::Extension for ClipboardExtension { - fn name(&self) -> String { - String::from("clipboard") - } - - fn calculate( - &self, - _: &Mapping, - _: &Vec, - _: &HashMap, - ) -> ExtensionOut { - if let Some(clipboard) = self.clipboard_manager.get_clipboard() { - Ok(Some(ExtensionResult::Single(clipboard))) - } else { - Ok(None) - } - } -} diff --git a/src/extension/date.rs b/src/extension/date.rs deleted file mode 100644 index 90e1ecc..0000000 --- a/src/extension/date.rs +++ /dev/null @@ -1,66 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019-2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use chrono::{DateTime, Duration, Local}; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; - -use super::ExtensionOut; - -pub struct DateExtension {} - -impl DateExtension { - pub fn new() -> DateExtension { - DateExtension {} - } -} - -impl super::Extension for DateExtension { - fn name(&self) -> String { - String::from("date") - } - - fn calculate( - &self, - params: &Mapping, - _: &Vec, - _: &HashMap, - ) -> ExtensionOut { - let mut now: DateTime = Local::now(); - - // Compute the given offset - let offset = params.get(&Value::from("offset")); - if let Some(offset) = offset { - let seconds = offset.as_i64().unwrap_or_else(|| 0); - let offset = Duration::seconds(seconds); - now = now + offset; - } - - let format = params.get(&Value::from("format")); - - let date = if let Some(format) = format { - now.format(format.as_str().unwrap()).to_string() - } else { - now.to_rfc2822() - }; - - Ok(Some(ExtensionResult::Single(date))) - } -} diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs deleted file mode 100644 index 030b71d..0000000 --- a/src/extension/dummy.rs +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; - -pub struct DummyExtension { - name: String, -} - -impl DummyExtension { - pub fn new(name: &str) -> DummyExtension { - DummyExtension { - name: name.to_owned(), - } - } -} - -impl super::Extension for DummyExtension { - fn name(&self) -> String { - self.name.clone() - } - - fn calculate( - &self, - params: &Mapping, - _: &Vec, - _: &HashMap, - ) -> super::ExtensionOut { - let echo = params.get(&Value::from("echo")); - - if let Some(echo) = echo { - Ok(Some(ExtensionResult::Single( - echo.as_str().unwrap_or_default().to_owned(), - ))) - } else { - Ok(None) - } - } -} diff --git a/src/extension/form.rs b/src/extension/form.rs deleted file mode 100644 index ac0858d..0000000 --- a/src/extension/form.rs +++ /dev/null @@ -1,123 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::{config::Configs, extension::ExtensionResult, ui::modulo::ModuloManager}; -use log::{error, warn}; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; - -pub struct FormExtension { - manager: ModuloManager, -} - -impl FormExtension { - pub fn new(config: &Configs) -> FormExtension { - let manager = ModuloManager::new(config); - FormExtension { manager } - } -} - -impl super::Extension for FormExtension { - fn name(&self) -> String { - "form".to_owned() - } - - fn calculate( - &self, - params: &Mapping, - _: &Vec, - _: &HashMap, - ) -> super::ExtensionOut { - let layout = params.get(&Value::from("layout")); - let layout = if let Some(value) = layout { - value.as_str().unwrap_or_default().to_string() - } else { - error!("invoking form extension without specifying a layout"); - return Err(super::ExtensionError::Internal); - }; - - let mut form_config = Mapping::new(); - form_config.insert(Value::from("title"), Value::from("espanso")); - form_config.insert(Value::from("layout"), Value::from(layout)); - - if let Some(fields) = params.get(&Value::from("fields")) { - form_config.insert(Value::from("fields"), fields.clone()); - } - - if let Some(icon_path) = crate::context::get_icon_path() { - form_config.insert( - Value::from("icon"), - Value::from(icon_path.to_string_lossy().to_string()), - ); - } - - let serialized_config: String = - serde_yaml::to_string(&form_config).expect("unable to serialize form config"); - - let output = self - .manager - .invoke(&["form", "-i", "-"], &serialized_config); - - // On macOS and Windows, after the form closes we have to wait until the user releases the modifier keys - on_form_close(); - - if let Some(output) = output { - let json: Result, _> = serde_json::from_str(&output); - match json { - Ok(json) => { - // Check if the JSON is empty. In those cases, it means the user exited - // the form before submitting it, therefore the expansion should stop - if json.is_empty() { - return Err(super::ExtensionError::Aborted); - } - - return Ok(Some(ExtensionResult::Multiple(json))); - } - Err(error) => { - error!("modulo json parsing error: {}", error); - return Err(super::ExtensionError::Internal); - } - } - } else { - error!("modulo form didn't return any output"); - return Err(super::ExtensionError::Internal); - } - } -} - -#[cfg(target_os = "linux")] -fn on_form_close() { - // NOOP on Linux -} - -#[cfg(target_os = "windows")] -fn on_form_close() { - let released = crate::keyboard::windows::wait_for_modifiers_release(); - if !released { - warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)"); - } -} - -#[cfg(target_os = "macos")] -fn on_form_close() { - let released = crate::keyboard::macos::wait_for_modifiers_release(); - if !released { - warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)"); - } -} diff --git a/src/extension/mod.rs b/src/extension/mod.rs deleted file mode 100644 index a98a138..0000000 --- a/src/extension/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::{clipboard::ClipboardManager, config::Configs}; -use serde_yaml::Mapping; -use std::collections::HashMap; - -mod clipboard; -mod date; -pub mod dummy; -mod form; -pub mod multiecho; -mod random; -mod script; -mod shell; -mod utils; -pub mod vardummy; - -#[derive(Clone, Debug, PartialEq)] -pub enum ExtensionResult { - Single(String), - Multiple(HashMap), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum ExtensionError { - // Returned by an extension if an internal process occurred - Internal, - // Returned by an extension if the user aborted the expansion - // for example when pressing ESC inside a FormExtension. - Aborted, -} - -pub type ExtensionOut = Result, ExtensionError>; - -pub trait Extension { - fn name(&self) -> String; - fn calculate( - &self, - params: &Mapping, - args: &Vec, - current_vars: &HashMap, - ) -> ExtensionOut; -} - -pub fn get_extensions( - config: &Configs, - clipboard_manager: Box, -) -> Vec> { - vec![ - Box::new(date::DateExtension::new()), - Box::new(shell::ShellExtension::new()), - Box::new(script::ScriptExtension::new()), - Box::new(random::RandomExtension::new()), - Box::new(multiecho::MultiEchoExtension::new()), - Box::new(dummy::DummyExtension::new("dummy")), - Box::new(dummy::DummyExtension::new("echo")), - Box::new(clipboard::ClipboardExtension::new(clipboard_manager)), - Box::new(form::FormExtension::new(config)), - ] -} diff --git a/src/extension/multiecho.rs b/src/extension/multiecho.rs deleted file mode 100644 index 99ec02f..0000000 --- a/src/extension/multiecho.rs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; - -pub struct MultiEchoExtension {} - -impl MultiEchoExtension { - pub fn new() -> MultiEchoExtension { - MultiEchoExtension {} - } -} - -impl super::Extension for MultiEchoExtension { - fn name(&self) -> String { - "multiecho".to_owned() - } - - fn calculate( - &self, - params: &Mapping, - _: &Vec, - _: &HashMap, - ) -> super::ExtensionOut { - let mut output: HashMap = HashMap::new(); - for (key, value) in params.iter() { - if let Some(key) = key.as_str() { - if let Some(value) = value.as_str() { - output.insert(key.to_owned(), value.to_owned()); - } - } - } - Ok(Some(ExtensionResult::Multiple(output))) - } -} diff --git a/src/extension/random.rs b/src/extension/random.rs deleted file mode 100644 index 54c21e3..0000000 --- a/src/extension/random.rs +++ /dev/null @@ -1,125 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use log::{error, warn}; -use rand::seq::SliceRandom; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; - -pub struct RandomExtension {} - -impl RandomExtension { - pub fn new() -> RandomExtension { - RandomExtension {} - } -} - -impl super::Extension for RandomExtension { - fn name(&self) -> String { - String::from("random") - } - - fn calculate( - &self, - params: &Mapping, - args: &Vec, - _: &HashMap, - ) -> super::ExtensionOut { - let choices = params.get(&Value::from("choices")); - if choices.is_none() { - warn!("No 'choices' parameter specified for random variable"); - return Ok(None); - } - let choices = choices.unwrap().as_sequence(); - if let Some(choices) = choices { - let str_choices = choices - .iter() - .map(|arg| arg.as_str().unwrap_or_default().to_string()) - .collect::>(); - - // Select a random choice between the possibilities - let choice = str_choices.choose(&mut rand::thread_rng()); - - match choice { - Some(output) => { - // Render arguments - let output = crate::render::utils::render_args(output, args); - - return Ok(Some(ExtensionResult::Single(output))); - } - None => { - error!("Could not select a random choice."); - return Err(super::ExtensionError::Internal); - } - } - } - - error!("choices array have an invalid format '{:?}'", choices); - Err(super::ExtensionError::Internal) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::extension::Extension; - - #[test] - fn test_random_basic() { - let mut params = Mapping::new(); - let choices = vec!["first", "second", "third"]; - params.insert(Value::from("choices"), Value::from(choices.clone())); - - let extension = RandomExtension::new(); - let output = extension - .calculate(¶ms, &vec![], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - - let output = output.unwrap(); - - assert!(choices - .into_iter() - .any(|x| ExtensionResult::Single(x.to_owned()) == output)); - } - - #[test] - fn test_random_with_args() { - let mut params = Mapping::new(); - let choices = vec!["first $0$", "second $0$", "$0$ third"]; - params.insert(Value::from("choices"), Value::from(choices.clone())); - - let extension = RandomExtension::new(); - let output = extension - .calculate(¶ms, &vec!["test".to_owned()], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - - let output = output.unwrap(); - - let rendered_choices = vec!["first test", "second test", "test third"]; - - assert!(rendered_choices - .into_iter() - .any(|x| ExtensionResult::Single(x.to_owned()) == output)); - } -} diff --git a/src/extension/script.rs b/src/extension/script.rs deleted file mode 100644 index 0c92a4c..0000000 --- a/src/extension/script.rs +++ /dev/null @@ -1,272 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use log::{error, warn}; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::process::Command; - -pub struct ScriptExtension {} - -impl ScriptExtension { - pub fn new() -> ScriptExtension { - ScriptExtension {} - } -} - -impl super::Extension for ScriptExtension { - fn name(&self) -> String { - String::from("script") - } - - fn calculate( - &self, - params: &Mapping, - user_args: &Vec, - vars: &HashMap, - ) -> super::ExtensionOut { - let args = params.get(&Value::from("args")); - if args.is_none() { - warn!("No 'args' parameter specified for script variable"); - return Err(super::ExtensionError::Internal); - } - let args = args.unwrap().as_sequence(); - if let Some(args) = args { - let mut str_args = args - .iter() - .map(|arg| arg.as_str().unwrap_or_default().to_string()) - .collect::>(); - - // The user has to enable argument concatenation explicitly - let inject_args = params - .get(&Value::from("inject_args")) - .unwrap_or(&Value::from(false)) - .as_bool() - .unwrap_or(false); - if inject_args { - str_args.extend(user_args.clone()); - } - - // Replace %HOME% with current user home directory to - // create cross-platform paths. See issue #265 - // Also replace %CONFIG% and %PACKAGES% path. See issue #380 - let home_dir = dirs::home_dir().unwrap_or_default(); - str_args.iter_mut().for_each(|arg| { - if arg.contains("%HOME%") { - *arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string()); - } - if arg.contains("%CONFIG%") { - *arg = arg.replace( - "%CONFIG%", - &crate::context::get_config_dir() - .to_string_lossy() - .to_string(), - ); - } - if arg.contains("%PACKAGES%") { - *arg = arg.replace( - "%PACKAGES%", - &crate::context::get_package_dir() - .to_string_lossy() - .to_string(), - ); - } - - // On Windows, correct paths separators - if cfg!(target_os = "windows") { - let path = PathBuf::from(&arg); - if path.exists() { - *arg = path.to_string_lossy().to_string() - } - } - }); - - let mut command = Command::new(&str_args[0]); - - // Set the OS-specific flags - crate::utils::set_command_flags(&mut command); - - // Inject the $CONFIG variable - command.env("CONFIG", crate::context::get_config_dir()); - - // Inject all the env variables - let env_variables = super::utils::convert_to_env_variables(&vars); - for (key, value) in env_variables.iter() { - command.env(key, value); - } - - let output = if str_args.len() > 1 { - command.args(&str_args[1..]).output() - } else { - command.output() - }; - - match output { - Ok(output) => { - let mut output_str = - String::from_utf8_lossy(output.stdout.as_slice()).to_string(); - let error_str = String::from_utf8_lossy(output.stderr.as_slice()); - let error_str = error_str.to_string(); - let error_str = error_str.trim(); - - // Print stderror if present - if !error_str.is_empty() { - warn!("Script command reported error: \n{}", error_str); - } - - // If specified, trim the output - let trim_opt = params.get(&Value::from("trim")); - let should_trim = if let Some(value) = trim_opt { - let val = value.as_bool(); - val.unwrap_or(true) - } else { - true - }; - - if should_trim { - output_str = output_str.trim().to_owned() - } - - return Ok(Some(ExtensionResult::Single(output_str))); - } - Err(e) => { - error!("Could not execute script '{:?}', error: {}", args, e); - return Err(super::ExtensionError::Internal); - } - } - } - - error!("Could not execute script with args '{:?}'", args); - Err(super::ExtensionError::Internal) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::extension::Extension; - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_script_basic() { - let mut params = Mapping::new(); - params.insert( - Value::from("args"), - Value::from(vec!["echo", "hello world"]), - ); - - let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec![], &HashMap::new()).unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world".to_owned()) - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_script_basic_no_trim() { - let mut params = Mapping::new(); - params.insert( - Value::from("args"), - Value::from(vec!["echo", "hello world"]), - ); - params.insert(Value::from("trim"), Value::from(false)); - - let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec![], &HashMap::new()).unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world\n".to_owned()) - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_script_inject_args_off() { - let mut params = Mapping::new(); - params.insert( - Value::from("args"), - Value::from(vec!["echo", "hello world"]), - ); - - let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()).unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world".to_owned()) - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_script_inject_args_on() { - let mut params = Mapping::new(); - params.insert( - Value::from("args"), - Value::from(vec!["echo", "hello world"]), - ); - params.insert(Value::from("inject_args"), Value::from(true)); - - let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()).unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world jon".to_owned()) - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_script_var_injection() { - let mut params = Mapping::new(); - params.insert( - Value::from("args"), - Value::from(vec!["bash", "-c", "echo $ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]), - ); - - let mut vars: HashMap = HashMap::new(); - let mut subvars = HashMap::new(); - subvars.insert("name".to_owned(), "John".to_owned()); - vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars)); - vars.insert( - "var1".to_owned(), - ExtensionResult::Single("hello".to_owned()), - ); - - let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec![], &vars).unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello John".to_owned()) - ); - } -} diff --git a/src/extension/shell.rs b/src/extension/shell.rs deleted file mode 100644 index b5e4dbe..0000000 --- a/src/extension/shell.rs +++ /dev/null @@ -1,470 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use log::{error, info, warn}; -use regex::{Captures, Regex}; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; -use std::process::{Command, Output}; - -lazy_static! { - static ref UNIX_POS_ARG_REGEX: Regex = Regex::new("\\$(?P\\d+)").unwrap(); - static ref WIN_POS_ARG_REGEX: Regex = Regex::new("%(?P\\d+)").unwrap(); -} - -pub enum Shell { - Cmd, - Powershell, - WSL, - WSL2, - Bash, - Sh, -} - -impl Shell { - fn execute_cmd(&self, cmd: &str, vars: &HashMap) -> std::io::Result { - let mut is_wsl = false; - - let mut command = match self { - Shell::Cmd => { - let mut command = Command::new("cmd"); - command.args(&["/C", &cmd]); - command - } - Shell::Powershell => { - let mut command = Command::new("powershell"); - command.args(&["-Command", &cmd]); - command - } - Shell::WSL => { - is_wsl = true; - let mut command = Command::new("bash"); - command.args(&["-c", &cmd]); - command - } - Shell::WSL2 => { - is_wsl = true; - let mut command = Command::new("wsl"); - command.args(&["bash", "-c", &cmd]); - command - } - Shell::Bash => { - let mut command = Command::new("bash"); - command.args(&["-c", &cmd]); - command - } - Shell::Sh => { - let mut command = Command::new("sh"); - command.args(&["-c", &cmd]); - command - } - }; - - // Set the OS-specific flags - crate::utils::set_command_flags(&mut command); - - // Inject the $CONFIG variable - command.env("CONFIG", crate::context::get_config_dir()); - - // Inject all the previous variables - for (key, value) in vars.iter() { - command.env(key, value); - } - - // In WSL environment, we have to specify which ENV variables - // should be passed to linux. - // For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/ - if is_wsl { - let mut tokens: Vec<&str> = Vec::new(); - tokens.push("CONFIG/p"); - - // Add all the previous variables - for (key, _) in vars.iter() { - tokens.push(key); - } - - let wsl_env = tokens.join(":"); - command.env("WSLENV", wsl_env); - } - - command.output() - } - - fn from_string(shell: &str) -> Option { - match shell { - "cmd" => Some(Shell::Cmd), - "powershell" => Some(Shell::Powershell), - "wsl" => Some(Shell::WSL), - "wsl2" => Some(Shell::WSL2), - "bash" => Some(Shell::Bash), - "sh" => Some(Shell::Sh), - _ => None, - } - } - - fn get_arg_regex(&self) -> &Regex { - let regex = match self { - Shell::Cmd | Shell::Powershell => &*WIN_POS_ARG_REGEX, - _ => &*UNIX_POS_ARG_REGEX, - }; - regex - } -} - -impl Default for Shell { - fn default() -> Shell { - if cfg!(target_os = "windows") { - Shell::Powershell - } else if cfg!(target_os = "macos") { - Shell::Sh - } else if cfg!(target_os = "linux") { - Shell::Bash - } else { - panic!("invalid target os for shell") - } - } -} - -pub struct ShellExtension {} - -impl ShellExtension { - pub fn new() -> ShellExtension { - ShellExtension {} - } -} - -impl super::Extension for ShellExtension { - fn name(&self) -> String { - String::from("shell") - } - - fn calculate( - &self, - params: &Mapping, - args: &Vec, - vars: &HashMap, - ) -> super::ExtensionOut { - let cmd = params.get(&Value::from("cmd")); - if cmd.is_none() { - warn!("No 'cmd' parameter specified for shell variable"); - return Err(super::ExtensionError::Internal); - } - - let inject_args = params - .get(&Value::from("inject_args")) - .unwrap_or(&Value::from(false)) - .as_bool() - .unwrap_or(false); - - let original_cmd = cmd.unwrap().as_str().unwrap(); - - let shell_param = params.get(&Value::from("shell")); - let shell = if let Some(shell_param) = shell_param { - let shell_param = shell_param.as_str().expect("invalid shell parameter"); - let shell = Shell::from_string(shell_param); - - if shell.is_none() { - error!("Invalid shell parameter, please select a valid one."); - return Err(super::ExtensionError::Internal); - } - - shell.unwrap() - } else { - Shell::default() - }; - - // Render positional parameters in args - let cmd = if inject_args { - shell - .get_arg_regex() - .replace_all(&original_cmd, |caps: &Captures| { - let position_str = caps.name("pos").unwrap().as_str(); - let position = position_str.parse::().unwrap_or(-1); - if position >= 0 && position < args.len() as i32 { - args[position as usize].to_owned() - } else { - "".to_owned() - } - }) - .to_string() - } else { - original_cmd.to_owned() - }; - - let env_variables = super::utils::convert_to_env_variables(&vars); - - let output = shell.execute_cmd(&cmd, &env_variables); - - match output { - Ok(output) => { - let output_str = String::from_utf8_lossy(output.stdout.as_slice()); - let mut output_str = output_str.into_owned(); - let error_str = String::from_utf8_lossy(output.stderr.as_slice()); - let error_str = error_str.to_string(); - let error_str = error_str.trim(); - - // Print stderror if present - if !error_str.is_empty() { - warn!("Shell command reported error: \n{}", error_str); - } - - // Check if debug flag set, provide additional context when an error occurs. - let debug_opt = params.get(&Value::from("debug")); - let with_debug = if let Some(value) = debug_opt { - let val = value.as_bool(); - val.unwrap_or(false) - } else { - false - }; - - if with_debug { - info!( - "debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'", - original_cmd, output.status, output_str, error_str - ); - } - - // If specified, trim the output - let trim_opt = params.get(&Value::from("trim")); - let should_trim = if let Some(value) = trim_opt { - let val = value.as_bool(); - val.unwrap_or(true) - } else { - true - }; - - if should_trim { - output_str = output_str.trim().to_owned() - } - - Ok(Some(ExtensionResult::Single(output_str))) - } - Err(e) => { - error!("Could not execute cmd '{}', error: {}", cmd, e); - Err(super::ExtensionError::Internal) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::extension::Extension; - - #[test] - fn test_shell_not_trimmed() { - let mut params = Mapping::new(); - params.insert(Value::from("cmd"), Value::from("echo \"hello world\"")); - params.insert(Value::from("trim"), Value::from(false)); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec![], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - - if cfg!(target_os = "windows") { - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world\r\n".to_owned()) - ); - } else { - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world\n".to_owned()) - ); - } - } - - #[test] - fn test_shell_basic() { - let mut params = Mapping::new(); - params.insert(Value::from("cmd"), Value::from("echo \"hello world\"")); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec![], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world".to_owned()) - ); - } - - #[test] - fn test_shell_trimmed_2() { - let mut params = Mapping::new(); - params.insert( - Value::from("cmd"), - Value::from("echo \" hello world \""), - ); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec![], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world".to_owned()) - ); - } - - #[test] - fn test_shell_trimmed_malformed() { - let mut params = Mapping::new(); - params.insert(Value::from("cmd"), Value::from("echo \"hello world\"")); - params.insert(Value::from("trim"), Value::from("error")); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec![], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world".to_owned()) - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_shell_pipes() { - let mut params = Mapping::new(); - params.insert(Value::from("cmd"), Value::from("echo hello world | cat")); - params.insert(Value::from("trim"), Value::from(true)); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec![], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - assert_eq!( - output.unwrap(), - ExtensionResult::Single("hello world".to_owned()) - ); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_shell_args_unix() { - let mut params = Mapping::new(); - params.insert(Value::from("cmd"), Value::from("echo $0")); - params.insert(Value::from("inject_args"), Value::from(true)); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - - assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned())); - } - - #[test] - #[cfg(not(target_os = "windows"))] - fn test_shell_no_default_inject_args_unix() { - let mut params = Mapping::new(); - params.insert( - Value::from("cmd"), - Value::from("echo 'hey friend' | awk '{ print $2 }'"), - ); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - - assert_eq!( - output.unwrap(), - ExtensionResult::Single("friend".to_owned()) - ); - } - - #[test] - #[cfg(target_os = "windows")] - fn test_shell_args_windows() { - let mut params = Mapping::new(); - params.insert(Value::from("cmd"), Value::from("echo %0")); - params.insert(Value::from("inject_args"), Value::from(true)); - - let extension = ShellExtension::new(); - let output = extension - .calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new()) - .unwrap(); - - assert!(output.is_some()); - - assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned())); - } - - #[test] - fn test_shell_vars_single_injection() { - let mut params = Mapping::new(); - if cfg!(target_os = "windows") { - params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_VAR1%")); - params.insert(Value::from("shell"), Value::from("cmd")); - } else { - params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_VAR1")); - } - - let extension = ShellExtension::new(); - let mut vars: HashMap = HashMap::new(); - vars.insert( - "var1".to_owned(), - ExtensionResult::Single("hello".to_owned()), - ); - let output = extension.calculate(¶ms, &vec![], &vars).unwrap(); - - assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned())); - } - - #[test] - fn test_shell_vars_multiple_injection() { - let mut params = Mapping::new(); - if cfg!(target_os = "windows") { - params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_FORM1_NAME%")); - params.insert(Value::from("shell"), Value::from("cmd")); - } else { - params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_FORM1_NAME")); - } - - let extension = ShellExtension::new(); - let mut vars: HashMap = HashMap::new(); - let mut subvars = HashMap::new(); - subvars.insert("name".to_owned(), "John".to_owned()); - vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars)); - let output = extension.calculate(¶ms, &vec![], &vars).unwrap(); - - assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned())); - } -} diff --git a/src/extension/utils.rs b/src/extension/utils.rs deleted file mode 100644 index fcf5bac..0000000 --- a/src/extension/utils.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::extension::ExtensionResult; -use std::collections::HashMap; - -pub fn convert_to_env_variables( - original_vars: &HashMap, -) -> HashMap { - let mut output = HashMap::new(); - - for (key, result) in original_vars.iter() { - match result { - ExtensionResult::Single(value) => { - let name = format!("ESPANSO_{}", key.to_uppercase()); - output.insert(name, value.clone()); - } - ExtensionResult::Multiple(values) => { - for (sub_key, sub_value) in values.iter() { - let name = format!("ESPANSO_{}_{}", key.to_uppercase(), sub_key.to_uppercase()); - output.insert(name, sub_value.clone()); - } - } - } - } - - output -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::extension::Extension; - - #[test] - fn test_convert_to_env_variables() { - let mut vars: HashMap = HashMap::new(); - let mut subvars = HashMap::new(); - subvars.insert("name".to_owned(), "John".to_owned()); - subvars.insert("lastname".to_owned(), "Snow".to_owned()); - vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars)); - vars.insert( - "var1".to_owned(), - ExtensionResult::Single("test".to_owned()), - ); - - let output = convert_to_env_variables(&vars); - assert_eq!(output.get("ESPANSO_FORM1_NAME").unwrap(), "John"); - assert_eq!(output.get("ESPANSO_FORM1_LASTNAME").unwrap(), "Snow"); - assert_eq!(output.get("ESPANSO_VAR1").unwrap(), "test"); - } -} diff --git a/src/extension/vardummy.rs b/src/extension/vardummy.rs deleted file mode 100644 index d683267..0000000 --- a/src/extension/vardummy.rs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::extension::ExtensionResult; -use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; - -pub struct VarDummyExtension {} - -impl VarDummyExtension { - pub fn new() -> Self { - Self {} - } -} - -impl super::Extension for VarDummyExtension { - fn name(&self) -> String { - "vardummy".to_owned() - } - - fn calculate( - &self, - params: &Mapping, - _: &Vec, - vars: &HashMap, - ) -> super::ExtensionOut { - let target = params.get(&Value::from("target")); - - if let Some(target) = target { - let value = vars.get(target.as_str().unwrap_or_default()); - Ok(Some(value.unwrap().clone())) - } else { - Ok(None) - } - } -} diff --git a/src/guard.rs b/src/guard.rs deleted file mode 100644 index 036b283..0000000 --- a/src/guard.rs +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::Configs; -use log::debug; -use std::sync::atomic::Ordering::Release; -use std::sync::{atomic::AtomicBool, Arc}; - -pub struct InjectGuard { - is_injecting: Arc, - post_inject_delay: u64, -} - -impl InjectGuard { - pub fn new(is_injecting: Arc, config: &Configs) -> Self { - debug!("enabling inject guard"); - - // Enable the injecting block - is_injecting.store(true, Release); - - Self { - is_injecting, - post_inject_delay: config.post_inject_delay, - } - } -} - -impl Drop for InjectGuard { - fn drop(&mut self) { - // Because the keyinjection is async, we need to wait a bit before - // giving back the control. Otherwise, the injected actions will be handled back - // by espanso itself. - std::thread::sleep(std::time::Duration::from_millis(self.post_inject_delay)); - - debug!("releasing inject guard"); - - // Re-allow espanso to interpret actions - self.is_injecting.store(false, Release); - } -} diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs deleted file mode 100644 index dd05f67..0000000 --- a/src/keyboard/linux.rs +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::PasteShortcut; -use crate::bridge::linux::*; -use crate::config::Configs; -use log::error; -use std::ffi::CString; - -pub struct LinuxKeyboardManager {} - -impl super::KeyboardManager for LinuxKeyboardManager { - fn send_string(&self, active_config: &Configs, s: &str) { - let res = CString::new(s); - match res { - Ok(cstr) => unsafe { - if active_config.fast_inject { - fast_send_string(cstr.as_ptr(), active_config.inject_delay); - } else { - send_string(cstr.as_ptr()); - } - }, - Err(e) => panic!(e.to_string()), - } - } - - fn send_enter(&self, active_config: &Configs) { - unsafe { - if active_config.fast_inject { - fast_send_enter(); - } else { - send_enter(); - } - } - } - - fn trigger_paste(&self, active_config: &Configs) { - unsafe { - match active_config.paste_shortcut { - PasteShortcut::Default => { - let is_special = is_current_window_special(); - - // Terminals use a different keyboard combination to paste from clipboard, - // so we need to check the correct situation. - if is_special == 0 { - trigger_paste(); - }else if is_special == 2 { // Special case for stterm - trigger_alt_shift_ins_paste(); - }else if is_special == 3 { // Special case for Emacs - trigger_shift_ins_paste(); - }else if is_special == 4 { // CTRL+ALT+V used in some terminals (urxvt) - trigger_ctrl_alt_paste(); - }else{ - trigger_terminal_paste(); - } - }, - PasteShortcut::CtrlV => { - trigger_paste(); - }, - PasteShortcut::CtrlShiftV => { - trigger_terminal_paste(); - }, - PasteShortcut::ShiftInsert=> { - trigger_shift_ins_paste(); - }, - PasteShortcut::CtrlAltV => { - trigger_ctrl_alt_paste(); - }, - _ => { - error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") - } - } - } - } - - fn delete_string(&self, active_config: &Configs, count: i32) { - unsafe { - if active_config.fast_inject { - fast_delete_string(count, active_config.backspace_delay); - } else { - delete_string(count) - } - } - } - - fn move_cursor_left(&self, active_config: &Configs, count: i32) { - unsafe { - if active_config.fast_inject { - fast_left_arrow(count); - } else { - left_arrow(count); - } - } - } - - fn trigger_copy(&self, _: &Configs) { - unsafe { - trigger_copy(); - } - } -} diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs deleted file mode 100644 index b7d9699..0000000 --- a/src/keyboard/macos.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::PasteShortcut; -use crate::bridge::macos::*; -use crate::config::Configs; -use log::error; -use std::ffi::CString; - -pub struct MacKeyboardManager {} - -impl super::KeyboardManager for MacKeyboardManager { - fn send_string(&self, _: &Configs, s: &str) { - let res = CString::new(s); - match res { - Ok(cstr) => unsafe { - send_string(cstr.as_ptr()); - }, - Err(e) => panic!(e.to_string()), - } - } - - fn send_enter(&self, _: &Configs) { - unsafe { - // Send the kVK_Return key press - send_vkey(0x24); - } - } - - fn trigger_paste(&self, active_config: &Configs) { - unsafe { - match active_config.paste_shortcut { - PasteShortcut::Default => { - unsafe { - trigger_paste(); - } - }, - _ => { - error!("MacOS backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") - } - } - } - } - - fn trigger_copy(&self, _: &Configs) { - unsafe { - trigger_copy(); - } - } - - fn delete_string(&self, _: &Configs, count: i32) { - unsafe { delete_string(count) } - } - - fn move_cursor_left(&self, _: &Configs, count: i32) { - unsafe { - // Simulate the Left arrow count times - send_multi_vkey(0x7B, count); - } - } -} - -pub fn wait_for_modifiers_release() -> bool { - let start = std::time::SystemTime::now(); - while start.elapsed().unwrap_or_default().as_millis() < 3000 { - let pressed = unsafe { crate::bridge::macos::are_modifiers_pressed() }; - if pressed == 0 { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - false -} diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs deleted file mode 100644 index d9f11a4..0000000 --- a/src/keyboard/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::Configs; -use log::warn; -use serde::{Deserialize, Serialize}; - -#[cfg(target_os = "windows")] -pub mod windows; - -#[cfg(target_os = "linux")] -mod linux; - -#[cfg(target_os = "macos")] -pub mod macos; - -pub trait KeyboardManager { - fn send_string(&self, active_config: &Configs, s: &str); - fn send_enter(&self, active_config: &Configs); - fn trigger_paste(&self, active_config: &Configs); - fn delete_string(&self, active_config: &Configs, count: i32); - fn move_cursor_left(&self, active_config: &Configs, count: i32); - fn trigger_copy(&self, active_config: &Configs); -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum PasteShortcut { - Default, // Default one for the current system - CtrlV, // Classic Ctrl+V shortcut - CtrlShiftV, // Could be used to paste without formatting in many applications - ShiftInsert, // Often used in Linux systems - CtrlAltV, // Used in some Linux terminals (urxvt) - MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS -} - -impl Default for PasteShortcut { - fn default() -> Self { - PasteShortcut::Default - } -} - -// WINDOWS IMPLEMENTATION -#[cfg(target_os = "windows")] -pub fn get_manager() -> impl KeyboardManager { - windows::WindowsKeyboardManager {} -} - -// LINUX IMPLEMENTATION -#[cfg(target_os = "linux")] -pub fn get_manager() -> impl KeyboardManager { - linux::LinuxKeyboardManager {} -} - -// MAC IMPLEMENTATION -#[cfg(target_os = "macos")] -pub fn get_manager() -> impl KeyboardManager { - macos::MacKeyboardManager {} -} - -// These methods are used to wait until all modifiers are released (or timeout occurs) -pub fn wait_for_modifiers_release() { - #[cfg(target_os = "windows")] - let released = crate::keyboard::windows::wait_for_modifiers_release(); - - #[cfg(target_os = "macos")] - let released = crate::keyboard::macos::wait_for_modifiers_release(); - - #[cfg(target_os = "linux")] - let released = true; // NOOP on linux (at least for now) - - if !released { - warn!("Wait for modifiers release timed out! Please release your modifiers keys (CTRL, CMD, ALT, SHIFT) after typing the trigger"); - } -} diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs deleted file mode 100644 index 507d4dd..0000000 --- a/src/keyboard/windows.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::PasteShortcut; -use crate::bridge::windows::*; -use crate::config::Configs; -use log::error; -use widestring::U16CString; - -pub struct WindowsKeyboardManager {} - -impl super::KeyboardManager for WindowsKeyboardManager { - fn send_string(&self, _: &Configs, s: &str) { - let res = U16CString::from_str(s); - match res { - Ok(s) => unsafe { - send_string(s.as_ptr()); - }, - Err(e) => println!("Error while sending string: {}", e.to_string()), - } - } - - fn send_enter(&self, _: &Configs) { - unsafe { - // Send the VK_RETURN key press - send_vkey(0x0D); - } - } - - fn trigger_paste(&self, active_config: &Configs) { - unsafe { - match active_config.paste_shortcut { - PasteShortcut::Default => { - unsafe { - trigger_paste(); - } - }, - PasteShortcut::CtrlShiftV => { - trigger_shift_paste(); - }, - _ => { - error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") - } - } - } - } - - fn delete_string(&self, config: &Configs, count: i32) { - unsafe { delete_string(count, config.backspace_delay) } - } - - fn move_cursor_left(&self, _: &Configs, count: i32) { - unsafe { - // Send the left arrow key multiple times - send_multi_vkey(0x25, count) - } - } - - fn trigger_copy(&self, _: &Configs) { - unsafe { - trigger_copy(); - } - } -} - -pub fn wait_for_modifiers_release() -> bool { - let start = std::time::SystemTime::now(); - while start.elapsed().unwrap_or_default().as_millis() < 3000 { - let pressed = unsafe { crate::bridge::windows::are_modifiers_pressed() }; - if pressed == 0 { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - false -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 294ad3c..0000000 --- a/src/main.rs +++ /dev/null @@ -1,1449 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#![cfg_attr(not(test), windows_subsystem = "windows")] - -#[macro_use] -extern crate lazy_static; - -use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; -use regex::Regex; -use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader}; -use std::process::exit; -use std::process::{Command, Stdio}; -use std::sync::atomic::AtomicBool; -use std::sync::mpsc::channel; -use std::sync::mpsc::{Receiver, RecvError, Sender}; -use std::sync::{mpsc, Arc}; -use std::thread; -use std::time::Duration; - -use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; -use fs2::FileExt; -use log::{error, info, warn, LevelFilter}; -use simplelog::{CombinedLogger, SharedLogger, TermLogger, TerminalMode, WriteLogger}; - -use crate::config::runtime::RuntimeConfigManager; -use crate::config::{ConfigManager, ConfigSet, Configs}; -use crate::engine::Engine; -use crate::event::manager::{DefaultEventManager, EventManager}; -use crate::event::*; -use crate::matcher::scrolling::ScrollingMatcher; -use crate::package::default::DefaultPackageManager; -use crate::package::zip::ZipPackageResolver; -use crate::package::{InstallResult, PackageManager, RemoveResult, UpdateResult}; -use crate::protocol::*; -use crate::system::SystemManager; -use crate::ui::UIManager; - -mod bridge; -mod check; -mod cli; -mod clipboard; -mod config; -mod context; -mod edit; -mod engine; -mod event; -mod extension; -mod guard; -mod keyboard; -mod matcher; -mod package; -mod process; -mod protocol; -mod render; -mod sysdaemon; -mod system; -mod ui; -mod utils; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const LOG_FILE: &str = "espanso.log"; - -fn main() { - attach_console(); - - let install_subcommand = SubCommand::with_name("install") - .about("Install a package. Equivalent to 'espanso package install'") - .arg( - Arg::with_name("external") - .short("e") - .long("external") - .required(false) - .takes_value(false) - .help("Allow installing packages from non-verified repositories."), - ) - .arg(Arg::with_name("package_name").help("Package name")) - .arg( - Arg::with_name("repository_url") - .help("(Optional) Link to GitHub repository") - .required(false) - .default_value("hub"), - ) - .arg( - Arg::with_name("proxy") - .help("Use a proxy, should be used as --proxy=https://proxy:1234") - .required(false) - .long("proxy") - .takes_value(true), - ); - - let uninstall_subcommand = SubCommand::with_name("uninstall") - .about("Remove an installed package. Equivalent to 'espanso package uninstall'") - .arg(Arg::with_name("package_name").help("Package name")); - - let mut clap_instance = App::new("espanso") - .version(VERSION) - .author("Federico Terzi") - .about("Cross-platform Text Expander written in Rust") - .arg(Arg::with_name("v") - .short("v") - .multiple(true) - .help("Sets the level of verbosity")) - .subcommand(SubCommand::with_name("cmd") - .about("Send a command to the espanso daemon.") - .subcommand(SubCommand::with_name("exit") - .about("Terminate the daemon.")) - .subcommand(SubCommand::with_name("enable") - .about("Enable the espanso replacement engine.")) - .subcommand(SubCommand::with_name("disable") - .about("Disable the espanso replacement engine.")) - .subcommand(SubCommand::with_name("toggle") - .about("Toggle the status of the espanso replacement engine.")) - ) - .subcommand(SubCommand::with_name("edit") - .about("Open the default text editor to edit config files and reload them automatically when exiting") - .arg(Arg::with_name("config") - .help("Defaults to \"default\". The configuration file name to edit (without the .yml extension).")) - .arg(Arg::with_name("norestart") - .short("n") - .long("norestart") - .required(false) - .takes_value(false) - .help("Avoid restarting espanso after editing the file")) - ) - .subcommand(SubCommand::with_name("dump") - .about("Prints all current configuration options.")) - .subcommand(SubCommand::with_name("detect") - .about("Tool to detect current window properties, to simplify filters creation.")) - .subcommand(SubCommand::with_name("daemon") - .about("Start the daemon without spawning a new process.")) - .subcommand(SubCommand::with_name("register") - .about("MacOS and Linux only. Register espanso in the system daemon manager.")) - .subcommand(SubCommand::with_name("unregister") - .about("MacOS and Linux only. Unregister espanso from the system daemon manager.")) - .subcommand(SubCommand::with_name("log") - .about("Print the latest daemon logs.")) - .subcommand(SubCommand::with_name("start") - .about("Start the daemon spawning a new process in the background.")) - .subcommand(SubCommand::with_name("stop") - .about("Stop the espanso daemon.")) - .subcommand(SubCommand::with_name("restart") - .about("Restart the espanso daemon.")) - .subcommand(SubCommand::with_name("status") - .about("Check if the espanso daemon is running or not.")) - .subcommand(SubCommand::with_name("path") - .about("Prints all the current espanso directory paths, to easily locate configuration and data paths.") - .subcommand(SubCommand::with_name("config") - .about("Print the current config folder path.")) - .subcommand(SubCommand::with_name("packages") - .about("Print the current packages folder path.")) - .subcommand(SubCommand::with_name("data") - .about("Print the current data folder path.")) - .subcommand(SubCommand::with_name("default") - .about("Print the default configuration file path.")) - ) - .subcommand(SubCommand::with_name("match") - .about("List and execute matches from the CLI") - .subcommand(SubCommand::with_name("list") - .about("Print all matches to standard output") - .arg(Arg::with_name("json") - .short("j") - .long("json") - .help("Return the matches as json") - .required(false) - .takes_value(false) - ) - .arg(Arg::with_name("onlytriggers") - .short("t") - .long("onlytriggers") - .help("Print only triggers without replacement") - .required(false) - .takes_value(false) - ) - .arg(Arg::with_name("preservenewlines") - .short("n") - .long("preservenewlines") - .help("Preserve newlines when printing replacements") - .required(false) - .takes_value(false) - ) - ) - .subcommand(SubCommand::with_name("exec") - .about("Triggers the expansion of the given match") - .arg(Arg::with_name("trigger") - .help("The trigger of the match to be expanded") - ) - ) - ) - // Package manager - .subcommand(SubCommand::with_name("package") - .about("Espanso package manager commands") - .subcommand(install_subcommand.clone()) - .subcommand(uninstall_subcommand.clone()) - .subcommand(SubCommand::with_name("list") - .about("List all installed packages") - .arg(Arg::with_name("full") - .help("Print all package info") - .long("full"))) - - .subcommand(SubCommand::with_name("refresh") - .about("Update espanso package index")) - ) - .subcommand(SubCommand::with_name("worker") - .setting(AppSettings::Hidden) - .arg(Arg::with_name("reload") - .short("r") - .long("reload") - .required(false) - .takes_value(false)) - ) - .subcommand(install_subcommand) - .subcommand(uninstall_subcommand); - - let matches = clap_instance.clone().get_matches(); - - // The edit subcommand must be run before the configuration parsing. Otherwise, if the - // configuration is corrupted, the edit command won't work, which makes it pretty useless. - if let Some(matches) = matches.subcommand_matches("edit") { - edit_main(matches); - return; - } - - let log_level = matches.occurrences_of("v") as i32; - - // Load the configuration - let mut config_set = ConfigSet::load_default().unwrap_or_else(|e| { - println!("{}", e); - exit(1); - }); - - config_set.default.log_level = log_level; - - // Commands that require the configuration - - if let Some(matches) = matches.subcommand_matches("cmd") { - cmd_main(config_set, matches); - return; - } - - if matches.subcommand_matches("dump").is_some() { - println!("{:#?}", config_set); - return; - } - - if matches.subcommand_matches("detect").is_some() { - detect_main(); - return; - } - - if matches.subcommand_matches("daemon").is_some() { - daemon_main(config_set); - return; - } - - if matches.subcommand_matches("register").is_some() { - register_main(config_set); - return; - } - - if matches.subcommand_matches("unregister").is_some() { - unregister_main(config_set); - return; - } - - if matches.subcommand_matches("log").is_some() { - log_main(); - return; - } - - if matches.subcommand_matches("start").is_some() { - start_main(config_set); - return; - } - - if matches.subcommand_matches("status").is_some() { - status_main(); - return; - } - - if matches.subcommand_matches("stop").is_some() { - stop_main(config_set); - return; - } - - if matches.subcommand_matches("restart").is_some() { - restart_main(config_set); - return; - } - - if let Some(matches) = matches.subcommand_matches("install") { - install_main(config_set, matches); - return; - } - - if let Some(matches) = matches.subcommand_matches("uninstall") { - remove_package_main(config_set, matches); - return; - } - - if let Some(matches) = matches.subcommand_matches("path") { - path_main(config_set, matches); - return; - } - - if let Some(matches) = matches.subcommand_matches("match") { - match_main(config_set, matches); - return; - } - - if let Some(matches) = matches.subcommand_matches("package") { - if let Some(matches) = matches.subcommand_matches("install") { - install_main(config_set, matches); - return; - } - if let Some(matches) = matches.subcommand_matches("uninstall") { - remove_package_main(config_set, matches); - return; - } - if let Some(matches) = matches.subcommand_matches("list") { - list_package_main(config_set, matches); - return; - } - if matches.subcommand_matches("refresh").is_some() { - update_index_main(config_set); - return; - } - } - - if let Some(matches) = matches.subcommand_matches("worker") { - worker_main(config_set, matches); - return; - } - - // Defaults help print - clap_instance - .print_long_help() - .expect("Unable to print help"); - println!(); -} - -#[cfg(target_os = "windows")] -fn attach_console() { - // When using the windows subsystem we loose the terminal output. - // Therefore we try to attach to the current console if available. - unsafe { winapi::um::wincon::AttachConsole(0xFFFFFFFF) }; -} - -#[cfg(not(target_os = "windows"))] -fn attach_console() { - // Not necessary on Linux and macOS -} - -fn init_logger(config_set: &ConfigSet, reset: bool) { - // Initialize log - let log_level = match config_set.default.log_level { - 0 => LevelFilter::Warn, - 1 => LevelFilter::Info, - 2 | _ => LevelFilter::Debug, - }; - - let mut log_outputs: Vec> = Vec::new(); - - // Initialize terminal output - let terminal_out = - TermLogger::new(log_level, simplelog::Config::default(), TerminalMode::Mixed); - if let Some(terminal_out) = terminal_out { - log_outputs.push(terminal_out); - } - - // Initialize log file output - let espanso_dir = context::get_data_dir(); - let log_file_path = espanso_dir.join(LOG_FILE); - - if reset && log_file_path.exists() { - std::fs::remove_file(&log_file_path).expect("unable to remove log file"); - } - - let log_file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .append(true) - .open(log_file_path) - .expect("Cannot create log file."); - let file_out = WriteLogger::new(LevelFilter::Info, simplelog::Config::default(), log_file); - log_outputs.push(file_out); - - CombinedLogger::init(log_outputs).expect("Error opening log destination"); - - // Activate logging for panics - log_panics::init(); -} - -/// Daemon subcommand, start the event loop and spawn a background thread worker -fn daemon_main(config_set: ConfigSet) { - // Try to acquire lock file - let lock_file = acquire_lock(); - if lock_file.is_none() { - println!("espanso is already running."); - exit(3); - } - - precheck_guard(); - - init_logger(&config_set, true); - - info!("espanso version {}", VERSION); - info!( - "using config path: {}", - context::get_config_dir().to_string_lossy() - ); - info!( - "using package path: {}", - context::get_package_dir().to_string_lossy() - ); - - let (send_channel, receive_channel) = mpsc::channel(); - - let ipc_server = protocol::get_ipc_server( - Service::Daemon, - config_set.default.clone(), - send_channel.clone(), - ); - ipc_server.start(); - - info!("spawning worker process..."); - - let espanso_path = std::env::current_exe().expect("unable to obtain espanso path location"); - let mut child = crate::process::spawn_process( - &espanso_path.to_string_lossy().to_string(), - &vec!["worker".to_owned()], - ) - .expect("unable to create worker process"); - - // Create a monitor thread that will exit with the same non-zero code if - // the worker thread exits - thread::Builder::new() - .name("worker monitor".to_string()) - .spawn(move || { - let result = child.wait(); - if let Ok(status) = result { - if let Some(code) = status.code() { - if code != 0 { - error!( - "worker process exited with non-zero code: {}, exiting", - code - ); - std::process::exit(code); - } - } - } - }) - .expect("Unable to spawn worker monitor thread"); - - register_signals(config_set.default.clone()); - - std::thread::sleep(Duration::from_millis(200)); - - if config_set.default.auto_restart { - let send_channel_clone = send_channel.clone(); - thread::Builder::new() - .name("watcher_background".to_string()) - .spawn(move || { - watcher_background(send_channel_clone); - }) - .expect("Unable to spawn watcher background thread"); - } - - loop { - match receive_channel.recv() { - Ok(event) => { - match event { - Event::Action(ActionType::RestartWorker) => { - // Terminate the worker process - send_command_or_warn( - Service::Worker, - config_set.default.clone(), - IPCCommand::exit_worker(), - ); - - std::thread::sleep(Duration::from_millis(500)); - - // Restart the worker process - crate::process::spawn_process( - &espanso_path.to_string_lossy().to_string(), - &vec!["worker".to_owned(), "--reload".to_owned()], - ); - } - Event::Action(ActionType::Exit) => { - send_command_or_warn( - Service::Worker, - config_set.default.clone(), - IPCCommand::exit_worker(), - ); - - std::thread::sleep(Duration::from_millis(200)); - - info!("terminating espanso."); - std::process::exit(0); - } - _ => { - // Forward the command to the worker - let command = IPCCommand::from(event); - if let Some(command) = command { - send_command_or_warn( - Service::Worker, - config_set.default.clone(), - command, - ); - } - } - } - } - Err(e) => { - warn!("error while reading event in daemon process: {}", e); - } - } - } -} - -#[cfg(target_os = "windows")] -fn register_signals(_: Configs) {} - -#[cfg(not(target_os = "windows"))] -fn register_signals(config: Configs) { - // On Unix, also listen for signals so that we can terminate the - // worker if the daemon receives a signal - use signal_hook::{iterator::Signals, SIGINT, SIGTERM}; - let signals = Signals::new(&[SIGTERM, SIGINT]).expect("unable to register for signals"); - thread::Builder::new() - .name("signal monitor".to_string()) - .spawn(move || { - for signal in signals.forever() { - info!("Received signal: {:?}, terminating worker", signal); - send_command_or_warn(Service::Worker, config, IPCCommand::exit_worker()); - - std::thread::sleep(Duration::from_millis(200)); - - info!("terminating espanso."); - std::process::exit(0); - } - }) - .expect("Unable to spawn signal monitor thread"); -} - -fn watcher_background(sender: Sender) { - // Create a channel to receive the events. - let (tx, rx) = channel(); - - let mut watcher: RecommendedWatcher = - Watcher::new(tx, Duration::from_secs(1)).expect("unable to create file watcher"); - - let config_path = crate::context::get_config_dir(); - watcher - .watch(&config_path, RecursiveMode::Recursive) - .expect("unable to start watcher"); - - info!( - "watching for changes in path: {}", - config_path.to_string_lossy() - ); - - loop { - let should_reload = match rx.recv() { - Ok(event) => { - let path = match event { - DebouncedEvent::Create(path) => Some(path), - DebouncedEvent::Write(path) => Some(path), - DebouncedEvent::Remove(path) => Some(path), - DebouncedEvent::Rename(_, path) => Some(path), - _ => None, - }; - - if let Some(path) = path { - if path.extension().unwrap_or_default() == "yml" - && !path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .starts_with(".") - { - // Only load non-hidden yml files - true - } else { - false - } - } else { - false - } - } - Err(e) => { - warn!("error while watching files: {:?}", e); - false - } - }; - - if should_reload { - info!("change detected, restarting worker process..."); - - let mut config_set = ConfigSet::load_default(); - - match config_set { - Ok(config_set) => { - let event = Event::Action(ActionType::RestartWorker); - sender.send(event).unwrap_or_else(|e| { - warn!("unable to communicate with daemon thread: {}", e); - }) - } - Err(error) => { - error!("Unable to reload configuration due to an error: {}", error); - let event = Event::System(SystemEvent::NotifyRequest( - "Unable to reload config due to an error, see the logs for more details." - .to_owned(), - )); - sender.send(event).unwrap_or_else(|e| { - warn!("unable to communicate with daemon thread: {}", e); - }) - } - } - } - } -} - -/// Worker process main which does the actual work -fn worker_main(config_set: ConfigSet, matches: &ArgMatches) { - init_logger(&config_set, false); - - info!("initializing worker process..."); - - let is_reloading: bool = if matches.is_present("reload") { - true - } else { - false - }; - - let (send_channel, receive_channel) = mpsc::channel(); - - // This atomic bool is used to "disable" espanso when espanding its own matches, otherwise - // we could reinterpret the characters we are injecting - let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false)); - - let context = context::new( - config_set.default.clone(), - send_channel.clone(), - is_injecting.clone(), - ); - - let config_set_copy = config_set.clone(); - thread::Builder::new() - .name("daemon_background".to_string()) - .spawn(move || { - worker_background(receive_channel, config_set_copy, is_injecting, is_reloading); - }) - .expect("Unable to spawn daemon background thread"); - - let ipc_server = - protocol::get_ipc_server(Service::Worker, config_set.default, send_channel.clone()); - ipc_server.start(); - - context.eventloop(); -} - -/// Background thread worker for the daemon -fn worker_background( - receive_channel: Receiver, - config_set: ConfigSet, - is_injecting: Arc, - is_reloading: bool, -) { - let system_manager = system::get_manager(); - let config_manager = RuntimeConfigManager::new(config_set, system_manager); - - let ui_manager = ui::get_uimanager(); - if config_manager.default_config().show_notifications { - if !is_reloading { - ui_manager.notify("espanso is running!"); - } else { - ui_manager.notify("Reloaded config!"); - } - } - - let clipboard_manager = clipboard::get_manager(); - - let keyboard_manager = keyboard::get_manager(); - - let extensions = extension::get_extensions( - config_manager.default_config(), - Box::new(clipboard::get_manager()), - ); - - let renderer = - render::default::DefaultRenderer::new(extensions, config_manager.default_config().clone()); - - let engine = Engine::new( - &keyboard_manager, - &clipboard_manager, - &config_manager, - &ui_manager, - &renderer, - is_injecting, - ); - - let matcher = ScrollingMatcher::new(&config_manager, &engine); - - let event_manager = DefaultEventManager::new( - receive_channel, - vec![&matcher], - vec![&engine, &matcher], - vec![&engine], - ); - - info!("worker is running!"); - - event_manager.eventloop(); -} - -/// start subcommand, spawn a background espanso process. -fn start_main(config_set: ConfigSet) { - // Try to acquire lock file - let lock_file = acquire_lock(); - if lock_file.is_none() { - println!("espanso is already running."); - exit(3); - } - release_lock(lock_file.unwrap()); - - precheck_guard(); - - start_daemon(config_set); -} - -#[cfg(target_os = "windows")] -fn start_daemon(_: ConfigSet) { - unsafe { - let res = bridge::windows::start_daemon_process(); - if res < 0 { - println!("Error starting daemon process"); - } - } -} - -#[cfg(target_os = "macos")] -fn start_daemon(config_set: ConfigSet) { - if config_set.default.use_system_agent { - use std::process::Command; - - let res = Command::new("launchctl") - .args(&["start", "com.federicoterzi.espanso"]) - .status(); - - if let Ok(status) = res { - if status.success() { - println!("Daemon started correctly!") - } else { - eprintln!("Error starting launchd daemon with status: {}", status); - } - } else { - eprintln!("Error starting launchd daemon: {}", res.unwrap_err()); - } - } else { - fork_daemon(config_set); - } -} - -#[cfg(target_os = "linux")] -fn start_daemon(config_set: ConfigSet) { - use crate::sysdaemon::{verify, VerifyResult}; - use std::process::{Command, Stdio}; - - // Check if Systemd is available in the system - let status = Command::new("systemctl") - .args(&["--version"]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .status(); - - // If Systemd is not available in the system, espanso should default to unmanaged mode - // See issue https://github.com/federico-terzi/espanso/issues/139 - let force_unmanaged = if let Err(_) = status { true } else { false }; - - if config_set.default.use_system_agent && !force_unmanaged { - // Make sure espanso is currently registered in systemd - let res = verify(); - match res { - VerifyResult::EnabledAndValid => { - // Do nothing, everything is ok! - } - VerifyResult::EnabledButInvalidPath => { - eprintln!("Updating espanso service file with new path..."); - unregister_main(config_set.clone()); - register_main(config_set); - } - VerifyResult::NotEnabled => { - use dialoguer::Confirmation; - if Confirmation::new() - .with_text("espanso must be registered to systemd (user level) first. Do you want to proceed?") - .default(true) - .show_default(true) - .interact().expect("Unable to read user answer") { - - register_main(config_set); - }else{ - eprintln!("Please register espanso to systemd with this command:"); - eprintln!(" espanso register"); - // TODO: enable flag to use non-managed daemon mode - - std::process::exit(4); - } - } - } - - // Start the espanso service - let res = Command::new("systemctl") - .args(&["--user", "start", "espanso.service"]) - .status(); - - if let Ok(status) = res { - if status.success() { - println!("Daemon started correctly!") - } else { - eprintln!("Error starting systemd daemon with status: {}", status); - } - } else { - eprintln!("Error starting systemd daemon: {}", res.unwrap_err()); - } - } else { - if force_unmanaged { - eprintln!("Systemd is not available in this system, switching to unmanaged mode."); - } - - fork_daemon(config_set); - } -} - -#[cfg(not(target_os = "windows"))] -fn fork_daemon(config_set: ConfigSet) { - unsafe { - let pid = libc::fork(); - if pid < 0 { - println!("Unable to fork."); - exit(4); - } - if pid > 0 { - // Parent process exit - println!("daemon started!"); - exit(0); - } - - // Spawned process - - // Create a new SID for the child process - let sid = libc::setsid(); - if sid < 0 { - exit(5); - } - - // Detach stdout and stderr - let null_path = std::ffi::CString::new("/dev/null").expect("CString unwrap failed"); - let fd = libc::open(null_path.as_ptr(), libc::O_RDWR, 0); - if fd != -1 { - libc::dup2(fd, libc::STDIN_FILENO); - libc::dup2(fd, libc::STDOUT_FILENO); - libc::dup2(fd, libc::STDERR_FILENO); - } - } - - daemon_main(config_set); -} - -/// status subcommand, print the current espanso status -fn status_main() { - let lock_file = acquire_lock(); - if let Some(lock_file) = lock_file { - println!("espanso is not running"); - - release_lock(lock_file); - } else { - println!("espanso is running"); - } -} - -/// Stop subcommand, used to stop the daemon. -fn stop_main(config_set: ConfigSet) { - // Try to acquire lock file - let lock_file = acquire_lock(); - if lock_file.is_some() { - println!("espanso daemon is not running."); - release_lock(lock_file.unwrap()); - exit(3); - } - - send_command_or_warn(Service::Daemon, config_set.default, IPCCommand::exit()); -} - -/// Kill the daemon if running and start it again -fn restart_main(config_set: ConfigSet) { - // Kill the daemon if running - let lock_file = acquire_lock(); - if lock_file.is_none() { - // Terminate the current espanso daemon - send_command_or_warn( - Service::Daemon, - config_set.default.clone(), - IPCCommand::exit(), - ); - } else { - release_lock(lock_file.unwrap()); - } - - std::thread::sleep(Duration::from_millis(500)); - - // Restart the daemon - start_main(config_set); -} - -/// Cli tool used to analyze active windows to extract useful information -/// to create configuration filters. -#[cfg(not(target_os = "macos"))] -fn detect_main() { - let system_manager = system::get_manager(); - - println!("Listening for changes, now focus the window you want to analyze."); - println!("You can terminate with CTRL+C\n"); - - let mut last_title: String = "".to_owned(); - let mut last_class: String = "".to_owned(); - let mut last_exec: String = "".to_owned(); - - loop { - let curr_title = system_manager - .get_current_window_title() - .unwrap_or_default(); - let curr_class = system_manager - .get_current_window_class() - .unwrap_or_default(); - let curr_exec = system_manager - .get_current_window_executable() - .unwrap_or_default(); - - // Check if a change occurred - if curr_title != last_title || curr_class != last_class || curr_exec != last_exec { - println!("Detected change, current window has properties:"); - println!("==> Title: '{}'", curr_title); - println!("==> Class: '{}'", curr_class); - println!("==> Executable: '{}'", curr_exec); - println!(); - } - - last_title = curr_title; - last_class = curr_class; - last_exec = curr_exec; - - thread::sleep(Duration::from_millis(500)); - } -} - -/// Cli tool used to analyze active windows to extract useful information -/// to create configuration filters. -/// On macOS version we need to start an event loop for the app to register changes. -#[cfg(target_os = "macos")] -fn detect_main() { - thread::spawn(|| { - use std::io::stdout; - use std::io::Write; - - let system_manager = system::get_manager(); - - println!("Listening for changes, now focus the window you want to analyze."); - println!( - "Warning: stay on the window for a few seconds, as it may take a while to register." - ); - println!("You can terminate with CTRL+C\n"); - - let mut last_title: String = "".to_owned(); - let mut last_class: String = "".to_owned(); - let mut last_exec: String = "".to_owned(); - - loop { - let curr_title = system_manager - .get_current_window_title() - .unwrap_or_default(); - let curr_class = system_manager - .get_current_window_class() - .unwrap_or_default(); - let curr_exec = system_manager - .get_current_window_executable() - .unwrap_or_default(); - - // Check if a change occurred - if curr_title != last_title || curr_class != last_class || curr_exec != last_exec { - println!("Detected change, current window has properties:"); - println!("==> Title: '{}'", curr_title); - println!("==> Class: '{}'", curr_class); - println!("==> Executable: '{}'", curr_exec); - println!(); - } - - last_title = curr_title; - last_class = curr_class; - last_exec = curr_exec; - - thread::sleep(Duration::from_millis(500)); - } - }); - - unsafe { - crate::bridge::macos::headless_eventloop(); - } -} - -/// Send the given command to the espanso daemon -fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) { - let command = if matches.subcommand_matches("exit").is_some() { - Some(IPCCommand::exit()) - } else if matches.subcommand_matches("toggle").is_some() { - Some(IPCCommand { - id: String::from("toggle"), - payload: String::from(""), - }) - } else if matches.subcommand_matches("enable").is_some() { - Some(IPCCommand { - id: String::from("enable"), - payload: String::from(""), - }) - } else if matches.subcommand_matches("disable").is_some() { - Some(IPCCommand { - id: String::from("disable"), - payload: String::from(""), - }) - } else { - None - }; - - if let Some(command) = command { - send_command_or_warn(Service::Daemon, config_set.default, command); - } - - exit(1); -} - -fn log_main() { - let espanso_dir = context::get_data_dir(); - let log_file_path = espanso_dir.join(LOG_FILE); - - if !log_file_path.exists() { - println!("No log file found."); - exit(2); - } - - let log_file = File::open(log_file_path); - if let Ok(log_file) = log_file { - let reader = BufReader::new(log_file); - for line in reader.lines() { - if let Ok(line) = line { - println!("{}", line); - } - } - - exit(0); - } else { - println!("Error reading log file"); - exit(1); - } -} - -fn register_main(config_set: ConfigSet) { - sysdaemon::register(config_set); -} - -fn unregister_main(config_set: ConfigSet) { - sysdaemon::unregister(config_set); -} - -fn install_main(_config_set: ConfigSet, matches: &ArgMatches) { - let package_name = matches.value_of("package_name").unwrap_or_else(|| { - eprintln!("Missing package name!"); - exit(1); - }); - - let mut repository = matches.value_of("repository_url").unwrap_or("hub"); - - // Remove trailing .git string if present - // See: https://github.com/federico-terzi/espanso/issues/326 - if repository.ends_with(".git") { - repository = repository.trim_end_matches(".git") - } - - let proxy = match matches.value_of("proxy") { - Some(proxy) => { - println!("Using proxy: {}", proxy); - Some(proxy.to_string()) - } - None => None, - }; - - let package_resolver = Box::new(ZipPackageResolver::new()); - - let allow_external: bool = if matches.is_present("external") { - println!("Allowing external repositories"); - true - } else { - false - }; - - let mut package_manager = DefaultPackageManager::new_default(Some(package_resolver)); - - let res = if repository == "hub" { - // Installation from the Hub - if package_manager.is_index_outdated() { - println!("Updating package index..."); - let res = package_manager.update_index(false); - - match res { - Ok(update_result) => match update_result { - UpdateResult::NotOutdated => { - eprintln!("Index was already up to date"); - } - UpdateResult::Updated => { - println!("Index updated!"); - } - }, - Err(e) => { - eprintln!("{}", e); - exit(2); - } - } - } else { - println!("Using cached package index, run 'espanso package refresh' to update it.") - } - - package_manager.install_package(package_name, allow_external, proxy) - } else { - // Make sure the repo is a valid github url - lazy_static! { - static ref GITHUB_REGEX: Regex = Regex::new(r#"https://github\.com/\S*/\S*"#).unwrap(); - }; - - if !GITHUB_REGEX.is_match(repository) { - eprintln!("repository url is not valid, it should be an HTTPS GitHub url in the following format:"); - eprintln!("https://github.com/user/repo"); - exit(3); - } - - if !allow_external { - Ok(InstallResult::BlockedExternalPackage(repository.to_owned())) - } else { - package_manager.install_package_from_repo(package_name, repository, proxy) - } - }; - - match res { - Ok(install_result) => match install_result { - InstallResult::NotFoundInIndex => { - eprintln!("Package not found"); - } - InstallResult::NotFoundInRepo => { - eprintln!( - "Package not found in repository, are you sure the folder exist in the repo?" - ); - } - InstallResult::UnableToParsePackageInfo => { - eprintln!("Unable to parse Package info from README.md"); - } - InstallResult::MissingPackageVersion => { - eprintln!("Missing package version"); - } - InstallResult::AlreadyInstalled => { - eprintln!("{} already installed!", package_name); - } - InstallResult::BlockedExternalPackage(repo_url) => { - eprintln!("Warning: the requested package is hosted on an external repository:"); - eprintln!(); - eprintln!("{}", repo_url); - eprintln!(); - eprintln!("and its contents may not have been verified by espanso."); - eprintln!(); - eprintln!("For your security, espanso blocks packages that are not verified."); - eprintln!("If you want to install the package anyway, you can force espanso"); - eprintln!("to install it with the following command, but please do it only"); - eprintln!("if you trust the source or you verified the contents of the package"); - eprintln!("by checking out the repository listed above."); - eprintln!(); - - if repository == "hub" { - eprintln!("espanso install {} --external", package_name); - } else { - eprintln!("espanso install {} {} --external", package_name, repository); - } - eprintln!(); - } - InstallResult::Installed => { - println!("{} successfully installed!", package_name); - println!(); - println!("You need to restart espanso for changes to take effect, using:"); - println!(" espanso restart"); - } - }, - Err(e) => { - eprintln!("{}", e); - } - } -} - -fn remove_package_main(_config_set: ConfigSet, matches: &ArgMatches) { - let package_name = matches.value_of("package_name").unwrap_or_else(|| { - eprintln!("Missing package name!"); - exit(1); - }); - - let package_manager = DefaultPackageManager::new_default(None); - - let res = package_manager.remove_package(package_name); - - match res { - Ok(remove_result) => match remove_result { - RemoveResult::NotFound => { - eprintln!("{} package was not installed.", package_name); - } - RemoveResult::Removed => { - println!("{} successfully removed!", package_name); - println!(); - println!("You need to restart espanso for changes to take effect, using:"); - println!(" espanso restart"); - } - }, - Err(e) => { - eprintln!("{}", e); - } - } -} - -fn update_index_main(_config_set: ConfigSet) { - let mut package_manager = DefaultPackageManager::new_default(None); - - let res = package_manager.update_index(true); - - match res { - Ok(update_result) => match update_result { - UpdateResult::NotOutdated => { - eprintln!("Index was already up to date"); - } - UpdateResult::Updated => { - println!("Index updated!"); - } - }, - Err(e) => { - eprintln!("{}", e); - exit(2); - } - } -} - -fn list_package_main(_config_set: ConfigSet, matches: &ArgMatches) { - let package_manager = DefaultPackageManager::new_default(None); - - let list = package_manager.list_local_packages(); - - if matches.is_present("full") { - for package in list.iter() { - println!("{:?}", package); - } - } else { - for package in list.iter() { - println!("{} - {}", package.name, package.version); - } - } -} - -fn path_main(_config_set: ConfigSet, matches: &ArgMatches) { - let config = crate::context::get_config_dir(); - let packages = crate::context::get_package_dir(); - let data = crate::context::get_data_dir(); - - if matches.subcommand_matches("config").is_some() { - println!("{}", config.to_string_lossy()); - } else if matches.subcommand_matches("packages").is_some() { - println!("{}", packages.to_string_lossy()); - } else if matches.subcommand_matches("data").is_some() { - println!("{}", data.to_string_lossy()); - } else if matches.subcommand_matches("default").is_some() { - let default_file = config.join(crate::config::DEFAULT_CONFIG_FILE_NAME); - println!("{}", default_file.to_string_lossy()); - } else { - println!("Config: {}", config.to_string_lossy()); - println!("Packages: {}", packages.to_string_lossy()); - println!("Data: {}", data.to_string_lossy()); - } -} - -fn match_main(config_set: ConfigSet, matches: &ArgMatches) { - if let Some(matches) = matches.subcommand_matches("list") { - let json = matches.is_present("json"); - let onlytriggers = matches.is_present("onlytriggers"); - let preserve_newlines = matches.is_present("preservenewlines"); - - if !json { - crate::cli::list_matches(config_set, onlytriggers, preserve_newlines); - } else { - crate::cli::list_matches_as_json(config_set); - } - } else if let Some(matches) = matches.subcommand_matches("exec") { - let trigger = matches.value_of("trigger").unwrap_or_else(|| { - eprintln!("missing trigger"); - exit(1); - }); - - send_command_or_warn( - Service::Worker, - config_set.default.clone(), - IPCCommand::trigger(trigger), - ); - } -} - -fn edit_main(matches: &ArgMatches) { - // Determine which is the file to edit - let config = matches.value_of("config").unwrap_or("default"); - - let config_dir = crate::context::get_config_dir(); - - let config_path = match config { - "default" => config_dir.join(crate::config::DEFAULT_CONFIG_FILE_NAME), - name => { - // Otherwise, search in the user/ config folder - config_dir - .join(crate::config::USER_CONFIGS_FOLDER_NAME) - .join(name.to_owned() + ".yml") - } - }; - - println!("Editing file: {:?}", &config_path); - - // Based on the fact that the file already exists or not, we should detect in different - // ways if a reload is needed - let should_reload = if config_path.exists() { - // Get the last modified date, so that we can detect if the user actually edits the file - // before reloading - let metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata"); - let last_modified = metadata - .modified() - .expect("cannot read file last modified date"); - - let result = crate::edit::open_editor(&config_path); - if result { - let new_metadata = - std::fs::metadata(&config_path).expect("cannot gather file metadata"); - let new_last_modified = new_metadata - .modified() - .expect("cannot read file last modified date"); - - if last_modified != new_last_modified { - println!("File has been modified, reloading configuration"); - true - } else { - println!("File has not been modified, avoiding reload"); - false - } - } else { - false - } - } else { - let result = crate::edit::open_editor(&config_path); - if result { - // If the file has been created, we should reload the espanso config - if config_path.exists() { - println!("A new file has been created, reloading configuration"); - true - } else { - println!("No file has been created, avoiding reload"); - false - } - } else { - false - } - }; - - let no_restart: bool = if matches.is_present("norestart") { - println!("Avoiding automatic restart"); - true - } else { - false - }; - - if should_reload && !no_restart { - // Load the configuration - let config_set = ConfigSet::load_default().unwrap_or_else(|e| { - eprintln!("{}", e); - eprintln!("Unable to reload espanso due to previous configuration error."); - exit(1); - }); - - restart_main(config_set) - } -} - -fn acquire_lock() -> Option { - acquire_custom_lock("espanso.lock") -} - -fn acquire_custom_lock(name: &str) -> Option { - let espanso_dir = context::get_data_dir(); - let lock_file_path = espanso_dir.join(name); - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(lock_file_path) - .expect("Cannot create reference to lock file."); - - let res = file.try_lock_exclusive(); - - if res.is_ok() { - return Some(file); - } - - None -} - -fn release_lock(lock_file: File) { - lock_file.unlock().unwrap() -} - -/// Used to make sure all the required dependencies and conditions are satisfied before starting espanso. -fn precheck_guard() { - let satisfied = check::check_preconditions(); - if !satisfied { - println!(); - println!("Pre-check was not successful, espanso could not be started."); - exit(5); - } -} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs deleted file mode 100644 index c8f8286..0000000 --- a/src/matcher/mod.rs +++ /dev/null @@ -1,801 +0,0 @@ -/* - * This file is part of espans{ name: (), var_type: (), params: ()} - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::event::KeyEventReceiver; -use crate::event::{KeyEvent, KeyModifier}; -use regex::{Captures, Regex}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_yaml::{Mapping, Value}; -use std::fs; -use std::path::PathBuf; -use std::{borrow::Cow, collections::HashMap}; - -pub(crate) mod scrolling; - -#[derive(Debug, Serialize, Clone)] -pub struct Match { - pub triggers: Vec, - pub content: MatchContentType, - pub word: bool, - pub passive_only: bool, - pub propagate_case: bool, - pub force_clipboard: bool, - pub is_html: bool, - - // Automatically calculated from the triggers, used by the matcher to check for correspondences. - #[serde(skip_serializing)] - pub _trigger_sequences: Vec>, -} - -#[derive(Debug, Serialize, Clone)] -pub enum MatchContentType { - Text(TextContent), - Image(ImageContent), -} - -#[derive(Debug, Serialize, Clone, PartialEq)] -pub struct TextContent { - pub replace: String, - pub vars: Vec, - - #[serde(skip_serializing)] - pub _has_vars: bool, -} - -#[derive(Debug, Serialize, Clone)] -pub struct ImageContent { - pub path: PathBuf, -} - -impl<'de> serde::Deserialize<'de> for Match { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let auto_match = AutoMatch::deserialize(deserializer)?; - Ok(Match::from(&auto_match)) - } -} - -impl<'a> From<&'a AutoMatch> for Match { - fn from(other: &'a AutoMatch) -> Self { - lazy_static! { - static ref VAR_REGEX: Regex = - Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap(); - }; - - let mut triggers = if !other.triggers.is_empty() { - other.triggers.clone() - } else if !other.trigger.is_empty() { - vec![other.trigger.clone()] - } else { - panic!("Match does not have any trigger defined: {:?}", other) - }; - - // If propagate_case is true, we need to generate all the possible triggers - // For example, specifying "hello" as a trigger, we need to have: - // "hello", "Hello", "HELLO" - if other.propagate_case { - // List with first letter capitalized - let first_capitalized: Vec = triggers - .iter() - .map(|trigger| { - let capitalized = trigger.clone(); - let mut v: Vec = capitalized.chars().collect(); - - // Capitalize the first alphabetic letter - // See issue #244 - let first_alphabetic = v.iter().position(|c| c.is_alphabetic()).unwrap_or(0); - - v[first_alphabetic] = v[first_alphabetic].to_uppercase().nth(0).unwrap(); - v.into_iter().collect() - }) - .collect(); - - let all_capitalized: Vec = triggers - .iter() - .map(|trigger| trigger.to_uppercase()) - .collect(); - - triggers.extend(first_capitalized); - triggers.extend(all_capitalized); - } - - let trigger_sequences = triggers - .iter() - .map(|trigger| { - // Calculate the trigger sequence - let mut trigger_sequence = Vec::new(); - let trigger_chars: Vec = trigger.chars().collect(); - trigger_sequence.extend(trigger_chars.into_iter().map(|c| TriggerEntry::Char(c))); - if other.word { - // If it's a word match, end with a word separator - trigger_sequence.push(TriggerEntry::WordSeparator); - } - - trigger_sequence - }) - .collect(); - - let (text_content, is_html) = if let Some(replace) = &other.replace { - (Some(Cow::from(replace)), false) - } else if let Some(markdown_str) = &other.markdown { - // Render the markdown into HTML - let mut html = markdown::to_html(markdown_str); - html = html.trim().to_owned(); - - if !other.paragraph { - // Remove the surrounding paragraph - if html.starts_with("

") { - html = html.trim_start_matches("

").to_owned(); - } - if html.ends_with("

") { - html = html.trim_end_matches("

").to_owned(); - } - } - - (Some(Cow::from(html)), true) - } else if let Some(html) = &other.html { - (Some(Cow::from(html)), true) - } else { - (None, false) - }; - - let content = if let Some(content) = text_content { - // Check if the match contains variables - let has_vars = VAR_REGEX.is_match(&content); - - let content = TextContent { - replace: content.to_string(), - vars: other.vars.clone(), - _has_vars: has_vars, - }; - - MatchContentType::Text(content) - } else if let Some(form) = &other.form { - // Form shorthand - // Replace all the form fields with actual variables - let new_replace = VAR_REGEX.replace_all(&form, |caps: &Captures| { - let var_name = caps.get(1).unwrap().as_str(); - format!("{{{{form1.{}}}}}", var_name) - }); - let new_replace = new_replace.to_string(); - - // Convert escaped brakets in forms - let form = form.replace("\\{", "{ ").replace("\\}", " }"); - - // Convert the form data to valid variables - let mut params = Mapping::new(); - if let Some(fields) = &other.form_fields { - let mut mapping_fields = Mapping::new(); - fields.iter().for_each(|(key, value)| { - mapping_fields.insert(Value::from(key.to_owned()), Value::from(value.clone())); - }); - params.insert(Value::from("fields"), Value::from(mapping_fields)); - } - params.insert(Value::from("layout"), Value::from(form)); - - let vars = vec![MatchVariable { - name: "form1".to_owned(), - var_type: "form".to_owned(), - params, - }]; - - let content = TextContent { - replace: new_replace, - vars, - _has_vars: true, - }; - - MatchContentType::Text(content) - } else if let Some(image_path) = &other.image_path { - // Image match - // On Windows, we have to replace the forward / with the backslash \ in the path - let new_path = if cfg!(target_os = "windows") { - image_path.replace("/", "\\") - } else { - image_path.to_owned() - }; - - // Calculate variables in path - let new_path = if new_path.contains("$CONFIG") { - let config_dir = crate::context::get_config_dir(); - let config_path = fs::canonicalize(&config_dir); - let config_path = if let Ok(config_path) = config_path { - config_path.to_string_lossy().into_owned() - } else { - "".to_owned() - }; - new_path.replace("$CONFIG", &config_path) - } else { - new_path.to_owned() - }; - - let content = ImageContent { - path: PathBuf::from(new_path), - }; - - MatchContentType::Image(content) - } else { - eprintln!("ERROR: no action specified for match {}, please specify either 'replace', 'markdown', 'html', image_path' or 'form'", other.trigger); - std::process::exit(2); - }; - - Self { - triggers, - content, - word: other.word, - passive_only: other.passive_only, - _trigger_sequences: trigger_sequences, - propagate_case: other.propagate_case, - force_clipboard: other.force_clipboard, - is_html, - } - } -} - -/// Used to deserialize the Match struct before applying some custom elaboration. -#[derive(Debug, Serialize, Deserialize, Clone)] -struct AutoMatch { - #[serde(default = "default_trigger")] - pub trigger: String, - - #[serde(default = "default_triggers")] - pub triggers: Vec, - - #[serde(default = "default_replace")] - pub replace: Option, - - #[serde(default = "default_image_path")] - pub image_path: Option, - - #[serde(default = "default_form")] - pub form: Option, - - #[serde(default = "default_form_fields")] - pub form_fields: Option>, - - #[serde(default = "default_vars")] - pub vars: Vec, - - #[serde(default = "default_word")] - pub word: bool, - - #[serde(default = "default_passive_only")] - pub passive_only: bool, - - #[serde(default = "default_propagate_case")] - pub propagate_case: bool, - - #[serde(default = "default_force_clipboard")] - pub force_clipboard: bool, - - #[serde(default = "default_markdown")] - pub markdown: Option, - - #[serde(default = "default_paragraph")] - pub paragraph: bool, - - #[serde(default = "default_html")] - pub html: Option, -} - -fn default_trigger() -> String { - "".to_owned() -} -fn default_triggers() -> Vec { - Vec::new() -} -fn default_vars() -> Vec { - Vec::new() -} -fn default_word() -> bool { - false -} -fn default_passive_only() -> bool { - false -} -fn default_replace() -> Option { - None -} -fn default_form() -> Option { - None -} -fn default_form_fields() -> Option> { - None -} -fn default_image_path() -> Option { - None -} -fn default_propagate_case() -> bool { - false -} -fn default_force_clipboard() -> bool { - false -} -fn default_markdown() -> Option { - None -} -fn default_paragraph() -> bool { - false -} -fn default_html() -> Option { - None -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct MatchVariable { - pub name: String, - - #[serde(rename = "type")] - pub var_type: String, - - #[serde(default = "default_params")] - pub params: Mapping, -} - -fn default_params() -> Mapping { - Mapping::new() -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub enum TriggerEntry { - Char(char), - WordSeparator, -} - -pub trait MatchReceiver { - fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize); - fn on_enable_update(&self, status: bool); - fn on_passive(&self); - fn on_undo(&self); -} - -pub trait Matcher: KeyEventReceiver { - fn handle_char(&self, c: &str); - fn handle_modifier(&self, m: KeyModifier); - fn handle_other(&self); -} - -impl KeyEventReceiver for M { - fn on_key_event(&self, e: KeyEvent) { - match e { - KeyEvent::Char(c) => { - self.handle_char(&c); - } - KeyEvent::Modifier(m) => { - self.handle_modifier(m); - } - KeyEvent::Other => { - self.handle_other(); - } - } - } -} - -// TESTS - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_match_has_vars_should_be_false() { - let match_str = r###" - trigger: ":test" - replace: "There are no variables" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!(content._has_vars, false); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_has_vars_should_be_true() { - let match_str = r###" - trigger: ":test" - replace: "There are {{one}} and {{two}} variables" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!(content._has_vars, true); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_has_vars_with_spaces_should_be_true() { - let match_str = r###" - trigger: ":test" - replace: "There is {{ one }} variable" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!(content._has_vars, true); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_trigger_sequence_without_word() { - let match_str = r###" - trigger: "test" - replace: "This is a test" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t')); - assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e')); - assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s')); - assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t')); - } - - #[test] - fn test_match_trigger_sequence_with_word() { - let match_str = r###" - trigger: "test" - replace: "This is a test" - word: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t')); - assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e')); - assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s')); - assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t')); - assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator); - } - - #[test] - fn test_match_with_image_content() { - let match_str = r###" - trigger: "test" - image_path: "/path/to/file" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Image(content) => { - assert_eq!(content.path, PathBuf::from("/path/to/file")); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_trigger_populates_triggers_vector() { - let match_str = r###" - trigger: ":test" - replace: "This is a test" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.triggers, vec![":test"]) - } - - #[test] - fn test_match_triggers_are_correctly_parsed() { - let match_str = r###" - triggers: - - ":test1" - - :test2 - replace: "This is a test" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.triggers, vec![":test1", ":test2"]) - } - - #[test] - fn test_match_triggers_are_correctly_parsed_square_brackets() { - let match_str = r###" - triggers: [":test1", ":test2"] - replace: "This is a test" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.triggers, vec![":test1", ":test2"]) - } - - #[test] - fn test_match_propagate_case() { - let match_str = r###" - trigger: "hello" - replace: "This is a test" - propagate_case: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.triggers, vec!["hello", "Hello", "HELLO"]) - } - - #[test] - fn test_match_propagate_case_multi_trigger() { - let match_str = r###" - triggers: ["hello", "hi"] - replace: "This is a test" - propagate_case: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!( - _match.triggers, - vec!["hello", "hi", "Hello", "Hi", "HELLO", "HI"] - ) - } - - #[test] - fn test_match_trigger_sequence_with_word_propagate_case() { - let match_str = r###" - trigger: "test" - replace: "This is a test" - word: true - propagate_case: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t')); - assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e')); - assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s')); - assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t')); - assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator); - - assert_eq!(_match._trigger_sequences[1][0], TriggerEntry::Char('T')); - assert_eq!(_match._trigger_sequences[1][1], TriggerEntry::Char('e')); - assert_eq!(_match._trigger_sequences[1][2], TriggerEntry::Char('s')); - assert_eq!(_match._trigger_sequences[1][3], TriggerEntry::Char('t')); - assert_eq!(_match._trigger_sequences[1][4], TriggerEntry::WordSeparator); - - assert_eq!(_match._trigger_sequences[2][0], TriggerEntry::Char('T')); - assert_eq!(_match._trigger_sequences[2][1], TriggerEntry::Char('E')); - assert_eq!(_match._trigger_sequences[2][2], TriggerEntry::Char('S')); - assert_eq!(_match._trigger_sequences[2][3], TriggerEntry::Char('T')); - assert_eq!(_match._trigger_sequences[2][4], TriggerEntry::WordSeparator); - } - - #[test] - fn test_match_empty_replace_doesnt_crash() { - let match_str = r###" - trigger: "hello" - replace: "" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - } - - #[test] - fn test_match_propagate_case_with_prefix_symbol() { - let match_str = r###" - trigger: ":hello" - replace: "This is a test" - propagate_case: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.triggers, vec![":hello", ":Hello", ":HELLO"]) - } - - #[test] - fn test_match_propagate_case_non_alphabetic_should_not_crash() { - let match_str = r###" - trigger: ":.." - replace: "This is a test" - propagate_case: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.triggers, vec![":..", ":..", ":.."]) - } - - #[test] - fn test_match_form_translated_correctly() { - let match_str = r###" - trigger: ":test" - form: "Hey {{name}}, how are you? {{greet}}" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - match _match.content { - MatchContentType::Text(content) => { - let mut mapping = Mapping::new(); - mapping.insert( - Value::from("layout"), - Value::from("Hey {{name}}, how are you? {{greet}}"), - ); - assert_eq!( - content, - TextContent { - replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), - _has_vars: true, - vars: vec![MatchVariable { - name: "form1".to_owned(), - var_type: "form".to_owned(), - params: mapping, - }] - } - ); - } - _ => panic!("wrong content"), - } - } - - #[test] - fn test_match_form_with_fields_translated_correctly() { - let match_str = r###" - trigger: ":test" - form: "Hey {{name}}, how are you? {{greet}}" - form_fields: - name: - multiline: true - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - match _match.content { - MatchContentType::Text(content) => { - let mut name_mapping = Mapping::new(); - name_mapping.insert(Value::from("multiline"), Value::Bool(true)); - let mut submapping = Mapping::new(); - submapping.insert(Value::from("name"), Value::from(name_mapping)); - let mut mapping = Mapping::new(); - mapping.insert(Value::from("fields"), Value::from(submapping)); - mapping.insert( - Value::from("layout"), - Value::from("Hey {{name}}, how are you? {{greet}}"), - ); - assert_eq!( - content, - TextContent { - replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), - _has_vars: true, - vars: vec![MatchVariable { - name: "form1".to_owned(), - var_type: "form".to_owned(), - params: mapping, - }] - } - ); - } - _ => panic!("wrong content"), - } - } - - #[test] - fn test_match_markdown_loaded_correctly() { - let match_str = r###" - trigger: ":test" - markdown: "This *text* is **very bold**" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!( - content.replace, - "This text is very bold" - ); - assert_eq!(_match.is_html, true); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_markdown_keep_vars() { - let match_str = r###" - trigger: ":test" - markdown: "This *text* is {{variable}} **very bold**" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!( - content.replace, - "This text is {{variable}} very bold" - ); - assert_eq!(_match.is_html, true); - assert_eq!(content._has_vars, true); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_html_loaded_correctly() { - let match_str = r###" - trigger: ":test" - html: "This text is very bold" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!(content.replace, "This text is very bold"); - assert_eq!(_match.is_html, true); - } - _ => { - assert!(false); - } - } - } - - #[test] - fn test_match_html_keep_vars() { - let match_str = r###" - trigger: ":test" - html: "This text is {{var}} very bold" - "###; - - let _match: Match = serde_yaml::from_str(match_str).unwrap(); - - match _match.content { - MatchContentType::Text(content) => { - assert_eq!( - content.replace, - "This text is {{var}} very bold" - ); - assert_eq!(_match.is_html, true); - assert_eq!(content._has_vars, true); - } - _ => { - assert!(false); - } - } - } -} diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs deleted file mode 100644 index d78cb48..0000000 --- a/src/matcher/scrolling.rs +++ /dev/null @@ -1,318 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::ConfigManager; -use crate::event::KeyModifier::{BACKSPACE, CAPS_LOCK, LEFT_SHIFT, RIGHT_SHIFT}; -use crate::event::{ActionEventReceiver, ActionType, KeyModifier}; -use crate::matcher::{Match, MatchReceiver, TriggerEntry}; -use std::cell::RefCell; -use std::collections::VecDeque; -use std::time::SystemTime; - -pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { - config_manager: &'a M, - receiver: &'a R, - current_set_queue: RefCell>>>, - toggle_press_time: RefCell, - passive_press_time: RefCell, - is_enabled: RefCell, - was_previous_char_word_separator: RefCell, - was_previous_char_a_match: RefCell, -} - -#[derive(Clone)] -struct MatchEntry<'a> { - start: usize, - count: usize, - trigger_offset: usize, // The index of the trigger in the Match that matched - _match: &'a Match, -} - -impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { - pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> { - let current_set_queue = RefCell::new(VecDeque::new()); - let toggle_press_time = RefCell::new(SystemTime::now()); - let passive_press_time = RefCell::new(SystemTime::now()); - - ScrollingMatcher { - config_manager, - receiver, - current_set_queue, - toggle_press_time, - passive_press_time, - is_enabled: RefCell::new(true), - was_previous_char_word_separator: RefCell::new(true), - was_previous_char_a_match: RefCell::new(true), - } - } - - fn toggle(&self) { - let mut is_enabled = self.is_enabled.borrow_mut(); - *is_enabled = !(*is_enabled); - - self.receiver.on_enable_update(*is_enabled); - } - - fn set_enabled(&self, enabled: bool) { - let mut is_enabled = self.is_enabled.borrow_mut(); - *is_enabled = enabled; - - self.receiver.on_enable_update(*is_enabled); - } - - fn is_matching( - mtc: &Match, - current_char: &str, - start: usize, - trigger_offset: usize, - is_current_word_separator: bool, - ) -> bool { - match mtc._trigger_sequences[trigger_offset][start] { - TriggerEntry::Char(c) => current_char.starts_with(c), - TriggerEntry::WordSeparator => is_current_word_separator, - } - } -} - -impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> { - fn handle_char(&self, c: &str) { - // if not enabled, avoid any processing - if !*(self.is_enabled.borrow()) { - return; - } - - // Obtain the configuration for the active application if present, - // otherwise get the default one - let active_config = self.config_manager.active_config(); - - // Check if the current char is a word separator - let mut is_current_word_separator = active_config - .word_separators - .contains(&c.chars().nth(0).unwrap_or_default()); - - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); - (*was_previous_char_a_match) = false; - - let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut(); - - let mut current_set_queue = self.current_set_queue.borrow_mut(); - - let mut new_matches: Vec = Vec::new(); - - for m in active_config.matches.iter() { - // only active-enabled matches are considered - if m.passive_only { - continue; - } - - for trigger_offset in 0..m._trigger_sequences.len() { - let mut result = - Self::is_matching(m, c, 0, trigger_offset, is_current_word_separator); - - if m.word { - result = result && *was_previous_word_separator - } - - if result { - new_matches.push(MatchEntry { - start: 1, - count: m._trigger_sequences[trigger_offset].len(), - trigger_offset, - _match: &m, - }); - } - } - } - // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup. - - let combined_matches: Vec = match current_set_queue.back_mut() { - Some(last_matches) => { - let mut updated: Vec = last_matches - .iter() - .filter(|&x| { - Self::is_matching( - x._match, - c, - x.start, - x.trigger_offset, - is_current_word_separator, - ) - }) - .map(|x| MatchEntry { - start: x.start + 1, - count: x.count, - trigger_offset: x.trigger_offset, - _match: &x._match, - }) - .collect(); - - updated.extend(new_matches); - updated - } - None => new_matches, - }; - - let mut found_entry = None; - - for entry in combined_matches.iter() { - if entry.start == entry.count { - found_entry = Some(entry.clone()); - break; - } - } - - current_set_queue.push_back(combined_matches); - - if current_set_queue.len() as i32 - > (self.config_manager.default_config().backspace_limit + 1) - { - current_set_queue.pop_front(); - } - - *was_previous_word_separator = is_current_word_separator; - - if let Some(entry) = found_entry { - let mtc = entry._match; - - current_set_queue.clear(); - - let trailing_separator = if !mtc.word { - // If it's not a word match, it cannot have a trailing separator - None - } else if !is_current_word_separator { - None - } else { - let as_char = c.chars().nth(0); - match as_char { - Some(c) => { - Some(c) // Current char is the trailing separator - } - None => None, - } - }; - - // Force espanso to consider the last char as a separator - *was_previous_word_separator = true; - - self.receiver - .on_match(mtc, trailing_separator, entry.trigger_offset); - - (*was_previous_char_a_match) = true; - } - } - - fn handle_modifier(&self, m: KeyModifier) { - let config = self.config_manager.default_config(); - - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); - - // TODO: at the moment, activating the passive key triggers the toggle key - // study a mechanism to avoid this problem - - if KeyModifier::shallow_equals(&m, &config.toggle_key) { - check_interval( - &self.toggle_press_time, - u128::from(config.toggle_interval), - || { - self.toggle(); - - let is_enabled = self.is_enabled.borrow(); - - if !*is_enabled { - self.current_set_queue.borrow_mut().clear(); - } - }, - ); - } else if KeyModifier::shallow_equals(&m, &config.passive_key) { - check_interval( - &self.passive_press_time, - u128::from(config.toggle_interval), - || { - self.receiver.on_passive(); - }, - ); - } - - // Backspace handling, basically "rewinding history" - if m == BACKSPACE { - let mut current_set_queue = self.current_set_queue.borrow_mut(); - current_set_queue.pop_back(); - - if (*was_previous_char_a_match) { - current_set_queue.clear(); - self.receiver.on_undo(); - } - } - - // Disable the "backspace undo" feature - (*was_previous_char_a_match) = false; - - // Consider modifiers as separators to improve word matches reliability - if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK { - let mut was_previous_char_word_separator = - self.was_previous_char_word_separator.borrow_mut(); - *was_previous_char_word_separator = true; - } - } - - fn handle_other(&self) { - // When receiving "other" type of events, we mark them as valid separators. - // This dramatically improves the reliability of word matches - let mut was_previous_char_word_separator = - self.was_previous_char_word_separator.borrow_mut(); - *was_previous_char_word_separator = true; - - // Disable the "backspace undo" feature - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); - (*was_previous_char_a_match) = false; - } -} - -impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver - for ScrollingMatcher<'a, R, M> -{ - fn on_action_event(&self, e: ActionType) { - match e { - ActionType::Toggle => { - self.toggle(); - } - ActionType::Enable => { - self.set_enabled(true); - } - ActionType::Disable => { - self.set_enabled(false); - } - _ => {} - } - } -} - -fn check_interval(state_var: &RefCell, interval: u128, elapsed_callback: F) -where - F: Fn(), -{ - let mut press_time = state_var.borrow_mut(); - if let Ok(elapsed) = press_time.elapsed() { - if elapsed.as_millis() < interval { - elapsed_callback(); - } - } - - (*press_time) = SystemTime::now(); -} diff --git a/src/package/default.rs b/src/package/default.rs deleted file mode 100644 index 69182e3..0000000 --- a/src/package/default.rs +++ /dev/null @@ -1,794 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::package::InstallResult::{AlreadyInstalled, BlockedExternalPackage, NotFoundInIndex}; -use crate::package::RemoveResult::Removed; -use crate::package::UpdateResult::{NotOutdated, Updated}; -use crate::package::{ - InstallResult, Package, PackageIndex, PackageResolver, RemoveResult, UpdateResult, -}; -use regex::Regex; -use std::collections::HashMap; -use std::error::Error; -use std::fs; -use std::fs::{create_dir, File}; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; - -const DEFAULT_PACKAGE_INDEX_FILE: &str = "package_index.json"; - -pub struct DefaultPackageManager { - package_dir: PathBuf, - data_dir: PathBuf, - - package_resolver: Option>, - - local_index: Option, -} - -impl DefaultPackageManager { - pub fn new( - package_dir: PathBuf, - data_dir: PathBuf, - package_resolver: Option>, - ) -> DefaultPackageManager { - let local_index = Self::load_local_index(&data_dir); - - DefaultPackageManager { - package_dir, - data_dir, - package_resolver, - local_index, - } - } - - pub fn new_default( - package_resolver: Option>, - ) -> DefaultPackageManager { - DefaultPackageManager::new( - crate::context::get_package_dir(), - crate::context::get_data_dir(), - package_resolver, - ) - } - - fn get_package_index_path(data_dir: &Path) -> PathBuf { - data_dir.join(DEFAULT_PACKAGE_INDEX_FILE) - } - - fn load_local_index(data_dir: &Path) -> Option { - let local_index_file = File::open(Self::get_package_index_path(data_dir)); - if let Ok(local_index_file) = local_index_file { - let reader = BufReader::new(local_index_file); - let local_index = serde_json::from_reader(reader); - - if let Ok(local_index) = local_index { - return local_index; - } - } - - None - } - - fn request_index() -> Result> { - let client = reqwest::Client::new(); - let request = client - .get("https://hub.espanso.org/json/") - .header("User-Agent", format!("espanso/{}", crate::VERSION)); - - let mut res = request.send()?; - let body = res.text()?; - let index: PackageIndex = serde_json::from_str(&body)?; - - Ok(index) - } - - fn parse_package_from_readme(readme_path: &Path) -> Option { - lazy_static! { - static ref FIELD_REGEX: Regex = - Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap(); - } - - // Read readme line by line - let file = File::open(readme_path); - if let Ok(file) = file { - let reader = BufReader::new(file); - - let mut fields: HashMap = HashMap::new(); - - let mut started = false; - - for (_index, line) in reader.lines().enumerate() { - let line = line.unwrap(); - if line.contains("---") { - if started { - break; - } else { - started = true; - } - } else if started { - let caps = FIELD_REGEX.captures(&line); - if let Some(caps) = caps { - let property = caps.get(1); - let value = caps.get(2); - if property.is_some() && value.is_some() { - fields.insert( - property.unwrap().as_str().to_owned(), - value.unwrap().as_str().to_owned(), - ); - } - } - } - } - - if !fields.contains_key("package_name") - || !fields.contains_key("package_title") - || !fields.contains_key("package_version") - || !fields.contains_key("package_repo") - || !fields.contains_key("package_desc") - || !fields.contains_key("package_author") - { - return None; - } - - let original_repo = if fields.contains_key("package_original_repo") { - fields.get("package_original_repo").unwrap().clone() - } else { - fields.get("package_repo").unwrap().clone() - }; - - let is_core = if fields.contains_key("is_core") { - match fields.get("is_core").unwrap().clone().as_ref() { - "true" => true, - "false" => false, - _ => false, - } - } else { - false - }; - - let package = Package { - name: fields.get("package_name").unwrap().clone(), - title: fields.get("package_title").unwrap().clone(), - version: fields.get("package_version").unwrap().clone(), - repo: fields.get("package_repo").unwrap().clone(), - desc: fields.get("package_desc").unwrap().clone(), - author: fields.get("package_author").unwrap().clone(), - is_core, - original_repo, - }; - - Some(package) - } else { - None - } - } - - fn local_index_timestamp(&self) -> u64 { - if let Some(local_index) = &self.local_index { - return local_index.last_update; - } - - 0 - } - - fn list_local_packages_names(&self) -> Vec { - let dir = fs::read_dir(&self.package_dir); - let mut output = Vec::new(); - if let Ok(dir) = dir { - for entry in dir { - if let Ok(entry) = entry { - let path = entry.path(); - if path.is_dir() { - let name = path.file_name(); - if let Some(name) = name { - output.push(name.to_str().unwrap().to_owned()) - } - } - } - } - } - - output - } - - fn cache_local_index(&self) { - if let Some(local_index) = &self.local_index { - let serialized = - serde_json::to_string(local_index).expect("Unable to serialize local index"); - let local_index_file = self.data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(local_index_file, serialized).expect("Unable to cache local index"); - } - } -} - -impl super::PackageManager for DefaultPackageManager { - fn is_index_outdated(&self) -> bool { - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - let current_timestamp = current_time.as_secs(); - - let local_index_timestamp = self.local_index_timestamp(); - - // Local index is outdated if older than a day - local_index_timestamp + 60 * 60 * 24 < current_timestamp - } - - fn update_index(&mut self, force: bool) -> Result> { - if force || self.is_index_outdated() { - let updated_index = DefaultPackageManager::request_index()?; - self.local_index = Some(updated_index); - - // Save the index to file - self.cache_local_index(); - - Ok(Updated) - } else { - Ok(NotOutdated) - } - } - - fn get_package(&self, name: &str) -> Option { - if let Some(local_index) = &self.local_index { - let result = local_index - .packages - .iter() - .find(|package| package.name == name); - if let Some(package) = result { - return Some(package.clone()); - } - } - - None - } - - fn install_package( - &self, - name: &str, - allow_external: bool, - proxy: Option, - ) -> Result> { - let package = self.get_package(name); - match package { - Some(package) => { - if package.is_core || allow_external { - self.install_package_from_repo(name, &package.repo, proxy) - } else { - Ok(BlockedExternalPackage(package.original_repo)) - } - } - None => Ok(NotFoundInIndex), - } - } - - fn install_package_from_repo( - &self, - name: &str, - repo_url: &str, - proxy: Option, - ) -> Result> { - // Check if package is already installed - let packages = self.list_local_packages_names(); - if packages.iter().any(|p| p == name) { - // Package already installed - return Ok(AlreadyInstalled); - } - - let temp_dir = self - .package_resolver - .as_ref() - .unwrap() - .clone_repo_to_temp(repo_url, proxy)?; - - let temp_package_dir = temp_dir.path().join(name); - if !temp_package_dir.exists() { - return Ok(InstallResult::NotFoundInRepo); - } - - let readme_path = temp_package_dir.join("README.md"); - - let package = Self::parse_package_from_readme(&readme_path); - if package.is_none() { - return Ok(InstallResult::UnableToParsePackageInfo); - } - let package = package.unwrap(); - - let source_dir = temp_package_dir.join(package.version); - if !source_dir.exists() { - return Ok(InstallResult::MissingPackageVersion); - } - - let target_dir = &self.package_dir.join(name); - create_dir(&target_dir)?; - - crate::utils::copy_dir(&source_dir, target_dir)?; - - let readme_dest = target_dir.join("README.md"); - std::fs::copy(readme_path, readme_dest)?; - - Ok(InstallResult::Installed) - } - - fn remove_package(&self, name: &str) -> Result> { - let package_dir = self.package_dir.join(name); - if !package_dir.exists() { - return Ok(RemoveResult::NotFound); - } - - std::fs::remove_dir_all(package_dir)?; - - Ok(Removed) - } - - fn list_local_packages(&self) -> Vec { - let mut output = Vec::new(); - - let package_names = self.list_local_packages_names(); - - for name in package_names.iter() { - let package_dir = &self.package_dir.join(name); - let readme_file = package_dir.join("README.md"); - let package = Self::parse_package_from_readme(&readme_file); - if let Some(package) = package { - output.push(package); - } - } - - output - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::package::zip::ZipPackageResolver; - use crate::package::InstallResult::*; - use crate::package::PackageManager; - use std::fs::{create_dir, create_dir_all}; - use std::path::Path; - use tempfile::{NamedTempFile, TempDir}; - - const OUTDATED_INDEX_CONTENT: &str = include_str!("../res/test/outdated_index.json"); - const INDEX_CONTENT_WITHOUT_UPDATE: &str = - include_str!("../res/test/index_without_update.json"); - const GET_PACKAGE_INDEX: &str = include_str!("../res/test/get_package_index.json"); - const INSTALL_PACKAGE_INDEX: &str = include_str!("../res/test/install_package_index.json"); - - struct TempPackageManager { - package_dir: TempDir, - data_dir: TempDir, - package_manager: DefaultPackageManager, - } - - fn create_temp_package_manager(setup: F) -> TempPackageManager - where - F: Fn(&Path, &Path) -> (), - { - let package_dir = TempDir::new().expect("unable to create temp directory"); - let data_dir = TempDir::new().expect("unable to create temp directory"); - - setup(package_dir.path(), data_dir.path()); - - let package_manager = DefaultPackageManager::new( - package_dir.path().clone().to_path_buf(), - data_dir.path().clone().to_path_buf(), - Some(Box::new(ZipPackageResolver::new())), - ); - - TempPackageManager { - package_dir, - data_dir, - package_manager, - } - } - - #[test] - fn test_download_index() { - create_temp_package_manager(|_, _| {}); - let index = DefaultPackageManager::request_index(); - - assert!(index.is_ok()); - assert!(index.unwrap().packages.len() > 0); - } - - #[test] - fn test_outdated_index() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap(); - }); - - assert!(temp.package_manager.is_index_outdated()); - } - - #[test] - fn test_up_to_date_index_should_not_be_updated() { - let mut temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - let current_timestamp = current_time.as_secs(); - let new_contents = - INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp)); - std::fs::write(index_file, new_contents).unwrap(); - }); - - assert_eq!( - temp.package_manager.update_index(false).unwrap(), - UpdateResult::NotOutdated - ); - } - - #[test] - fn test_up_to_date_index_with_force_should_be_updated() { - let mut temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - let current_timestamp = current_time.as_secs(); - let new_contents = - INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp)); - std::fs::write(index_file, new_contents).unwrap(); - }); - - assert_eq!( - temp.package_manager.update_index(true).unwrap(), - UpdateResult::Updated - ); - } - - #[test] - fn test_outdated_index_should_be_updated() { - let mut temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap(); - }); - - assert_eq!( - temp.package_manager.update_index(false).unwrap(), - UpdateResult::Updated - ); - } - - #[test] - fn test_update_index_should_create_file() { - let mut temp = create_temp_package_manager(|_, _| {}); - - assert_eq!( - temp.package_manager.update_index(false).unwrap(), - UpdateResult::Updated - ); - assert!(temp - .data_dir - .path() - .join(DEFAULT_PACKAGE_INDEX_FILE) - .exists()) - } - - #[test] - fn test_get_package_should_be_found() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .get_package("italian-accents") - .unwrap() - .title, - "Italian Accents" - ); - } - - #[test] - fn test_get_package_should_not_be_found() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap(); - }); - - assert!(temp.package_manager.get_package("not-existing").is_none()); - } - - #[test] - fn test_list_local_packages_names() { - let temp = create_temp_package_manager(|package_dir, _| { - create_dir(package_dir.join("package-1")).unwrap(); - create_dir(package_dir.join("package2")).unwrap(); - std::fs::write(package_dir.join("dummyfile.txt"), "test").unwrap(); - }); - - let packages = temp.package_manager.list_local_packages_names(); - assert_eq!(packages.len(), 2); - assert!(packages.iter().any(|p| p == "package-1")); - assert!(packages.iter().any(|p| p == "package2")); - } - - #[test] - fn test_install_package_not_found() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("doesnotexist", false, None) - .unwrap(), - NotFoundInIndex - ); - } - - #[test] - fn test_install_package_already_installed() { - let temp = create_temp_package_manager(|package_dir, data_dir| { - create_dir(package_dir.join("italian-accents")).unwrap(); - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("italian-accents", false, None) - .unwrap(), - AlreadyInstalled - ); - } - - #[test] - fn test_install_package() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("dummy-package", false, None) - .unwrap(), - Installed - ); - assert!(temp.package_dir.path().join("dummy-package").exists()); - assert!(temp - .package_dir - .path() - .join("dummy-package/README.md") - .exists()); - assert!(temp - .package_dir - .path() - .join("dummy-package/package.yml") - .exists()); - } - - #[test] - fn test_install_package_does_not_exist_in_repo() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("not-existing", false, None) - .unwrap(), - NotFoundInRepo - ); - } - - #[test] - fn test_install_package_missing_version() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("dummy-package2", false, None) - .unwrap(), - MissingPackageVersion - ); - } - - #[test] - fn test_install_package_missing_readme_unable_to_parse_package_info() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("dummy-package3", false, None) - .unwrap(), - UnableToParsePackageInfo - ); - } - - #[test] - fn test_install_package_bad_readme_unable_to_parse_package_info() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("dummy-package4", false, None) - .unwrap(), - UnableToParsePackageInfo - ); - } - - #[test] - fn test_list_local_packages() { - let temp = create_temp_package_manager(|_, data_dir| { - let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); - std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); - }); - - assert_eq!( - temp.package_manager - .install_package("dummy-package", false, None) - .unwrap(), - Installed - ); - assert!(temp.package_dir.path().join("dummy-package").exists()); - assert!(temp - .package_dir - .path() - .join("dummy-package/README.md") - .exists()); - assert!(temp - .package_dir - .path() - .join("dummy-package/package.yml") - .exists()); - - let list = temp.package_manager.list_local_packages(); - assert_eq!(list.len(), 1); - assert_eq!(list[0].name, "dummy-package"); - } - - #[test] - fn test_remove_package() { - let temp = create_temp_package_manager(|package_dir, _| { - let dummy_package_dir = package_dir.join("dummy-package"); - create_dir_all(&dummy_package_dir).unwrap(); - std::fs::write(dummy_package_dir.join("README.md"), "readme").unwrap(); - std::fs::write(dummy_package_dir.join("package.yml"), "name: package").unwrap(); - }); - - assert!(temp.package_dir.path().join("dummy-package").exists()); - assert!(temp - .package_dir - .path() - .join("dummy-package/README.md") - .exists()); - assert!(temp - .package_dir - .path() - .join("dummy-package/package.yml") - .exists()); - assert_eq!( - temp.package_manager - .remove_package("dummy-package") - .unwrap(), - RemoveResult::Removed - ); - assert!(!temp.package_dir.path().join("dummy-package").exists()); - assert!(!temp - .package_dir - .path() - .join("dummy-package/README.md") - .exists()); - assert!(!temp - .package_dir - .path() - .join("dummy-package/package.yml") - .exists()); - } - - #[test] - fn test_remove_package_not_found() { - let temp = create_temp_package_manager(|_, _| {}); - - assert_eq!( - temp.package_manager.remove_package("not-existing").unwrap(), - RemoveResult::NotFound - ); - } - - #[test] - fn test_parse_package_from_readme() { - let file = NamedTempFile::new().unwrap(); - fs::write( - file.path(), - r###" - --- - package_name: "italian-accents" - package_title: "Italian Accents" - package_desc: "Include Italian accents substitutions to espanso." - package_version: "0.1.0" - package_author: "Federico Terzi" - package_repo: "https://github.com/federico-terzi/espanso-hub-core" - is_core: true - --- - "###, - ) - .unwrap(); - - let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap(); - - let target_package = Package { - name: "italian-accents".to_string(), - title: "Italian Accents".to_string(), - version: "0.1.0".to_string(), - repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(), - desc: "Include Italian accents substitutions to espanso.".to_string(), - author: "Federico Terzi".to_string(), - original_repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(), - is_core: true, - }; - - assert_eq!(package, target_package); - } - - #[test] - fn test_parse_package_from_readme_with_bad_metadata() { - let file = NamedTempFile::new().unwrap(); - fs::write( - file.path(), - r###" - --- - package_name: italian-accents - package_title: "Italian Accents" - package_desc: "Include Italian accents substitutions to espanso." - package_version:"0.1.0" - package_author:Federico Terzi - package_repo: "https://github.com/federico-terzi/espanso-hub-core" - is_core: true - --- - Readme text - "###, - ) - .unwrap(); - - let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap(); - - let target_package = Package { - name: "italian-accents".to_string(), - title: "Italian Accents".to_string(), - version: "0.1.0".to_string(), - repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(), - desc: "Include Italian accents substitutions to espanso.".to_string(), - author: "Federico Terzi".to_string(), - original_repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(), - is_core: true, - }; - - assert_eq!(package, target_package); - } -} diff --git a/src/package/mod.rs b/src/package/mod.rs deleted file mode 100644 index ea6d23d..0000000 --- a/src/package/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -pub(crate) mod default; -pub(crate) mod zip; - -use serde::{Deserialize, Serialize}; -use std::error::Error; -use tempfile::TempDir; - -pub trait PackageManager { - fn is_index_outdated(&self) -> bool; - fn update_index(&mut self, force: bool) -> Result>; - - fn get_package(&self, name: &str) -> Option; - - fn install_package( - &self, - name: &str, - allow_external: bool, - proxy: Option, - ) -> Result>; - fn install_package_from_repo( - &self, - name: &str, - repo_url: &str, - proxy: Option, - ) -> Result>; - - fn remove_package(&self, name: &str) -> Result>; - - fn list_local_packages(&self) -> Vec; -} - -pub trait PackageResolver { - fn clone_repo_to_temp( - &self, - repo_url: &str, - proxy: Option, - ) -> Result>; -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Package { - pub name: String, - pub title: String, - pub version: String, - pub repo: String, - pub desc: String, - pub author: String, - - #[serde(default = "default_is_core")] - pub is_core: bool, - #[serde(default = "default_original_repo")] - pub original_repo: String, -} - -fn default_is_core() -> bool { - false -} -fn default_original_repo() -> String { - "".to_owned() -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct PackageIndex { - #[serde(rename = "lastUpdate")] - pub last_update: u64, - - pub packages: Vec, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum UpdateResult { - NotOutdated, - Updated, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum InstallResult { - NotFoundInIndex, - NotFoundInRepo, - UnableToParsePackageInfo, - MissingPackageVersion, - AlreadyInstalled, - Installed, - BlockedExternalPackage(String), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum RemoveResult { - NotFound, - Removed, -} diff --git a/src/package/zip.rs b/src/package/zip.rs deleted file mode 100644 index b3a197a..0000000 --- a/src/package/zip.rs +++ /dev/null @@ -1,129 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use log::debug; -use std::error::Error; -use std::io::{copy, Cursor}; -use std::{fs, io}; -use tempfile::TempDir; - -pub struct ZipPackageResolver; - -impl ZipPackageResolver { - pub fn new() -> ZipPackageResolver { - return ZipPackageResolver {}; - } -} - -impl super::PackageResolver for ZipPackageResolver { - fn clone_repo_to_temp( - &self, - repo_url: &str, - proxy: Option, - ) -> Result> { - let temp_dir = TempDir::new()?; - - let zip_url = repo_url.to_owned() + "/archive/master.zip"; - - let mut client = reqwest::Client::builder(); - - if let Some(proxy) = proxy { - let proxy = reqwest::Proxy::https(&proxy).expect("unable to setup https proxy"); - client = client.proxy(proxy); - }; - - let client = client.build().expect("unable to create http client"); - - // Download the archive from GitHub - let mut response = client.get(&zip_url).send()?; - - // Extract zip file - let mut buffer = Vec::new(); - copy(&mut response, &mut buffer)?; - - let reader = Cursor::new(buffer); - - let mut archive = zip::ZipArchive::new(reader).unwrap(); - - // Find the root folder name - let mut root_folder = { - let root_folder = archive.by_index(0).unwrap(); - let root_folder = root_folder.sanitized_name(); - root_folder.to_str().unwrap().to_owned() - }; - root_folder.push(std::path::MAIN_SEPARATOR); - - for i in 1..archive.len() { - let mut file = archive.by_index(i).unwrap(); - - let current_path = file.sanitized_name(); - let current_filename = current_path.to_str().unwrap(); - let trimmed_filename = current_filename.trim_start_matches(&root_folder); - - let outpath = temp_dir.path().join(trimmed_filename); - - { - let comment = file.comment(); - if !comment.is_empty() { - debug!("File {} comment: {}", i, comment); - } - } - - if (&*file.name()).ends_with('/') { - debug!( - "File {} extracted to \"{}\"", - i, - outpath.as_path().display() - ); - fs::create_dir_all(&outpath).unwrap(); - } else { - debug!( - "File {} extracted to \"{}\" ({} bytes)", - i, - outpath.as_path().display(), - file.size() - ); - if let Some(p) = outpath.parent() { - if !p.exists() { - fs::create_dir_all(&p).unwrap(); - } - } - let mut outfile = fs::File::create(&outpath).unwrap(); - io::copy(&mut file, &mut outfile).unwrap(); - } - } - - Ok(temp_dir) - } -} - -#[cfg(test)] -mod tests { - use super::super::PackageResolver; - use super::*; - - #[test] - fn test_clone_temp_repository() { - let resolver = ZipPackageResolver::new(); - let cloned_dir = resolver - .clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core", None) - .unwrap(); - assert!(cloned_dir.path().join("LICENSE").exists()); - } -} diff --git a/src/process.rs b/src/process.rs deleted file mode 100644 index 660abb0..0000000 --- a/src/process.rs +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use log::warn; -use std::io; -use std::process::{Child, Command, Stdio}; - -#[cfg(target_os = "windows")] -pub fn spawn_process(cmd: &str, args: &Vec) -> io::Result { - use std::os::windows::process::CommandExt; - Command::new(cmd) - .creation_flags(0x08000008) // Detached Process without window - .args(args) - .spawn() -} - -#[cfg(not(target_os = "windows"))] -pub fn spawn_process(cmd: &str, args: &Vec) -> io::Result { - Command::new(cmd).args(args).spawn() -} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs deleted file mode 100644 index a609160..0000000 --- a/src/protocol/mod.rs +++ /dev/null @@ -1,224 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::Configs; -use crate::event::ActionType; -use crate::event::{Event, SystemEvent}; -use log::error; -use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::io::{BufReader, Read, Write}; -use std::sync::mpsc::Sender; - -#[cfg(target_os = "windows")] -mod windows; - -#[cfg(not(target_os = "windows"))] -mod unix; - -pub trait IPCServer { - fn start(&self); -} - -pub trait IPCClient { - fn send_command(&self, command: IPCCommand) -> Result<(), String>; -} - -pub fn send_command_or_warn(service: Service, configs: Configs, command: IPCCommand) { - let ipc_client = get_ipc_client(service, configs); - if let Err(e) = ipc_client.send_command(command) { - error!("unable to send command to IPC server"); - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct IPCCommand { - pub id: String, - - #[serde(default)] - pub payload: String, -} - -impl IPCCommand { - fn to_event(&self) -> Option { - match self.id.as_ref() { - "exit" => Some(Event::Action(ActionType::Exit)), - "wexit" => Some(Event::Action(ActionType::ExitWorker)), - "toggle" => Some(Event::Action(ActionType::Toggle)), - "enable" => Some(Event::Action(ActionType::Enable)), - "disable" => Some(Event::Action(ActionType::Disable)), - "restartworker" => Some(Event::Action(ActionType::RestartWorker)), - "notify" => Some(Event::System(SystemEvent::NotifyRequest( - self.payload.clone(), - ))), - "trigger" => Some(Event::System(SystemEvent::Trigger(self.payload.clone()))), - _ => None, - } - } - - pub fn from(event: Event) -> Option { - match event { - Event::Action(ActionType::Exit) => Some(IPCCommand { - id: "exit".to_owned(), - payload: "".to_owned(), - }), - Event::Action(ActionType::ExitWorker) => Some(IPCCommand { - id: "wexit".to_owned(), - payload: "".to_owned(), - }), - Event::Action(ActionType::Toggle) => Some(IPCCommand { - id: "toggle".to_owned(), - payload: "".to_owned(), - }), - Event::Action(ActionType::Enable) => Some(IPCCommand { - id: "enable".to_owned(), - payload: "".to_owned(), - }), - Event::Action(ActionType::Disable) => Some(IPCCommand { - id: "disable".to_owned(), - payload: "".to_owned(), - }), - Event::Action(ActionType::RestartWorker) => Some(IPCCommand { - id: "restartworker".to_owned(), - payload: "".to_owned(), - }), - Event::System(SystemEvent::NotifyRequest(message)) => Some(IPCCommand { - id: "notify".to_owned(), - payload: message, - }), - Event::System(SystemEvent::Trigger(trigger)) => Some(IPCCommand { - id: "trigger".to_owned(), - payload: trigger, - }), - _ => None, - } - } - - pub fn exit() -> IPCCommand { - Self { - id: "exit".to_owned(), - payload: "".to_owned(), - } - } - - pub fn exit_worker() -> IPCCommand { - Self { - id: "wexit".to_owned(), - payload: "".to_owned(), - } - } - - pub fn restart_worker() -> IPCCommand { - Self { - id: "restartworker".to_owned(), - payload: "".to_owned(), - } - } - - pub fn trigger(trigger: &str) -> IPCCommand { - Self { - id: "trigger".to_owned(), - payload: trigger.to_owned(), - } - } -} - -fn process_event(event_channel: &Sender, stream: Result) { - match stream { - Ok(stream) => { - let mut json_str = String::new(); - let mut buf_reader = BufReader::new(stream); - let res = buf_reader.read_to_string(&mut json_str); - - if res.is_ok() { - let command: Result = - serde_json::from_str(&json_str); - match command { - Ok(command) => { - let event = command.to_event(); - if let Some(event) = event { - event_channel.send(event).expect("Broken event channel"); - } - } - Err(e) => { - error!("Error deserializing JSON command: {}", e); - } - } - } - } - Err(err) => { - println!("Error: {}", err); - } - } -} - -fn send_command( - command: IPCCommand, - stream: Result, -) -> Result<(), String> { - match stream { - Ok(mut stream) => { - let json_str = serde_json::to_string(&command); - if let Ok(json_str) = json_str { - stream.write_all(json_str.as_bytes()).unwrap_or_else(|e| { - println!("Can't write to IPC socket: {}", e); - }); - return Ok(()); - } - } - Err(e) => return Err(format!("Can't connect to daemon: {}", e)), - } - - Err("Can't send command".to_owned()) -} - -pub enum Service { - Daemon, - Worker, -} - -// UNIX IMPLEMENTATION -#[cfg(not(target_os = "windows"))] -pub fn get_ipc_server( - service: Service, - _: Configs, - event_channel: Sender, -) -> impl IPCServer { - unix::UnixIPCServer::new(service, event_channel) -} - -#[cfg(not(target_os = "windows"))] -pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient { - unix::UnixIPCClient::new(service) -} - -// WINDOWS IMPLEMENTATION -#[cfg(target_os = "windows")] -pub fn get_ipc_server( - service: Service, - _: Configs, - event_channel: Sender, -) -> impl IPCServer { - windows::WindowsIPCServer::new(service, event_channel) -} - -#[cfg(target_os = "windows")] -pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient { - windows::WindowsIPCClient::new(service) -} diff --git a/src/protocol/unix.rs b/src/protocol/unix.rs deleted file mode 100644 index 98313da..0000000 --- a/src/protocol/unix.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::IPCCommand; -use log::{info, warn}; -use std::os::unix::net::{UnixListener, UnixStream}; -use std::sync::mpsc::Sender; - -use super::Service; -use crate::context; -use crate::event::*; -use crate::protocol::{process_event, send_command}; - -const DAEMON_UNIX_SOCKET_NAME: &str = "espanso.sock"; -const WORKER_UNIX_SOCKET_NAME: &str = "worker.sock"; - -pub struct UnixIPCServer { - service: Service, - event_channel: Sender, -} - -impl UnixIPCServer { - pub fn new(service: Service, event_channel: Sender) -> UnixIPCServer { - UnixIPCServer { - service, - event_channel, - } - } -} - -fn get_unix_name(service: &Service) -> String { - match service { - Service::Daemon => DAEMON_UNIX_SOCKET_NAME.to_owned(), - Service::Worker => WORKER_UNIX_SOCKET_NAME.to_owned(), - } -} - -impl super::IPCServer for UnixIPCServer { - fn start(&self) { - let event_channel = self.event_channel.clone(); - let socket_name = get_unix_name(&self.service); - std::thread::Builder::new() - .name("ipc_server".to_string()) - .spawn(move || { - let espanso_dir = context::get_data_dir(); - let unix_socket = espanso_dir.join(socket_name); - - std::fs::remove_file(unix_socket.clone()).unwrap_or_else(|e| { - warn!("Unable to delete Unix socket: {}", e); - }); - let listener = - UnixListener::bind(unix_socket.clone()).expect("Can't bind to Unix Socket"); - - info!( - "Binded to IPC unix socket: {}", - unix_socket.as_path().display() - ); - - for stream in listener.incoming() { - process_event(&event_channel, stream); - } - }) - .expect("Unable to spawn IPC server thread"); - } -} - -pub struct UnixIPCClient { - service: Service, -} - -impl UnixIPCClient { - pub fn new(service: Service) -> UnixIPCClient { - UnixIPCClient { service } - } -} - -impl super::IPCClient for UnixIPCClient { - fn send_command(&self, command: IPCCommand) -> Result<(), String> { - let espanso_dir = context::get_data_dir(); - let socket_name = get_unix_name(&self.service); - let unix_socket = espanso_dir.join(socket_name); - - // Open the stream - let stream = UnixStream::connect(unix_socket); - - send_command(command, stream) - } -} diff --git a/src/protocol/windows.rs b/src/protocol/windows.rs deleted file mode 100644 index 10ff042..0000000 --- a/src/protocol/windows.rs +++ /dev/null @@ -1,98 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::IPCCommand; -use log::info; -use std::net::{TcpListener, TcpStream}; -use std::sync::mpsc::Sender; - -use crate::config::Configs; -use crate::context; -use crate::event::*; -use crate::protocol::{process_event, send_command, Service}; -use named_pipe::{PipeClient, PipeOptions, PipeServer}; -use std::io::Error; -use std::path::PathBuf; - -const DAEMON_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansodaemon"; -const WORKER_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansoworker"; -const CLIENT_TIMEOUT: u32 = 2000; - -pub struct WindowsIPCServer { - service: Service, - event_channel: Sender, -} - -fn get_pipe_name(service: &Service) -> String { - match service { - Service::Daemon => DAEMON_WIN_PIPE_NAME.to_owned(), - Service::Worker => WORKER_WIN_PIPE_NAME.to_owned(), - } -} - -impl WindowsIPCServer { - pub fn new(service: Service, event_channel: Sender) -> WindowsIPCServer { - WindowsIPCServer { - service, - event_channel, - } - } -} - -impl super::IPCServer for WindowsIPCServer { - fn start(&self) { - let event_channel = self.event_channel.clone(); - let pipe_name = get_pipe_name(&self.service); - std::thread::Builder::new() - .name("ipc_server".to_string()) - .spawn(move || { - let options = PipeOptions::new(&pipe_name); - - info!("Binding to named pipe: {}", pipe_name); - - loop { - let server = options - .single() - .expect("unable to initialize IPC named pipe"); - let pipe_server = server.wait(); - process_event(&event_channel, pipe_server); - } - }) - .expect("Unable to spawn IPC server thread"); - } -} - -pub struct WindowsIPCClient { - service: Service, -} - -impl WindowsIPCClient { - pub fn new(service: Service) -> WindowsIPCClient { - WindowsIPCClient { service } - } -} - -impl super::IPCClient for WindowsIPCClient { - fn send_command(&self, command: IPCCommand) -> Result<(), String> { - let pipe_name = get_pipe_name(&self.service); - let client = PipeClient::connect_ms(pipe_name, CLIENT_TIMEOUT); - - send_command(command, client) - } -} diff --git a/src/render/default.rs b/src/render/default.rs deleted file mode 100644 index 71e8f23..0000000 --- a/src/render/default.rs +++ /dev/null @@ -1,902 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::*; -use crate::config::Configs; -use crate::extension::{Extension, ExtensionResult}; -use crate::matcher::{Match, MatchContentType, MatchVariable}; -use log::{error, warn}; -use regex::{Captures, Regex}; -use serde_yaml::Value; -use std::collections::{HashMap, HashSet}; - -lazy_static! { - static ref VAR_REGEX: Regex = - Regex::new(r"\{\{\s*((?P\w+)(\.(?P(\w+)))?)\s*\}\}").unwrap(); - static ref UNKNOWN_VARIABLE: String = "".to_string(); -} - -pub struct DefaultRenderer { - extension_map: HashMap>, - - // Regex used to identify matches (and arguments) in passive expansions - passive_match_regex: Regex, -} - -impl DefaultRenderer { - pub fn new(extensions: Vec>, config: Configs) -> DefaultRenderer { - // Register all the extensions - let mut extension_map = HashMap::new(); - for extension in extensions.into_iter() { - extension_map.insert(extension.name(), extension); - } - - // Compile the regexes - let passive_match_regex = Regex::new(&config.passive_match_regex).unwrap_or_else(|e| { - panic!("Invalid passive match regex: {:?}", e); - }); - - DefaultRenderer { - extension_map, - passive_match_regex, - } - } - - fn find_match(config: &Configs, trigger: &str) -> Option<(Match, usize)> { - let mut result = None; - - // TODO: if performances become a problem, implement a more efficient lookup - for m in config.matches.iter() { - for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() { - if m_trigger == trigger { - result = Some((m.clone(), trigger_offset)); - break; - } - } - } - - result - } -} - -impl super::Renderer for DefaultRenderer { - fn render_match( - &self, - m: &Match, - trigger_offset: usize, - config: &Configs, - args: Vec, - ) -> RenderResult { - // Manage the different types of matches - match &m.content { - // Text Match - MatchContentType::Text(content) => { - let target_string = if content._has_vars { - // Find all the variables that are required by the current match - let mut target_vars: HashSet = HashSet::new(); - - for caps in VAR_REGEX.captures_iter(&content.replace) { - let var_name = caps.name("name").unwrap().as_str(); - target_vars.insert(var_name.to_owned()); - } - - let match_variables: HashSet<&String> = - content.vars.iter().map(|var| &var.name).collect(); - - // Find the global variables that are not specified in the var list - let mut missing_globals = Vec::new(); - let mut specified_globals: HashMap = HashMap::new(); - for global_var in config.global_vars.iter() { - if target_vars.contains(&global_var.name) { - if match_variables.contains(&global_var.name) { - specified_globals.insert(global_var.name.clone(), &global_var); - } else { - missing_globals.push(global_var); - } - } - } - - // Determine the variable evaluation order - let mut variables: Vec<&MatchVariable> = Vec::new(); - // First place the global that are not explicitly specified - variables.extend(missing_globals); - // Then the ones explicitly specified, in the given order - variables.extend(&content.vars); - - // Replace variable type "global" with the actual reference - let variables: Vec<&MatchVariable> = variables - .into_iter() - .map(|variable| { - if variable.var_type == "global" { - if let Some(actual_variable) = specified_globals.get(&variable.name) - { - return actual_variable.clone(); - } - } - variable - }) - .collect(); - - let mut output_map: HashMap = HashMap::new(); - - for variable in variables.into_iter() { - // In case of variables of type match, we need to recursively call - // the render function - if variable.var_type == "match" { - // Extract the match trigger from the variable params - let trigger = variable.params.get(&Value::from("trigger")); - if trigger.is_none() { - warn!( - "Missing param 'trigger' in match variable: {}", - variable.name - ); - continue; - } - let trigger = trigger.unwrap(); - - // Find the given match from the active configs - let inner_match = - DefaultRenderer::find_match(config, trigger.as_str().unwrap_or("")); - - if inner_match.is_none() { - warn!( - "Could not find inner match with trigger: '{}'", - trigger.as_str().unwrap_or("undefined") - ); - continue; - } - - let (inner_match, trigger_offset) = inner_match.unwrap(); - - // Render the inner match - // TODO: inner arguments - let result = - self.render_match(&inner_match, trigger_offset, config, vec![]); - - // Inner matches are only supported for text-expansions, warn the user otherwise - match result { - RenderResult::Text(inner_content) => { - output_map.insert(variable.name.clone(), ExtensionResult::Single(inner_content)); - }, - _ => { - warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.") - }, - } - } else { - // Normal extension variables - let extension = self.extension_map.get(&variable.var_type); - if let Some(extension) = extension { - let ext_res = - extension.calculate(&variable.params, &args, &output_map); - match ext_res { - Ok(ext_out) => { - if let Some(output) = ext_out { - output_map.insert(variable.name.clone(), output); - } else { - output_map.insert( - variable.name.clone(), - ExtensionResult::Single("".to_owned()), - ); - warn!( - "Could not generate output for variable: {}", - variable.name - ); - } - } - Err(_) => return RenderResult::Error, - } - } else { - error!( - "No extension found for variable type: {}", - variable.var_type - ); - } - } - } - - // Replace the variables - let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| { - let var_name = caps.name("name").unwrap().as_str(); - let var_subname = caps.name("subname"); - match output_map.get(var_name) { - Some(result) => match result { - ExtensionResult::Single(output) => output, - ExtensionResult::Multiple(results) => match var_subname { - Some(var_subname) => { - let var_subname = var_subname.as_str(); - results.get(var_subname).unwrap_or(&UNKNOWN_VARIABLE) - } - None => { - error!( - "nested name missing from multi-value variable: {}", - var_name - ); - &UNKNOWN_VARIABLE - } - }, - }, - None => &UNKNOWN_VARIABLE, - } - }); - - result.to_string() - } else { - // No variables, simple text substitution - content.replace.clone() - }; - - // Unescape any brackets (needed to be able to insert double brackets in replacement - // text, without triggering the variable system). See issue #187 - let mut target_string = target_string.replace("\\{", "{").replace("\\}", "}"); - - // Render any argument that may be present - if !args.is_empty() { - target_string = utils::render_args(&target_string, &args); - } - - // Handle case propagation - target_string = if m.propagate_case { - let trigger = &m.triggers[trigger_offset]; - - // The check should be carried out from the position of the first - // alphabetic letter - // See issue #244 - let first_alphabetic = - trigger.chars().position(|c| c.is_alphabetic()).unwrap_or(0); - - let first_char = trigger.chars().nth(first_alphabetic); - let second_char = trigger.chars().nth(first_alphabetic + 1); - let mode: i32 = if let Some(first_char) = first_char { - if first_char.is_uppercase() { - if let Some(second_char) = second_char { - if second_char.is_uppercase() { - 2 // Full CAPITALIZATION - } else { - 1 // Only first letter capitalized: Capitalization - } - } else { - 2 // Single char, defaults to full CAPITALIZATION - } - } else { - 0 // Lowercase, no action - } - } else { - 0 - }; - - match mode { - 1 => { - // Capitalize the first letter - let mut v: Vec = target_string.chars().collect(); - v[0] = v[0].to_uppercase().nth(0).unwrap(); - v.into_iter().collect() - } - 2 => { - // Full capitalization - target_string.to_uppercase() - } - _ => { - // Noop - target_string - } - } - } else { - target_string - }; - - RenderResult::Text(target_string) - } - - // Image Match - MatchContentType::Image(content) => { - // Make sure the image exist beforehand - if content.path.exists() { - RenderResult::Image(content.path.clone()) - } else { - error!("Image not found in path: {:?}", content.path); - RenderResult::Error - } - } - } - } - - fn render_passive(&self, text: &str, config: &Configs) -> RenderResult { - // Render the matches - let result = self - .passive_match_regex - .replace_all(&text, |caps: &Captures| { - let match_name = if let Some(name) = caps.name("name") { - name.as_str() - } else { - "" - }; - - // Get the original matching string, useful to return the match untouched - let original_match = caps.get(0).unwrap().as_str(); - - // Find the corresponding match - let m = DefaultRenderer::find_match(config, match_name); - - // If no match is found, leave the match without modifications - if m.is_none() { - return original_match.to_owned(); - } - - // Compute the args by separating them - let match_args = if let Some(args) = caps.name("args") { - args.as_str() - } else { - "" - }; - let args: Vec = utils::split_args( - match_args, - config.passive_arg_delimiter, - config.passive_arg_escape, - ); - - let (m, trigger_offset) = m.unwrap(); - // Render the actual match - let result = self.render_match(&m, trigger_offset, &config, args); - - match result { - RenderResult::Text(out) => out, - _ => original_match.to_owned(), - } - }); - - RenderResult::Text(result.into_owned()) - } -} - -// TESTS - -#[cfg(test)] -mod tests { - use super::*; - - fn get_renderer(config: Configs) -> DefaultRenderer { - DefaultRenderer::new( - vec![ - Box::new(crate::extension::dummy::DummyExtension::new("dummy")), - Box::new(crate::extension::vardummy::VarDummyExtension::new()), - Box::new(crate::extension::multiecho::MultiEchoExtension::new()), - ], - config, - ) - } - - fn get_config_for(s: &str) -> Configs { - let config: Configs = serde_yaml::from_str(s).unwrap(); - config - } - - fn verify_render(rendered: RenderResult, target: &str) { - match rendered { - RenderResult::Text(rendered) => { - assert_eq!(rendered, target); - } - _ => assert!(false), - } - } - - #[test] - fn test_render_passive_no_matches() { - let text = r###" - this text contains no matches - "###; - - let config = get_config_for( - r###" - matches: - - trigger: test - replace: result - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, text); - } - - #[test] - fn test_render_passive_simple_match_no_args() { - let text = "this is a :test"; - - let config = get_config_for( - r###" - matches: - - trigger: ':test' - replace: result - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is a result"); - } - - #[test] - fn test_render_passive_multiple_match_no_args() { - let text = "this is a :test and then another :test"; - - let config = get_config_for( - r###" - matches: - - trigger: ':test' - replace: result - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is a result and then another result"); - } - - #[test] - fn test_render_passive_simple_match_multiline_no_args() { - let text = r###"this is a - :test - "###; - - let result = r###"this is a - result - "###; - - let config = get_config_for( - r###" - matches: - - trigger: ':test' - replace: result - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, result); - } - - #[test] - fn test_render_passive_nested_matches_no_args() { - let text = ":greet"; - - let config = get_config_for( - r###" - matches: - - trigger: ':greet' - replace: "hi {{name}}" - vars: - - name: name - type: match - params: - trigger: ":name" - - - trigger: ':name' - replace: john - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "hi john"); - } - - #[test] - fn test_render_passive_simple_match_with_args() { - let text = ":greet/Jon/"; - - let config = get_config_for( - r###" - matches: - - trigger: ':greet' - replace: "Hi $0$" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "Hi Jon"); - } - - #[test] - fn test_render_passive_simple_match_no_args_should_not_replace_args_syntax() { - let text = ":greet"; - - let config = get_config_for( - r###" - matches: - - trigger: ':greet' - replace: "Hi $0$" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "Hi $0$"); - } - - #[test] - fn test_render_passive_simple_match_with_multiple_args() { - let text = ":greet/Jon/Snow/"; - - let config = get_config_for( - r###" - matches: - - trigger: ':greet' - replace: "Hi $0$, there is $1$ outside" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "Hi Jon, there is Snow outside"); - } - - #[test] - fn test_render_passive_simple_match_with_escaped_args() { - let text = ":greet/Jon/10\\/12/"; - - let config = get_config_for( - r###" - matches: - - trigger: ':greet' - replace: "Hi $0$, today is $1$" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "Hi Jon, today is 10/12"); - } - - #[test] - fn test_render_passive_simple_match_with_args_not_closed() { - let text = ":greet/Jon/Snow"; - - let config = get_config_for( - r###" - matches: - - trigger: ':greet' - replace: "Hi $0$" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "Hi JonSnow"); - } - - #[test] - fn test_render_passive_local_var() { - let text = "this is :test"; - - let config = get_config_for( - r###" - matches: - - trigger: ':test' - replace: "my {{output}}" - vars: - - name: output - type: dummy - params: - echo: "result" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is my result"); - } - - #[test] - fn test_render_passive_global_var() { - let text = "this is :test"; - - let config = get_config_for( - r###" - global_vars: - - name: output - type: dummy - params: - echo: "result" - matches: - - trigger: ':test' - replace: "my {{output}}" - - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is my result"); - } - - #[test] - fn test_render_passive_global_var_is_overridden_by_local() { - let text = "this is :test"; - - let config = get_config_for( - r###" - global_vars: - - name: output - type: dummy - params: - echo: "result" - matches: - - trigger: ':test' - replace: "my {{output}}" - vars: - - name: "output" - type: dummy - params: - echo: "local" - - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is my local"); - } - - #[test] - fn test_render_match_with_unknown_variable_does_not_crash() { - let text = "this is :test"; - - let config = get_config_for( - r###" - matches: - - trigger: ':test' - replace: "my {{unknown}}" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is my "); - } - - #[test] - fn test_render_escaped_double_brackets_should_not_consider_them_variable() { - let text = "this is :test"; - - let config = get_config_for( - r###" - matches: - - trigger: ':test' - replace: "my \\{\\{unknown\\}\\}" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is my {{unknown}}"); - } - - #[test] - fn test_render_passive_simple_match_multi_trigger_no_args() { - let text = "this is a :yolo and :test"; - - let config = get_config_for( - r###" - matches: - - triggers: [':test', ':yolo'] - replace: result - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "this is a result and result"); - } - - #[test] - fn test_render_passive_simple_match_multi_trigger_with_args() { - let text = ":yolo/Jon/"; - - let config = get_config_for( - r###" - matches: - - triggers: [':greet', ':yolo'] - replace: "Hi $0$" - "###, - ); - - let renderer = get_renderer(config.clone()); - - let rendered = renderer.render_passive(text, &config); - - verify_render(rendered, "Hi Jon"); - } - - #[test] - fn test_render_match_case_propagation_no_case() { - let config = get_config_for( - r###" - matches: - - trigger: 'test' - replace: result - propagate_case: true - "###, - ); - - let renderer = get_renderer(config.clone()); - - let m = config.matches[0].clone(); - - let trigger_offset = m.triggers.iter().position(|x| x == "test").unwrap(); - - let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]); - - verify_render(rendered, "result"); - } - - #[test] - fn test_render_match_case_propagation_first_capital() { - let config = get_config_for( - r###" - matches: - - trigger: 'test' - replace: result - propagate_case: true - "###, - ); - - let renderer = get_renderer(config.clone()); - - let m = config.matches[0].clone(); - - let trigger_offset = m.triggers.iter().position(|x| x == "Test").unwrap(); - - let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]); - - verify_render(rendered, "Result"); - } - - #[test] - fn test_render_match_case_propagation_all_capital() { - let config = get_config_for( - r###" - matches: - - trigger: 'test' - replace: result - propagate_case: true - "###, - ); - - let renderer = get_renderer(config.clone()); - - let m = config.matches[0].clone(); - - let trigger_offset = m.triggers.iter().position(|x| x == "TEST").unwrap(); - - let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]); - - verify_render(rendered, "RESULT"); - } - - #[test] - fn test_render_variable_order() { - let config = get_config_for( - r###" - matches: - - trigger: 'test' - replace: "{{output}}" - vars: - - name: first - type: dummy - params: - echo: "hello" - - name: output - type: vardummy - params: - target: "first" - "###, - ); - - let renderer = get_renderer(config.clone()); - let m = config.matches[0].clone(); - let rendered = renderer.render_match(&m, 0, &config, vec![]); - verify_render(rendered, "hello"); - } - - #[test] - fn test_render_global_variable_order() { - let config = get_config_for( - r###" - global_vars: - - name: hello - type: dummy - params: - echo: "hello" - matches: - - trigger: 'test' - replace: "{{hello}} {{output}}" - vars: - - name: first - type: dummy - params: - echo: "world" - - name: output - type: vardummy - params: - target: "first" - - name: hello - type: global - "###, - ); - - let renderer = get_renderer(config.clone()); - let m = config.matches[0].clone(); - let rendered = renderer.render_match(&m, 0, &config, vec![]); - verify_render(rendered, "hello world"); - } - - #[test] - fn test_render_multiple_results() { - let config = get_config_for( - r###" - matches: - - trigger: 'test' - replace: "hello {{var1.name}}" - vars: - - name: var1 - type: multiecho - params: - name: "world" - "###, - ); - - let renderer = get_renderer(config.clone()); - let m = config.matches[0].clone(); - let rendered = renderer.render_match(&m, 0, &config, vec![]); - verify_render(rendered, "hello world"); - } -} diff --git a/src/render/mod.rs b/src/render/mod.rs deleted file mode 100644 index 264fc06..0000000 --- a/src/render/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::Configs; -use crate::matcher::Match; -use std::path::PathBuf; - -pub(crate) mod default; -pub(crate) mod utils; - -pub trait Renderer { - // Render a match output - fn render_match( - &self, - m: &Match, - trigger_offset: usize, - config: &Configs, - args: Vec, - ) -> RenderResult; - - // Render a passive expansion text - fn render_passive(&self, text: &str, config: &Configs) -> RenderResult; -} - -pub enum RenderResult { - Text(String), - Image(PathBuf), - Error, -} diff --git a/src/render/utils.rs b/src/render/utils.rs deleted file mode 100644 index cfee8d7..0000000 --- a/src/render/utils.rs +++ /dev/null @@ -1,133 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use regex::{Captures, Regex}; - -lazy_static! { - static ref ARG_REGEX: Regex = Regex::new("\\$(?P\\d+)\\$").unwrap(); -} - -pub fn render_args(text: &str, args: &Vec) -> String { - let result = ARG_REGEX.replace_all(text, |caps: &Captures| { - let position_str = caps.name("pos").unwrap().as_str(); - let position = position_str.parse::().unwrap_or(-1); - - if position >= 0 && position < args.len() as i32 { - args[position as usize].to_owned() - } else { - "".to_owned() - } - }); - - result.to_string() -} - -pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec { - let mut output = vec![]; - - // Make sure the text is not empty - if text.is_empty() { - return output; - } - - let mut last = String::from(""); - let mut previous: char = char::from(0); - text.chars().into_iter().for_each(|c| { - if c == delimiter { - if previous != escape { - output.push(last.clone()); - last = String::from(""); - } else { - last.push(c); - } - } else if c == escape { - if previous == escape { - last.push(c); - } - } else { - last.push(c); - } - previous = c; - }); - - // Add the last one - output.push(last); - - output -} - -// TESTS - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_render_args_no_args() { - let args = vec!["hello".to_owned()]; - assert_eq!(render_args("no args", &args), "no args") - } - - #[test] - fn test_render_args_one_arg() { - let args = vec!["jon".to_owned()]; - assert_eq!(render_args("hello $0$", &args), "hello jon") - } - - #[test] - fn test_render_args_one_multiple_args() { - let args = vec!["jon".to_owned(), "snow".to_owned()]; - assert_eq!( - render_args("hello $0$, the $1$ is white", &args), - "hello jon, the snow is white" - ) - } - - #[test] - fn test_render_args_out_of_range() { - let args = vec!["jon".to_owned()]; - assert_eq!(render_args("hello $10$", &args), "hello ") - } - - #[test] - fn test_split_args_one_arg() { - assert_eq!(split_args("jon", '/', '\\'), vec!["jon"]) - } - - #[test] - fn test_split_args_two_args() { - assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"]) - } - - #[test] - fn test_split_args_escaping() { - assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"]) - } - - #[test] - fn test_split_args_escaping_escape() { - assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"]) - } - - #[test] - fn test_split_args_empty() { - let empty_vec: Vec = vec![]; - assert_eq!(split_args("", '/', '\\'), empty_vec) - } -} diff --git a/src/res/config.yml b/src/res/config.yml deleted file mode 100644 index 7ad3e30..0000000 --- a/src/res/config.yml +++ /dev/null @@ -1,30 +0,0 @@ -# espanso configuration file - -# This is the default configuration file, change it as you like it -# You can refer to the official documentation: -# https://espanso.org/docs/ - -# Matches are the substitution rules, when you type the "trigger" string -# it gets replaced by the "replace" string. -matches: - # Simple text replacement - - trigger: ":espanso" - replace: "Hi there!" - - # Dates - - trigger: ":date" - replace: "{{mydate}}" - vars: - - name: mydate - type: date - params: - format: "%m/%d/%Y" - - # Shell commands - - trigger: ":shell" - replace: "{{output}}" - vars: - - name: output - type: shell - params: - cmd: "echo Hello from your shell" \ No newline at end of file diff --git a/src/res/linux/icon.png b/src/res/linux/icon.png deleted file mode 100644 index 9bde0d2..0000000 Binary files a/src/res/linux/icon.png and /dev/null differ diff --git a/src/res/linux/systemd.service b/src/res/linux/systemd.service deleted file mode 100644 index 4fdd26e..0000000 --- a/src/res/linux/systemd.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=espanso daemon - -[Service] -ExecStart={{{espanso_path}}} daemon -Restart=on-failure -RestartSec=3 - -[Install] -WantedBy=default.target - diff --git a/src/res/mac/AppIcon.icns b/src/res/mac/AppIcon.icns deleted file mode 100644 index d3f07c3..0000000 Binary files a/src/res/mac/AppIcon.icns and /dev/null differ diff --git a/src/res/mac/EspansoNotifyHelper.zip b/src/res/mac/EspansoNotifyHelper.zip deleted file mode 100644 index 036ba7c..0000000 Binary files a/src/res/mac/EspansoNotifyHelper.zip and /dev/null differ diff --git a/src/res/mac/com.federicoterzi.espanso.plist b/src/res/mac/com.federicoterzi.espanso.plist deleted file mode 100644 index e537367..0000000 --- a/src/res/mac/com.federicoterzi.espanso.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Label - com.federicoterzi.espanso - EnvironmentVariables - - PATH - {{{PATH}}} - - ProgramArguments - - {{{espanso_path}}} - daemon - - RunAtLoad - - StandardErrorPath - /tmp/espanso.err - StandardOutPath - /tmp/espanso.out - - \ No newline at end of file diff --git a/src/res/mac/icon.png b/src/res/mac/icon.png deleted file mode 100644 index cde3e91..0000000 Binary files a/src/res/mac/icon.png and /dev/null differ diff --git a/src/res/mac/icondisabled.png b/src/res/mac/icondisabled.png deleted file mode 100644 index a289156..0000000 Binary files a/src/res/mac/icondisabled.png and /dev/null differ diff --git a/src/res/mac/modulo.plist b/src/res/mac/modulo.plist deleted file mode 100644 index 3bf440a..0000000 --- a/src/res/mac/modulo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - {{{modulo_path}}} - CFBundleIconFile - AppIcon - CFBundleIconName - AppIcon - CFBundleIdentifier - com.federicoterzi.modulo - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Modulo - CFBundlePackageType - APPL - CFBundleSignature - ???? - CFBundleVersion - 1.0 - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/src/res/test/config_with_bad_yaml.yml b/src/res/test/config_with_bad_yaml.yml deleted file mode 100644 index b5d6822..0000000 --- a/src/res/test/config_with_bad_yaml.yml +++ /dev/null @@ -1,12 +0,0 @@ -backend: Clipboard - -definitely a bad yaml - -matches: - # Default - - trigger: ":espanso" - replace: "Hi there!" - - # Emojis - - trigger: ":lol" - replace: "😂" \ No newline at end of file diff --git a/src/res/test/get_package_index.json b/src/res/test/get_package_index.json deleted file mode 100644 index 428ed4e..0000000 --- a/src/res/test/get_package_index.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lastUpdate": 1565437389, - "packages": [ - - { - "name": "basic-emojis", - "title": "Basic Emojis", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "A package to include some basic emojis in espanso.", - "author": "Federico Terzi", - "is_core": true - - }, - - - { - "name": "italian-accents", - "title": "Italian Accents", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Include Italian accents substitutions to espanso.", - "author": "Federico Terzi", - "is_core": true - - } - - -]} \ No newline at end of file diff --git a/src/res/test/index_without_update.json b/src/res/test/index_without_update.json deleted file mode 100644 index 7d83c46..0000000 --- a/src/res/test/index_without_update.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lastUpdate": XXXX, - "packages": [ - - { - "name": "basic-emojis", - "title": "Basic Emojis", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "A package to include some basic emojis in espanso.", - "author": "Federico Terzi", - "is_core": true - - }, - - - { - "name": "italian-accents", - "title": "Italian Accents", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Include Italian accents substitutions to espanso.", - "author": "Federico Terzi", - "is_core": true - - } - - -]} \ No newline at end of file diff --git a/src/res/test/install_package_index.json b/src/res/test/install_package_index.json deleted file mode 100644 index 63222f7..0000000 --- a/src/res/test/install_package_index.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "lastUpdate": 1565437389, - "packages": [ - - { - "name": "dummy-package", - "title": "Dummy Package", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Dummy package", - "author": "Federico Terzi", - "is_core": true - }, - - { - "name": "dummy-package2", - "title": "Dummy Package", - "version": "9.9.9", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Dummy package", - "author": "Federico Terzi", - "is_core": true - - }, - - { - "name": "dummy-package3", - "title": "Dummy Package", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Dummy package", - "author": "Federico Terzi", - "is_core": true - - }, - - { - "name": "dummy-package4", - "title": "Dummy Package", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Dummy package", - "author": "Federico Terzi", - "is_core": true - - }, - - - { - "name": "italian-accents", - "title": "Italian Accents", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Include Italian accents substitutions to espanso.", - "author": "Federico Terzi", - "is_core": true - - }, - - { - "name": "not-existing", - "title": "Not Existing", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Package that does not exist in the repo", - "author": "Federico Terzi", - "is_core": true - - } - ]} \ No newline at end of file diff --git a/src/res/test/outdated_index.json b/src/res/test/outdated_index.json deleted file mode 100644 index 428ed4e..0000000 --- a/src/res/test/outdated_index.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lastUpdate": 1565437389, - "packages": [ - - { - "name": "basic-emojis", - "title": "Basic Emojis", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "A package to include some basic emojis in espanso.", - "author": "Federico Terzi", - "is_core": true - - }, - - - { - "name": "italian-accents", - "title": "Italian Accents", - "version": "0.1.0", - "repo": "https://github.com/federico-terzi/espanso-hub-core", - "desc": "Include Italian accents substitutions to espanso.", - "author": "Federico Terzi", - "is_core": true - - } - - -]} \ No newline at end of file diff --git a/src/res/test/working_config.yml b/src/res/test/working_config.yml deleted file mode 100644 index bcf8bf6..0000000 --- a/src/res/test/working_config.yml +++ /dev/null @@ -1,10 +0,0 @@ -backend: Clipboard - -matches: - # Default - - trigger: ":espanso" - replace: "Hi there!" - - # Emojis - - trigger: ":lol" - replace: "😂" \ No newline at end of file diff --git a/src/res/win/espanso.bmp b/src/res/win/espanso.bmp deleted file mode 100644 index 882c849..0000000 Binary files a/src/res/win/espanso.bmp and /dev/null differ diff --git a/src/res/win/espanso.ico b/src/res/win/espanso.ico deleted file mode 100644 index 1d1c06c..0000000 Binary files a/src/res/win/espanso.ico and /dev/null differ diff --git a/src/res/win/espansored.ico b/src/res/win/espansored.ico deleted file mode 100644 index 334bab0..0000000 Binary files a/src/res/win/espansored.ico and /dev/null differ diff --git a/src/sysdaemon.rs b/src/sysdaemon.rs deleted file mode 100644 index de54983..0000000 --- a/src/sysdaemon.rs +++ /dev/null @@ -1,303 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -// This functions are used to register/unregister espanso from the system daemon manager. - -use crate::config::ConfigSet; -use crate::sysdaemon::VerifyResult::{EnabledAndValid, EnabledButInvalidPath, NotEnabled}; - -// INSTALLATION - -#[cfg(target_os = "macos")] -const MAC_PLIST_CONTENT: &str = include_str!("res/mac/com.federicoterzi.espanso.plist"); -#[cfg(target_os = "macos")] -const MAC_PLIST_FILENAME: &str = "com.federicoterzi.espanso.plist"; - -#[cfg(target_os = "macos")] -pub fn register(_config_set: ConfigSet) { - use std::fs::create_dir_all; - use std::process::{Command, ExitStatus}; - - let home_dir = dirs::home_dir().expect("Could not get user home directory"); - let library_dir = home_dir.join("Library"); - let agents_dir = library_dir.join("LaunchAgents"); - - // Make sure agents directory exists - if !agents_dir.exists() { - create_dir_all(agents_dir.clone()).expect("Could not create LaunchAgents directory"); - } - - let plist_file = agents_dir.join(MAC_PLIST_FILENAME); - if !plist_file.exists() { - println!( - "Creating LaunchAgents entry: {}", - plist_file.to_str().unwrap_or_default() - ); - - let espanso_path = std::env::current_exe().expect("Could not get espanso executable path"); - println!( - "Entry will point to: {}", - espanso_path.to_str().unwrap_or_default() - ); - - let plist_content = String::from(MAC_PLIST_CONTENT).replace( - "{{{espanso_path}}}", - espanso_path.to_str().unwrap_or_default(), - ); - - // Copy the user PATH variable and inject it in the Plist file so that - // it gets loaded by Launchd. - // To see why this is necessary: https://github.com/federico-terzi/espanso/issues/233 - let user_path = std::env::var("PATH").unwrap_or("".to_owned()); - let plist_content = plist_content.replace("{{{PATH}}}", &user_path); - - std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file"); - - println!("Entry created correctly!") - } - - println!("Reloading entry..."); - - let res = Command::new("launchctl") - .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) - .output(); - - let res = Command::new("launchctl") - .args(&["load", "-w", plist_file.to_str().unwrap_or_default()]) - .status(); - - if let Ok(status) = res { - if status.success() { - println!("Entry loaded correctly!") - } - } else { - println!("Error loading new entry"); - } -} - -#[cfg(target_os = "macos")] -pub fn unregister(_config_set: ConfigSet) { - use std::fs::create_dir_all; - use std::process::{Command, ExitStatus}; - - let home_dir = dirs::home_dir().expect("Could not get user home directory"); - let library_dir = home_dir.join("Library"); - let agents_dir = library_dir.join("LaunchAgents"); - - let plist_file = agents_dir.join(MAC_PLIST_FILENAME); - if plist_file.exists() { - let _res = Command::new("launchctl") - .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) - .output(); - - std::fs::remove_file(&plist_file).expect("Could not remove espanso entry"); - - println!("Entry removed correctly!") - } else { - println!("espanso is not installed"); - } -} - -// LINUX - -#[cfg(target_os = "linux")] -const LINUX_SERVICE_CONTENT: &str = include_str!("res/linux/systemd.service"); -#[cfg(target_os = "linux")] -const LINUX_SERVICE_FILENAME: &str = "espanso.service"; - -#[cfg(target_os = "linux")] -pub fn register(_: ConfigSet) { - use std::fs::create_dir_all; - use std::process::Command; - - // Check if espanso service is already registered - let res = Command::new("systemctl") - .args(&["--user", "is-enabled", "espanso"]) - .output(); - if let Ok(res) = res { - let output = String::from_utf8_lossy(res.stdout.as_slice()); - let output = output.trim(); - if res.status.success() { - if output == "enabled" { - eprintln!("espanso service is already registered to systemd"); - eprintln!("If you want to register it again, please uninstall it first with:"); - eprintln!(" espanso unregister"); - std::process::exit(5); - } - } else { - if output == "disabled" { - use dialoguer::Confirmation; - if !Confirmation::new() - .with_text("espanso is already registered but currently disabled. Do you want to override it?") - .default(false) - .show_default(true) - .interact().expect("Unable to read user answer") { - - std::process::exit(6); - } - } - } - } - - // User level systemd services should be placed in this directory: - // $XDG_CONFIG_HOME/systemd/user/, usually: ~/.config/systemd/user/ - let config_dir = dirs::config_dir().expect("Could not get configuration directory"); - let systemd_dir = config_dir.join("systemd"); - let user_dir = systemd_dir.join("user"); - - // Make sure the directory exists - if !user_dir.exists() { - create_dir_all(user_dir.clone()).expect("Could not create systemd user directory"); - } - - let service_file = user_dir.join(LINUX_SERVICE_FILENAME); - if !service_file.exists() { - println!( - "Creating service entry: {}", - service_file.to_str().unwrap_or_default() - ); - - let espanso_path = std::env::current_exe().expect("Could not get espanso executable path"); - println!( - "Entry will point to: {}", - espanso_path.to_str().unwrap_or_default() - ); - - let service_content = String::from(LINUX_SERVICE_CONTENT).replace( - "{{{espanso_path}}}", - espanso_path.to_str().unwrap_or_default(), - ); - - std::fs::write(service_file.clone(), service_content) - .expect("Unable to write service file"); - - println!("Service file created correctly!") - } - - println!("Enabling espanso for systemd..."); - - let res = Command::new("systemctl") - .args(&["--user", "enable", "espanso"]) - .status(); - - if let Ok(status) = res { - if status.success() { - println!("Service registered correctly!") - } - } else { - println!("Error loading espanso service"); - } -} - -pub enum VerifyResult { - EnabledAndValid, - EnabledButInvalidPath, - NotEnabled, -} - -#[cfg(target_os = "linux")] -pub fn verify() -> VerifyResult { - use regex::Regex; - use std::process::Command; - - // Check if espanso service is already registered - let res = Command::new("systemctl") - .args(&["--user", "is-enabled", "espanso"]) - .output(); - if let Ok(res) = res { - let output = String::from_utf8_lossy(res.stdout.as_slice()); - let output = output.trim(); - if !res.status.success() || output != "enabled" { - return NotEnabled; - } - } - - lazy_static! { - static ref EXEC_PATH_REGEX: Regex = Regex::new("ExecStart=(?P.*?)\\s").unwrap(); - } - - // Check if the currently registered path is valid - let res = Command::new("systemctl") - .args(&["--user", "cat", "espanso"]) - .output(); - if let Ok(res) = res { - let output = String::from_utf8_lossy(res.stdout.as_slice()); - let output = output.trim(); - if res.status.success() { - let caps = EXEC_PATH_REGEX.captures(output).unwrap(); - let path = caps.get(1).map_or("", |m| m.as_str()); - let espanso_path = - std::env::current_exe().expect("Could not get espanso executable path"); - - if espanso_path.to_string_lossy() != path { - return EnabledButInvalidPath; - } - } - } - - EnabledAndValid -} - -#[cfg(target_os = "linux")] -pub fn unregister(_: ConfigSet) { - use std::process::Command; - - // Disable the service first - Command::new("systemctl") - .args(&["--user", "disable", "espanso"]) - .status() - .expect("Unable to invoke systemctl"); - - // Then delete the espanso.service entry - let config_dir = dirs::config_dir().expect("Could not get configuration directory"); - let systemd_dir = config_dir.join("systemd"); - let user_dir = systemd_dir.join("user"); - let service_file = user_dir.join(LINUX_SERVICE_FILENAME); - - if service_file.exists() { - let res = std::fs::remove_file(&service_file); - match res { - Ok(_) => { - println!("Deleted entry at {}", service_file.to_string_lossy()); - println!("Service unregistered successfully!"); - } - Err(e) => { - println!( - "Error, could not delete service entry at {} with error {}", - service_file.to_string_lossy(), - e - ); - } - } - } else { - eprintln!("Error, could not find espanso service file"); - } -} - -// WINDOWS - -#[cfg(target_os = "windows")] -pub fn register(_config_set: ConfigSet) { - println!("Windows does not support automatic system daemon integration.") -} - -#[cfg(target_os = "windows")] -pub fn unregister(_config_set: ConfigSet) { - println!("Windows does not support automatic system daemon integration.") -} diff --git a/src/system/linux.rs b/src/system/linux.rs deleted file mode 100644 index 0f8b144..0000000 --- a/src/system/linux.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::os::raw::c_char; - -use crate::bridge::linux::{ - get_active_window_class, get_active_window_executable, get_active_window_name, -}; -use std::ffi::CStr; - -pub struct LinuxSystemManager {} - -impl super::SystemManager for LinuxSystemManager { - fn get_current_window_title(&self) -> Option { - unsafe { - let mut buffer: [c_char; 256] = [0; 256]; - let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - - let string = c_string.to_str(); - if let Ok(string) = string { - return Some((*string).to_owned()); - } - } - } - - None - } - - fn get_current_window_class(&self) -> Option { - unsafe { - let mut buffer: [c_char; 256] = [0; 256]; - let res = get_active_window_class(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - - let string = c_string.to_str(); - if let Ok(string) = string { - return Some((*string).to_owned()); - } - } - } - - None - } - - fn get_current_window_executable(&self) -> Option { - unsafe { - let mut buffer: [c_char; 256] = [0; 256]; - let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - - let string = c_string.to_str(); - if let Ok(string) = string { - return Some((*string).to_owned()); - } - } - } - - None - } -} - -impl LinuxSystemManager { - pub fn new() -> LinuxSystemManager { - LinuxSystemManager {} - } -} diff --git a/src/system/macos.rs b/src/system/macos.rs deleted file mode 100644 index 4480c3d..0000000 --- a/src/system/macos.rs +++ /dev/null @@ -1,165 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::os::raw::c_char; - -use crate::bridge::macos::{ - get_active_app_bundle, get_active_app_identifier, get_path_from_pid, get_secure_input_process, -}; -use std::ffi::CStr; - -pub struct MacSystemManager {} - -impl super::SystemManager for MacSystemManager { - fn get_current_window_title(&self) -> Option { - self.get_current_window_class() - } - - fn get_current_window_class(&self) -> Option { - unsafe { - let mut buffer: [c_char; 256] = [0; 256]; - let res = get_active_app_identifier(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - - let string = c_string.to_str(); - if let Ok(string) = string { - return Some((*string).to_owned()); - } - } - } - - None - } - - fn get_current_window_executable(&self) -> Option { - unsafe { - let mut buffer: [c_char; 256] = [0; 256]; - let res = get_active_app_bundle(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - - let string = c_string.to_str(); - if let Ok(string) = string { - return Some((*string).to_owned()); - } - } - } - - None - } -} - -impl MacSystemManager { - pub fn new() -> MacSystemManager { - MacSystemManager {} - } - - /// Check whether an application is currently holding the Secure Input. - /// Return None if no application has claimed SecureInput, its PID otherwise. - pub fn get_secure_input_pid() -> Option { - unsafe { - let mut pid: i64 = -1; - let res = get_secure_input_process(&mut pid as *mut i64); - - if res > 0 { - Some(pid) - } else { - None - } - } - } - - /// Check whether an application is currently holding the Secure Input. - /// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise. - pub fn get_secure_input_application() -> Option<(String, String)> { - unsafe { - let pid = MacSystemManager::get_secure_input_pid(); - - if let Some(pid) = pid { - // Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant. - // the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently. - let mut buffer: [c_char; 4096] = [0; 4096]; - let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32); - - if res > 0 { - let c_string = CStr::from_ptr(buffer.as_ptr()); - let string = c_string.to_str(); - if let Ok(path) = string { - if !path.trim().is_empty() { - let process = path.trim().to_string(); - let app_name = - if let Some(name) = Self::get_app_name_from_path(&process) { - name - } else { - process.to_owned() - }; - - return Some((app_name, process)); - } - } - } - } - - None - } - } - - fn get_app_name_from_path(path: &str) -> Option { - use regex::Regex; - - lazy_static! { - static ref APP_REGEX: Regex = Regex::new("/([^/]+).(app|bundle)/").unwrap(); - }; - - let caps = APP_REGEX.captures(&path); - if let Some(caps) = caps { - Some(caps.get(1).map_or("", |m| m.as_str()).to_owned()) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_app_name_from_path() { - let app_name = MacSystemManager::get_app_name_from_path( - "/Applications/iTerm.app/Contents/MacOS/iTerm2", - ); - assert_eq!(app_name.unwrap(), "iTerm") - } - - #[test] - fn test_get_app_name_from_path_no_app_name() { - let app_name = MacSystemManager::get_app_name_from_path("/another/directory"); - assert!(app_name.is_none()) - } - - #[test] - fn test_get_app_name_from_path_security_bundle() { - let app_name = MacSystemManager::get_app_name_from_path("/System/Library/Frameworks/Security.framework/Versions/A/MachServices/SecurityAgent.bundle/Contents/MacOS/SecurityAgent"); - assert_eq!(app_name.unwrap(), "SecurityAgent") - } -} diff --git a/src/system/mod.rs b/src/system/mod.rs deleted file mode 100644 index 479ef86..0000000 --- a/src/system/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#[cfg(target_os = "windows")] -mod windows; - -#[cfg(target_os = "linux")] -mod linux; - -#[cfg(target_os = "macos")] -pub mod macos; - -pub trait SystemManager { - fn get_current_window_title(&self) -> Option; - fn get_current_window_class(&self) -> Option; - fn get_current_window_executable(&self) -> Option; -} - -// LINUX IMPLEMENTATION -#[cfg(target_os = "linux")] -pub fn get_manager() -> impl SystemManager { - linux::LinuxSystemManager::new() -} - -// WINDOWS IMPLEMENTATION -#[cfg(target_os = "windows")] -pub fn get_manager() -> impl SystemManager { - windows::WindowsSystemManager::new() -} - -// MAC IMPLEMENTATION -#[cfg(target_os = "macos")] -pub fn get_manager() -> impl SystemManager { - macos::MacSystemManager::new() -} diff --git a/src/system/windows.rs b/src/system/windows.rs deleted file mode 100644 index 4d7eb27..0000000 --- a/src/system/windows.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::windows::*; -use widestring::U16CString; - -pub struct WindowsSystemManager {} - -impl WindowsSystemManager { - pub fn new() -> WindowsSystemManager { - WindowsSystemManager {} - } -} - -impl super::SystemManager for WindowsSystemManager { - fn get_current_window_title(&self) -> Option { - unsafe { - let mut buffer: [u16; 256] = [0; 256]; - let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = U16CString::from_ptr_str(buffer.as_ptr()); - - let string = c_string.to_string_lossy(); - return Some((*string).to_owned()); - } - } - - None - } - - fn get_current_window_class(&self) -> Option { - self.get_current_window_executable() - } - - fn get_current_window_executable(&self) -> Option { - unsafe { - let mut buffer: [u16; 256] = [0; 256]; - let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32); - - if res > 0 { - let c_string = U16CString::from_ptr_str(buffer.as_ptr()); - - let string = c_string.to_string_lossy(); - return Some((*string).to_owned()); - } - } - - None - } -} diff --git a/src/ui/linux.rs b/src/ui/linux.rs deleted file mode 100644 index d62a46a..0000000 --- a/src/ui/linux.rs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use super::MenuItem; -use log::{error, info}; -use std::path::PathBuf; -use std::process::Command; - -const LINUX_ICON_CONTENT: &[u8] = include_bytes!("../res/linux/icon.png"); - -pub struct LinuxUIManager { - icon_path: PathBuf, -} - -impl super::UIManager for LinuxUIManager { - fn notify(&self, message: &str) { - self.notify_delay(message, 2000); - } - - fn notify_delay(&self, message: &str, duration: i32) { - let res = Command::new("notify-send") - .args(&[ - "-i", - self.icon_path.to_str().unwrap_or_default(), - "-t", - &duration.to_string(), - "espanso", - message, - ]) - .output(); - - if let Err(e) = res { - error!("Could not send a notification, error: {}", e); - } - } - - fn show_menu(&self, _menu: Vec) { - // Not implemented on linux - } - - fn cleanup(&self) { - // Nothing to do here - } -} - -impl LinuxUIManager { - pub fn new() -> LinuxUIManager { - // Initialize the icon if not present - let data_dir = crate::context::get_data_dir(); - let icon_path = data_dir.join("icon.png"); - if !icon_path.exists() { - info!( - "Creating espanso icon in '{}'", - icon_path.to_str().unwrap_or_default() - ); - std::fs::write(&icon_path, LINUX_ICON_CONTENT).expect("Unable to copy espanso icon"); - } - - LinuxUIManager { icon_path } - } -} diff --git a/src/ui/macos.rs b/src/ui/macos.rs deleted file mode 100644 index 8784f70..0000000 --- a/src/ui/macos.rs +++ /dev/null @@ -1,161 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::macos::{show_context_menu, MacMenuItem}; -use crate::context; -use crate::ui::{MenuItem, MenuItemType}; -use log::{debug, info, warn}; -use std::ffi::CString; -use std::io::Cursor; -use std::os::raw::c_char; -use std::path::PathBuf; -use std::process::Command; -use std::{fs, io}; - -const NOTIFY_HELPER_BINARY: &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip"); - -pub struct MacUIManager { - notify_helper_path: PathBuf, -} - -impl super::UIManager for MacUIManager { - fn notify(&self, message: &str) { - self.notify_delay(message, 1500); - } - - fn notify_delay(&self, message: &str, duration: i32) { - let executable_path = self.notify_helper_path.join("Contents"); - let executable_path = executable_path.join("MacOS"); - let executable_path = executable_path.join("EspansoNotifyHelper"); - - let duration_float = duration as f64 / 1000.0; - - let res = Command::new(executable_path) - .args(&["espanso", message, &duration_float.to_string()]) - .spawn(); - - if let Err(e) = res { - warn!("Error while dispatching Notify Helper {}", e) - } - } - - fn show_menu(&self, menu: Vec) { - let mut raw_menu = Vec::new(); - - for item in menu.iter() { - let text = CString::new(item.item_name.clone()).unwrap_or_default(); - let mut str_buff: [c_char; 100] = [0; 100]; - unsafe { - std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), item.item_name.len()); - } - - let menu_type = match item.item_type { - MenuItemType::Button => 1, - MenuItemType::Separator => 2, - }; - - let raw_item = MacMenuItem { - item_id: item.item_id, - item_type: menu_type, - item_name: str_buff, - }; - - raw_menu.push(raw_item); - } - - unsafe { - show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32); - } - } - - fn cleanup(&self) { - // Nothing to do here - } -} - -impl MacUIManager { - pub fn new() -> MacUIManager { - let notify_helper_path = MacUIManager::initialize_notify_helper(); - - MacUIManager { notify_helper_path } - } - - fn initialize_notify_helper() -> PathBuf { - let espanso_dir = context::get_data_dir(); - - info!( - "Initializing EspansoNotifyHelper in {}", - espanso_dir.as_path().display() - ); - - let espanso_target = espanso_dir.join("EspansoNotifyHelper.app"); - - if espanso_target.exists() { - info!("EspansoNotifyHelper already initialized, skipping."); - } else { - // Extract zip file - let reader = Cursor::new(NOTIFY_HELPER_BINARY); - - let mut archive = zip::ZipArchive::new(reader).unwrap(); - - for i in 0..archive.len() { - let mut file = archive.by_index(i).unwrap(); - let outpath = espanso_dir.join(file.sanitized_name()); - - { - let comment = file.comment(); - if !comment.is_empty() { - debug!("File {} comment: {}", i, comment); - } - } - - if (&*file.name()).ends_with('/') { - debug!( - "File {} extracted to \"{}\"", - i, - outpath.as_path().display() - ); - fs::create_dir_all(&outpath).unwrap(); - } else { - debug!( - "File {} extracted to \"{}\" ({} bytes)", - i, - outpath.as_path().display(), - file.size() - ); - if let Some(p) = outpath.parent() { - if !p.exists() { - fs::create_dir_all(&p).unwrap(); - } - } - let mut outfile = fs::File::create(&outpath).unwrap(); - io::copy(&mut file, &mut outfile).unwrap(); - } - - use std::os::unix::fs::PermissionsExt; - - if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); - } - } - } - - espanso_target - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index f25491c..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -#[cfg(target_os = "windows")] -mod windows; - -#[cfg(target_os = "linux")] -mod linux; - -#[cfg(target_os = "macos")] -mod macos; - -pub mod modulo; - -pub trait UIManager { - fn notify(&self, message: &str); - fn notify_delay(&self, message: &str, duration: i32); - fn show_menu(&self, menu: Vec); - fn cleanup(&self); -} - -pub enum MenuItemType { - Button, - Separator, -} - -pub struct MenuItem { - pub item_id: i32, - pub item_type: MenuItemType, - pub item_name: String, -} - -// MAC IMPLEMENTATION -#[cfg(target_os = "macos")] -pub fn get_uimanager() -> impl UIManager { - macos::MacUIManager::new() -} - -// LINUX IMPLEMENTATION -#[cfg(target_os = "linux")] -pub fn get_uimanager() -> impl UIManager { - linux::LinuxUIManager::new() -} - -// WINDOWS IMPLEMENTATION -#[cfg(target_os = "windows")] -pub fn get_uimanager() -> impl UIManager { - windows::WindowsUIManager::new() -} diff --git a/src/ui/modulo/mac.rs b/src/ui/modulo/mac.rs deleted file mode 100644 index 394ec88..0000000 --- a/src/ui/modulo/mac.rs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use log::info; -use std::os::unix::fs::symlink; -use std::path::PathBuf; - -const MODULO_APP_BUNDLE_NAME: &str = "Modulo.app"; -const MODULO_APP_BUNDLE_PLIST_CONTENT: &'static str = include_str!("../../res/mac/modulo.plist"); -const MODULO_APP_BUNDLE_ICON: &[u8] = include_bytes!("../../res/mac/AppIcon.icns"); - -pub fn generate_modulo_app_bundle(modulo_path: &str) -> Result { - let modulo_pathbuf = PathBuf::from(modulo_path); - let modulo_path: String = if !modulo_pathbuf.exists() { - // If modulo was taken from the PATH, we need to calculate the absolute path - // To do so, we use the `which` command - let output = std::process::Command::new("which") - .arg("modulo") - .output() - .expect("unable to call 'which' command to determine modulo's full path"); - let path = String::from_utf8_lossy(output.stdout.as_slice()); - let path = path.trim(); - - info!("Detected modulo's full path: {:?}", &path); - path.to_string() - } else { - modulo_path.to_owned() - }; - - let data_dir = crate::context::get_data_dir(); - - let modulo_app_dir = data_dir.join(MODULO_APP_BUNDLE_NAME); - - // Remove previous bundle if present - if modulo_app_dir.exists() { - std::fs::remove_dir_all(&modulo_app_dir)?; - } - - // Recreate the App bundle stub - std::fs::create_dir(&modulo_app_dir)?; - - let contents_dir = modulo_app_dir.join("Contents"); - std::fs::create_dir(&contents_dir)?; - - let macos_dir = contents_dir.join("MacOS"); - std::fs::create_dir(&macos_dir)?; - - let resources_dir = contents_dir.join("Resources"); - std::fs::create_dir(&resources_dir)?; - - // Generate the Plist file - let plist_content = MODULO_APP_BUNDLE_PLIST_CONTENT.replace("{{{modulo_path}}}", &modulo_path); - let plist_file = contents_dir.join("Info.plist"); - std::fs::write(plist_file, plist_content)?; - - // Copy the icon file - let icon_file = resources_dir.join("AppIcon.icns"); - std::fs::write(icon_file, MODULO_APP_BUNDLE_ICON)?; - - // Generate the symbolic link to the modulo binary - let target_link = macos_dir.join("modulo"); - symlink(modulo_path, &target_link)?; - - info!("Created Modulo APP stub at: {:?}", &target_link); - - Ok(target_link) -} diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs deleted file mode 100644 index 7b1544f..0000000 --- a/src/ui/modulo/mod.rs +++ /dev/null @@ -1,160 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2020 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::config::Configs; -use log::{error, info}; -use std::io::{Error, Write}; -use std::process::{Child, Command, Output}; - -#[cfg(target_os = "macos")] -mod mac; - -pub struct ModuloManager { - modulo_path: Option, -} - -impl ModuloManager { - pub fn new(config: &Configs) -> Self { - let mut modulo_path: Option = None; - // Check if the `MODULO_PATH` env variable is configured - if let Some(_modulo_path) = std::env::var_os("MODULO_PATH") { - modulo_path = Some(_modulo_path.to_string_lossy().to_string()) - } else if let Some(ref _modulo_path) = config.modulo_path { - // Check the configs - modulo_path = Some(_modulo_path.to_owned()); - } else { - // Check in the same directory of espanso - if let Ok(exe_path) = std::env::current_exe() { - if let Some(parent) = exe_path.parent() { - let possible_path = parent.join("modulo"); - let possible_path = possible_path.to_string_lossy().to_string(); - - if let Ok(output) = Command::new(&possible_path).arg("--version").output() { - if output.status.success() { - modulo_path = Some(possible_path); - } - } - } - } - - // Otherwise check if present in the PATH - if modulo_path.is_none() { - if let Ok(output) = Command::new("modulo").arg("--version").output() { - if output.status.success() { - modulo_path = Some("modulo".to_owned()); - } - } - } - } - - if let Some(ref path) = modulo_path { - info!("Using modulo at {:?}", path); - - // MacOS specific remark - // In order to give modulo the focus when spawning a form, modulo has to be - // wrapped inside an application bundle. Therefore, we generate a bundle - // at startup. - // See issue: https://github.com/federico-terzi/espanso/issues/430 - #[cfg(target_os = "macos")] - { - modulo_path = Some( - mac::generate_modulo_app_bundle(path) - .expect("unable to generate modulo app stub") - .to_string_lossy() - .to_string(), - ); - } - } - - Self { modulo_path } - } - - pub fn is_valid(&self) -> bool { - self.modulo_path.is_some() - } - - pub fn get_version(&self) -> Option { - if let Some(ref modulo_path) = self.modulo_path { - if let Ok(output) = Command::new(modulo_path).arg("--version").output() { - let version = String::from_utf8_lossy(&output.stdout); - return Some(version.to_string()); - } - } - - None - } - - pub fn invoke(&self, args: &[&str], body: &str) -> Option { - if self.modulo_path.is_none() { - error!("Attempt to invoke modulo even though it's not configured"); - return None; - } - - if let Some(ref modulo_path) = self.modulo_path { - let mut command = Command::new(modulo_path); - command - .args(args) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); - - crate::utils::set_command_flags(&mut command); - - let child = command.spawn(); - - match child { - Ok(mut child) => { - if let Some(stdin) = child.stdin.as_mut() { - match stdin.write_all(body.as_bytes()) { - Ok(_) => { - // Get the output - match child.wait_with_output() { - Ok(child_output) => { - let output = String::from_utf8_lossy(&child_output.stdout); - - // Check also if the program reports an error - let error = String::from_utf8_lossy(&child_output.stderr); - if !error.is_empty() { - error!("modulo reported an error: {}", error); - } - - return Some(output.to_string()); - } - Err(error) => { - error!("error while getting output from modulo: {}", error); - } - } - } - Err(error) => { - error!("error while sending body to modulo: {}", error); - } - } - } else { - error!("unable to open stdin to modulo"); - } - } - Err(error) => { - error!("error reported when invoking modulo: {}", error); - } - } - } - - None - } -} diff --git a/src/ui/windows.rs b/src/ui/windows.rs deleted file mode 100644 index e0bbc99..0000000 --- a/src/ui/windows.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use crate::bridge::windows::{ - cleanup_ui, close_notification, show_context_menu, show_notification, WindowsMenuItem, -}; -use crate::ui::{MenuItem, MenuItemType}; -use log::debug; -use std::sync::Arc; -use std::sync::Mutex; -use std::{thread, time}; -use widestring::U16CString; - -pub struct WindowsUIManager { - id: Arc>, -} - -impl super::UIManager for WindowsUIManager { - fn notify(&self, message: &str) { - self.notify_delay(message, 2000); - } - - fn notify_delay(&self, message: &str, duration: i32) { - let current_id: i32 = { - let mut id = self.id.lock().unwrap(); - *id += 1; - *id - }; - - let step = duration / 10; - - // Setup a timeout to close the notification - let id = Arc::clone(&self.id); - let _ = thread::Builder::new() - .name("notification_thread".to_string()) - .spawn(move || { - for _ in 1..10 { - let duration = time::Duration::from_millis(step as u64); - thread::sleep(duration); - - let new_id = id.lock().unwrap(); - if *new_id != current_id { - debug!("Cancelling notification close event with id {}", current_id); - return; - } - } - - unsafe { - close_notification(); - } - }); - - // Create and show a window notification - unsafe { - let message = U16CString::from_str(message).unwrap(); - show_notification(message.as_ptr()); - } - } - - fn show_menu(&self, menu: Vec) { - let mut raw_menu = Vec::new(); - - for item in menu.iter() { - let text = U16CString::from_str(item.item_name.clone()).unwrap_or_default(); - let mut str_buff: [u16; 100] = [0; 100]; - unsafe { - std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), text.len()); - } - - let menu_type = match item.item_type { - MenuItemType::Button => 1, - MenuItemType::Separator => 2, - }; - - let raw_item = WindowsMenuItem { - item_id: item.item_id, - item_type: menu_type, - item_name: str_buff, - }; - - raw_menu.push(raw_item); - } - - unsafe { - show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32); - } - } - - fn cleanup(&self) { - unsafe { - cleanup_ui(); - } - } -} - -impl WindowsUIManager { - pub fn new() -> WindowsUIManager { - let id = Arc::new(Mutex::new(0)); - - let manager = WindowsUIManager { id }; - - manager - } -} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index bea9fe2..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This file is part of espanso. - * - * Copyright (C) 2019 Federico Terzi - * - * espanso is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - -use std::error::Error; -use std::fs::create_dir; -use std::path::Path; -use std::process::Command; - -pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box> { - for entry in std::fs::read_dir(source_dir)? { - let entry = entry?; - let entry = entry.path(); - if entry.is_dir() { - let name = entry.file_name().expect("Error obtaining the filename"); - let target_dir = dest_dir.join(name); - create_dir(&target_dir)?; - copy_dir(&entry, &target_dir)?; - } else if entry.is_file() { - let target_entry = - dest_dir.join(entry.file_name().expect("Error obtaining the filename")); - std::fs::copy(entry, target_entry)?; - } - } - - Ok(()) -} - -#[cfg(target_os = "windows")] -pub fn set_command_flags(command: &mut Command) { - use std::os::windows::process::CommandExt; - // Avoid showing the shell window - // See: https://github.com/federico-terzi/espanso/issues/249 - command.creation_flags(0x08000000); -} - -#[cfg(not(target_os = "windows"))] -pub fn set_command_flags(command: &mut Command) { - // NOOP on Linux and macOS -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs::create_dir; - use tempfile::TempDir; - - #[test] - fn test_copy_dir_into() { - let source_tmp_dir = TempDir::new().expect("Error creating temp directory"); - let dest_tmp_dir = TempDir::new().expect("Error creating temp directory"); - - let source_dir = source_tmp_dir.path().join("source"); - create_dir(&source_dir).unwrap(); - std::fs::write(source_dir.join("file1.txt"), "file1").unwrap(); - std::fs::write(source_dir.join("file2.txt"), "file2").unwrap(); - - let target_dir = dest_tmp_dir.path().join("source"); - create_dir(&target_dir).unwrap(); - - copy_dir(&source_dir, &target_dir).unwrap(); - - assert!(dest_tmp_dir.path().join("source").exists()); - assert!(dest_tmp_dir.path().join("source/file1.txt").exists()); - assert!(dest_tmp_dir.path().join("source/file2.txt").exists()); - } - - #[test] - fn test_copy_dir_into_recursive() { - let source_tmp_dir = TempDir::new().expect("Error creating temp directory"); - let dest_tmp_dir = TempDir::new().expect("Error creating temp directory"); - - let source_dir = source_tmp_dir.path().join("source"); - create_dir(&source_dir).unwrap(); - std::fs::write(source_dir.join("file1.txt"), "file1").unwrap(); - std::fs::write(source_dir.join("file2.txt"), "file2").unwrap(); - let nested_dir = source_dir.join("nested"); - create_dir(&nested_dir).unwrap(); - std::fs::write(nested_dir.join("nestedfile.txt"), "nestedfile1").unwrap(); - - let target_dir = dest_tmp_dir.path().join("source"); - create_dir(&target_dir).unwrap(); - - copy_dir(&source_dir, &target_dir).unwrap(); - - assert!(dest_tmp_dir.path().join("source").exists()); - assert!(dest_tmp_dir.path().join("source/file1.txt").exists()); - assert!(dest_tmp_dir.path().join("source/file2.txt").exists()); - - assert!(dest_tmp_dir.path().join("source/nested").exists()); - assert!(dest_tmp_dir - .path() - .join("source/nested/nestedfile.txt") - .exists()); - } -}