From b16d1a04aecf7a26efe5b8fd55f422a6f803575b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 Jan 2021 21:55:47 +0100 Subject: [PATCH] Fresh start --- Cargo.lock | 2231 +---------------- Cargo.toml | 59 +- build.rs | 59 - espanso-detect/Cargo.toml | 15 + espanso-detect/build.rs | 55 + espanso-detect/src/event.rs | 117 + .../mod.rs => espanso-detect/src/lib.rs | 18 +- espanso-detect/src/win32/mod.rs | 237 ++ espanso-detect/src/win32/native.cpp | 318 +++ espanso-detect/src/win32/native.h | 69 + espanso/Cargo.toml | 12 + espanso/src/main.rs | 8 + native/liblinuxbridge/CMakeLists.txt | 9 - native/liblinuxbridge/bridge.cpp | 625 ----- native/liblinuxbridge/bridge.h | 162 -- native/liblinuxbridge/fast_xdo.cpp | 245 -- native/liblinuxbridge/fast_xdo.h | 21 - native/libmacbridge/AppDelegate.h | 35 - native/libmacbridge/AppDelegate.mm | 90 - native/libmacbridge/CMakeLists.txt | 9 - native/libmacbridge/bridge.h | 189 -- native/libmacbridge/bridge.mm | 434 ---- native/libwinbridge/CMakeLists.txt | 8 - native/libwinbridge/bridge.cpp | 934 ------- native/libwinbridge/bridge.h | 193 -- .../project.pbxproj | 312 --- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../UserInterfaceState.xcuserstate | Bin 22877 -> 0 bytes .../xcschemes/EspansoNotifyHelper.xcscheme | 91 - .../xcschemes/xcschememanagement.plist | 22 - .../EspansoNotifyHelper/AppDelegate.h | 26 - .../EspansoNotifyHelper/AppDelegate.m | 69 - .../AppIcon.appiconset/Contents.json | 68 - .../AppIcon.appiconset/icongreen-1024.png | Bin 98365 -> 0 bytes .../AppIcon.appiconset/icongreen-128.png | Bin 8027 -> 0 bytes .../AppIcon.appiconset/icongreen-16.png | Bin 1789 -> 0 bytes .../AppIcon.appiconset/icongreen-256.png | Bin 15943 -> 0 bytes .../AppIcon.appiconset/icongreen-32.png | Bin 2728 -> 0 bytes .../AppIcon.appiconset/icongreen-512.png | Bin 20639 -> 0 bytes .../AppIcon.appiconset/icongreen-64.png | Bin 4483 -> 0 bytes .../Assets.xcassets/Contents.json | 6 - .../EspansoNotifyHelper.entitlements | 10 - .../EspansoNotifyHelper/Info.plist | 36 - .../EspansoNotifyHelper/main.m | 30 - rustfmt.toml | 1 + src/bridge/linux.rs | 64 - src/bridge/macos.rs | 74 - src/bridge/windows.rs | 78 - src/check.rs | 71 - src/cli.rs | 87 - src/clipboard/linux.rs | 123 - src/clipboard/macos.rs | 86 - src/clipboard/mod.rs | 54 - src/clipboard/windows.rs | 119 - src/config/mod.rs | 1937 -------------- src/config/runtime.rs | 555 ---- src/context/linux.rs | 170 -- src/context/macos.rs | 275 -- src/context/mod.rs | 173 -- src/context/windows.rs | 252 -- src/edit.rs | 76 - src/engine.rs | 545 ---- src/event/manager.rs | 75 - src/event/mod.rs | 211 -- src/extension/clipboard.rs | 54 - src/extension/date.rs | 66 - src/extension/dummy.rs | 57 - src/extension/form.rs | 123 - src/extension/mod.rs | 77 - src/extension/multiecho.rs | 53 - src/extension/random.rs | 125 - src/extension/script.rs | 272 -- src/extension/shell.rs | 470 ---- src/extension/utils.rs | 49 - src/extension/vardummy.rs | 52 - src/guard.rs | 56 - src/keyboard/linux.rs | 117 - src/keyboard/macos.rs | 89 - src/keyboard/mod.rs | 90 - src/keyboard/windows.rs | 92 - src/main.rs | 1449 ----------- src/matcher/mod.rs | 801 ------ src/matcher/scrolling.rs | 318 --- src/package/default.rs | 794 ------ src/package/mod.rs | 110 - src/package/zip.rs | 129 - src/process.rs | 36 - src/protocol/mod.rs | 224 -- src/protocol/unix.rs | 104 - src/protocol/windows.rs | 98 - src/render/default.rs | 902 ------- src/render/mod.rs | 45 - src/render/utils.rs | 133 - src/res/config.yml | 30 - src/res/linux/icon.png | Bin 11596 -> 0 bytes src/res/linux/systemd.service | 11 - src/res/mac/AppIcon.icns | Bin 40048 -> 0 bytes src/res/mac/EspansoNotifyHelper.zip | Bin 118226 -> 0 bytes src/res/mac/com.federicoterzi.espanso.plist | 24 - src/res/mac/icon.png | Bin 4207 -> 0 bytes src/res/mac/icondisabled.png | Bin 4099 -> 0 bytes src/res/mac/modulo.plist | 30 - src/res/test/config_with_bad_yaml.yml | 12 - src/res/test/get_package_index.json | 29 - src/res/test/index_without_update.json | 29 - src/res/test/install_package_index.json | 70 - src/res/test/outdated_index.json | 29 - src/res/test/working_config.yml | 10 - src/res/win/espanso.bmp | Bin 19256 -> 0 bytes src/res/win/espanso.ico | Bin 28522 -> 0 bytes src/res/win/espansored.ico | Bin 30894 -> 0 bytes src/sysdaemon.rs | 303 --- src/system/linux.rs | 89 - src/system/macos.rs | 165 -- src/system/mod.rs | 51 - src/system/windows.rs | 67 - src/ui/linux.rs | 77 - src/ui/macos.rs | 161 -- src/ui/mod.rs | 65 - src/ui/modulo/mac.rs | 83 - src/ui/modulo/mod.rs | 160 -- src/ui/windows.rs | 120 - src/utils.rs | 111 - 124 files changed, 863 insertions(+), 19711 deletions(-) delete mode 100644 build.rs create mode 100644 espanso-detect/Cargo.toml create mode 100644 espanso-detect/build.rs create mode 100644 espanso-detect/src/event.rs rename src/bridge/mod.rs => espanso-detect/src/lib.rs (81%) create mode 100644 espanso-detect/src/win32/mod.rs create mode 100644 espanso-detect/src/win32/native.cpp create mode 100644 espanso-detect/src/win32/native.h create mode 100644 espanso/Cargo.toml create mode 100644 espanso/src/main.rs delete mode 100644 native/liblinuxbridge/CMakeLists.txt delete mode 100644 native/liblinuxbridge/bridge.cpp delete mode 100644 native/liblinuxbridge/bridge.h delete mode 100644 native/liblinuxbridge/fast_xdo.cpp delete mode 100644 native/liblinuxbridge/fast_xdo.h delete mode 100644 native/libmacbridge/AppDelegate.h delete mode 100644 native/libmacbridge/AppDelegate.mm delete mode 100644 native/libmacbridge/CMakeLists.txt delete mode 100644 native/libmacbridge/bridge.h delete mode 100644 native/libmacbridge/bridge.mm delete mode 100644 native/libwinbridge/CMakeLists.txt delete mode 100644 native/libwinbridge/bridge.cpp delete mode 100644 native/libwinbridge/bridge.h delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.pbxproj delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/project.xcworkspace/xcuserdata/freddy.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcshareddata/xcschemes/EspansoNotifyHelper.xcscheme delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper.xcodeproj/xcuserdata/freddy.xcuserdatad/xcschemes/xcschememanagement.plist delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.h delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/AppDelegate.m delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-1024.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-128.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-16.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-256.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-32.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-512.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/AppIcon.appiconset/icongreen-64.png delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Assets.xcassets/Contents.json delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/EspansoNotifyHelper.entitlements delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/Info.plist delete mode 100644 other/EspansoNotifyHelper/EspansoNotifyHelper/main.m create mode 100644 rustfmt.toml delete mode 100644 src/bridge/linux.rs delete mode 100644 src/bridge/macos.rs delete mode 100644 src/bridge/windows.rs delete mode 100644 src/check.rs delete mode 100644 src/cli.rs delete mode 100644 src/clipboard/linux.rs delete mode 100644 src/clipboard/macos.rs delete mode 100644 src/clipboard/mod.rs delete mode 100644 src/clipboard/windows.rs delete mode 100644 src/config/mod.rs delete mode 100644 src/config/runtime.rs delete mode 100644 src/context/linux.rs delete mode 100644 src/context/macos.rs delete mode 100644 src/context/mod.rs delete mode 100644 src/context/windows.rs delete mode 100644 src/edit.rs delete mode 100644 src/engine.rs delete mode 100644 src/event/manager.rs delete mode 100644 src/event/mod.rs delete mode 100644 src/extension/clipboard.rs delete mode 100644 src/extension/date.rs delete mode 100644 src/extension/dummy.rs delete mode 100644 src/extension/form.rs delete mode 100644 src/extension/mod.rs delete mode 100644 src/extension/multiecho.rs delete mode 100644 src/extension/random.rs delete mode 100644 src/extension/script.rs delete mode 100644 src/extension/shell.rs delete mode 100644 src/extension/utils.rs delete mode 100644 src/extension/vardummy.rs delete mode 100644 src/guard.rs delete mode 100644 src/keyboard/linux.rs delete mode 100644 src/keyboard/macos.rs delete mode 100644 src/keyboard/mod.rs delete mode 100644 src/keyboard/windows.rs delete mode 100644 src/main.rs delete mode 100644 src/matcher/mod.rs delete mode 100644 src/matcher/scrolling.rs delete mode 100644 src/package/default.rs delete mode 100644 src/package/mod.rs delete mode 100644 src/package/zip.rs delete mode 100644 src/process.rs delete mode 100644 src/protocol/mod.rs delete mode 100644 src/protocol/unix.rs delete mode 100644 src/protocol/windows.rs delete mode 100644 src/render/default.rs delete mode 100644 src/render/mod.rs delete mode 100644 src/render/utils.rs delete mode 100644 src/res/config.yml delete mode 100644 src/res/linux/icon.png delete mode 100644 src/res/linux/systemd.service delete mode 100644 src/res/mac/AppIcon.icns delete mode 100644 src/res/mac/EspansoNotifyHelper.zip delete mode 100644 src/res/mac/com.federicoterzi.espanso.plist delete mode 100644 src/res/mac/icon.png delete mode 100644 src/res/mac/icondisabled.png delete mode 100644 src/res/mac/modulo.plist delete mode 100644 src/res/test/config_with_bad_yaml.yml delete mode 100644 src/res/test/get_package_index.json delete mode 100644 src/res/test/index_without_update.json delete mode 100644 src/res/test/install_package_index.json delete mode 100644 src/res/test/outdated_index.json delete mode 100644 src/res/test/working_config.yml delete mode 100644 src/res/win/espanso.bmp delete mode 100644 src/res/win/espanso.ico delete mode 100644 src/res/win/espansored.ico delete mode 100644 src/sysdaemon.rs delete mode 100644 src/system/linux.rs delete mode 100644 src/system/macos.rs delete mode 100644 src/system/mod.rs delete mode 100644 src/system/windows.rs delete mode 100644 src/ui/linux.rs delete mode 100644 src/ui/macos.rs delete mode 100644 src/ui/mod.rs delete mode 100644 src/ui/modulo/mac.rs delete mode 100644 src/ui/modulo/mod.rs delete mode 100644 src/ui/windows.rs delete mode 100644 src/utils.rs 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 84373c4e00a86ad03b782eee20a1609684346f27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22877 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 VRNjI%1DfLg@=Ckd&nR!v{{ueDB@qAs 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 3f2555add7bf56ad0b9f48e3ccb965b47cbdebe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98365 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 >ZJ1B&W+)9%-|HhDdt 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¬c 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 X`oHNNf0ag3fj?CgTB%sk{PF()e%sh3 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 181c5e892d1d8e0a51bfbaa618c349d95f28cdef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8027 zcmV-hAEe-kP)Hg1+lHrgWSWcKdPn90sKGrRqvPeo9CG3uKX#J{(IASm?@+di}}l?o-=)F3E6 zwD^Ni=!>T7nL9I?X}YoAW$t|Qo$sD|?zw001?ah|SeB6#0T!CBEf+H4bBB+JJu8re zhoBb*p;u8ID_yBf0ya+zcePvJL&AGs+11_tpRKn>9TgyPA7ZoSs0)aX0r00)%XR^J z`jH<$>RKN5V(7OqK*TS4xZz{h!*f1C3ECFkK$#7nA@pGN!$;%jYv zwjAKwmYb0gKL(K8-kPtb5${A?tlI~wzMrJ6wTdBr=Y%%%EaEMQ&o}4FQ^DA)s*}Z> z!FI&AHCpoWI|RUqx?7s@$8!5^Q=anY%X@i5{QA6kNcMelpE>R6eCYFpmMsVT zrI(b06~u#xf1yS}_UGdMvD``!0~u->P=lA4?YN`hilQ z|3tHka)7T{2CGqwjZfMwx$5irQN_*|e4l)UHmiYuz74Yp1t^#>hrJ3-SOXDcC_o0^ z7T9R1gAN8V6s;5)ieI5-7aQlmJn}lUna#nz!j%5V$X|o`xX!dHWQRV27P1=rj;t2b zW$~+pTw@bIek?ZvKPDL<64`^#UNTAck#RBsB6*5DP4<%UA_FqU$I>2EH_cM;u)Q~SI+rg`Rn{L z_AC5qq~L$#SMj%U$6Cz0vP{G5Y*=%5RT^yu;}-DInZ=349rJPVM6C3K^oO)8y(fJr{l>k`ead~!ea?NsT>_Ci%bnxC;Vy6= zb6>{xYV#Ue-+LB$7`JEXmTRm^AtP)R9u{)KHsMiWGV&)32xCG~*nyU<>-!d;FP=Re z4r3qYr~6#KE>;1F`>_J_P5xC?ROxV(DIHdCO*p$HRQI@7^PwV@Pvuf+ z5K}u-6REM(K@W$srgorh0{i?O)v0c>QtHxU-hBdD(>iYJ4b2sIOVX2K8m~4gmYVA5 zh^QEb$V`rCQ-|7ZS{nuL-t>?3n=-o(6I(7vocj#GzCZEo`!3>+v;dYIfPu#&ZWzzX z2i^rZ^Mu;6+rb@?NPG+6)c5T6zxpzGe*M(x+{AON=PiJ>H#?ob-|uwRK0yDg0B4PV z0id6JRRdfL?*ITA{YgYYRCodHT?u#;Rkl7=)!j)T0kU)w0tjIfWD~?~a2ecJR0t6q zoI!cx@;&vGSRR{V??Cz>tcRByL=bn4+xu=MdDM^7O1(Fmv|6&DV-!qA<~a^8FzFY!0Qc?B-P<+EuO2?CEAbY_UfvA zDeDhTexycFjkXojWf-7fm9JYMpj;x$Qn9Y-R|1H>62Q;_ID>8gZX4N95lafx->wa% zk}SQiX?ls6Y@CxBVibzV8s*tFuosxf_^7NX*!5z|GXYAFj#@~fDr;JmYK8& z6UD-^n;$(8uj(Qx*Lemg@p-&1<(DALBN#0OrxfrfBXOJ&f@NUzC#!<$t5oc3IXRo1 zza3?OoRWFxC?5F_(55FI*XHf)y;F4^TRmX)vQ}-;9C$5kYAQO?ir`_@=i29ZOg@$ z!0Rrzc6U7Y72?JK-V$HBOw0KAY;Y#3uwZ2q{{nuoVD+zuq$RB%_2axPm zZe*JFE%|ktfFe&QLM#Ko-|lFw{AbMhlatH56X+b%52hv4G~mj5i9q9+3Ongxp$w3x zjGT`-e`XT=lc+4kh9nYMy}Nju#{UF*Mf{`y#Jig^mi zePcl$M*bqWKqbMC6^(G7#{e^E4VoLY59gT>d16y2;Dg_-fL@hT+?SrNJ9;5#8^2r3 z1S#%poq~Ojo^|Ql^x=n5&$%&4fV@%C9kSasEvfXy4{NKEndVG;??`#3Ri1FJN38?o zzV+xJ@}zu*J$AsT6;QT8@&6 z@;tI)Fho@J6ZH_zLtrY%)tmG`0FxqcYs6DQRP2oH$|Bh%FTs*eR9A$T!y~v^RgDol zFe96O;vGPjcnfJvW^V)d?h5wbBS-;&Ue;JoUmvNU_se(E+RELuzoFKU3~n%klL>n+ zqy@F9Ra53HjkFyRGeFjRz8>yI`3oG-7?y|(z`!@DK}vV0(8RnUG=0Drx)|X1bfrW} zn~?cyS{muyvhB2J&j#95Q%Y_u&v=S(A~6HtK+5(&ns$Eeb-ubt+7R&+;A(W;Ln#11 z0X=$}0X&QHpB0Xwy9S>}V|(?EgccFy&BnCx^8sV2sNX1hZQn*(_}x0H!gP@$M@-U) z>NU&E0})ffFju2`5AHu>mdEN^L=qrp*@E6OsT<+No1aLEp9Uqa1#|qR`Ge?pXHBFF zv-?@~H*VLztE;95Hm;;Es`gP@gwuV4RrOepXf z_-+{W3&vhc%PyMX2>49w;XQI_@lU4HWxa<|BP5aS$xsMA`HWE{b%!kh;NX!DJMJ!+Yh?^XE*W^RxO!((kzCt;Q;0$|tYW*4l$uDcC$6u$frbD={UU4F{5n za5@>j1d!#KyyQp}@Eg?tU6ePFmS22(!hp{N&+&Al7r>A`+-Y$4k9buU4rbPQ*h1)W zj6qNaZuIGzgAvkjw> zt!)HGj1^-RPQBqU09#4#FwA4&lo-c6tndx#nM<#pH-&f^mdGg!?YpJlNJs$(q$V^w z{F@2-bgJWZ$^h9*eP_bNc!?2}!$FGke9Fe1m&V^rePO=s3h6pbQ@kX!SrcfDpdw3` z8rXG0s7@IG)~!hpDczu5c2ZP)Z}aH?E!OynqV0#%+mVn~{nNATp9X*xW5(EZ*3Qrj zzz&GIev8wZKPyw0iI-vR$KxLj0fY zq|Kjg`)P`Y=8nAF*nzX1kTF&`B*SZLJI7c|#E%=8Nz!-&!w^r0v{b zv$1E!z_aPxtUe*S5&5mk<{xS6;R>p5J_<{s80y0aU>Iv~X-bv$66XyQw z`wXYEFgisfKSuqGPv4?vx2>ZZ?A&=^I!#kt*uhf}pCLeR?EQ-F`p4VO*z!em@T4*r zJMh4G#`2_F$A4NffJbf_CP{LEQwpFl_hXai&VgbSKb}hN+pv<>R_w%R_jK61XJZFT zfxngQpf`Wma-2Y2`U{(eC8-Z%Y1I!)27uGoIj}3Y`bb$l9mngu?7JYRKaI~S5K_wv zJ3piM4sJ7c4nq~_f>d~8-)3SDAQ345#zXdiFHbPz9z45vjbX-bppe=mMmvC6?w40Dpgo zjn{%6nL&Jt%i8Vpb^T)sr4y$y#1uy715!AL@v zhJy=DJ8#MWy~a=v1gts32xEhB?zO4Dde!svrLbm^85vUWU^^VC@&fzRvPRFeFTwzUpT&5Vi|Mq%6 z9aUQ>)xtTGjZH1==7u`2EI5;XKjs=C`vLWkJWp@?uvPp^lfz4i@u``84`z$%8_-%UACFo$4o;U};% zUXFt#qv60QlG#@r{O*|1$(*#2_CZ*F3rx*h$%b(U60e&JU;xU%4qB`Ux-S*Ml6VPR z$Jw6H8;s>mX=SdS0?IdQ-zS*!f4y-P6mJchzqxv4_M9!(EHE`^09#tru@bfbvcI1Q z?&C6Ug;Sq9f5y=)2D?a>2pFc05?2W!16B=0*J`_+WyTnd-ED{G)LIz;r2w=y*NF^qkZGy zG627wRs;Oq81cNUbRrN$EL~(w&7L=90KBTxk2fQm<&WDnUbe9WNQVo!)2R$j9Gec6 z(>nhi`ozDNcGg!J_5=h<_sW76;Aj9u+&MhasAA0Ux(oMaR5=k zc3@?sMEg7o?C&QjQ%$u~!Pl zji^CHMbza|v+_+DKu@7^DIgue!XeX6w=f2v5&Q8X(teEU>FeKw-M=+HAnyb4G{gS` z#>PC(`}C}L@aiK~!2Z98b_*|q-%F^|5#2+kaf3`5pr$ccmFJQx5Su&`k%xr0$*e#Y z|3QFLkKH>FDFYily>O_czNN`ohYS&&iKPslQgyYWrctdjE5_VljTaijlV}&mrnfWk zoO>kDETqaj*b{i;hc}o`=VZKsPN$t#FR2cN@D?FP`3R`b3~&f8R3Z|)bPeyB2dN@P zf-xI0GdbqtA1zTJTG95cR*zdUfGp`7oLC9`fVo>#`YCn-MI7&7#$*kmXG%9p!9h}v z`oSJVT|v4qhK!{ZO-*$G&MZluEqXA}fTz>#QUsYB_u&wSsB}&KP-A;o{PaGbFWjHWym_cHqb>^V2kbqD~-N4?L)5%Nh0{qo${ z>uJrzSv0x-XhR|#TVq61P28Sg>=;AF(y9_`bbY=cqK|(BD~Q#z!mqOv#ep9WQ7QcR zMWy?3w0s;w$FeOT($t94$-fzXDP0FiVwMIY{L3N)toqIHOAxA7#AIVuOAOb5oq-{+ zv??Vey5OGobsIy&@w%KXb6*`Qvuba|*Lz`e>CflhOoa#;&+~RTiAOUVjiy6-JT&a0 zkdN^4bb628Xw*fsrL1bTcP^FEt`J4?(2zL8fTAXKLP0imoK2h12d z#?UGYUk_>PY+C$bQ+n~l(Kup!wq4P|SqiYJb}Hu9?l{LUDskMtDR2=RgqS^bYP0b+ z5C*#i8b13XLhUu`I~OOCo)~jAqLN$?7Wi%L>yFhyDa^{B!8I%qyn=ukNvgiAX7Xc; z+uCUMX}Jn$%Zyi)EyGe`X4~(0f3mh-ilyKxY`@Mbv^>@xRWo=rPdj6j@pk}v#t%mi z8_dRnG80ip`@QC> zc1Z!=kom9O8)!PbdgHYV#~Qup08V*Wb?}yPt32t9^l`k;Q+80@t-2c4byiEK6I}I8 z&(!OhUSj82K^CD~p$L{7_@=Y=C)9mrT-FDiPK*^}#@Kb%mQERf3sBtp>v%C_Bg+)K z659lxhtFLV^ntFD4r3pKeGKfQk_lyi5o4t(@@pn{J6sBj0V;0xZG%O5H7gc&C63j@ z?pIIYT|8YSu_K8nBUZG1ZpBDnT{C%(Wo*FqWrxK8{D7?Mf5f^{jESy&3qpKn6YF30 z{Db(oRaZ#wSL|~9I5=a(ScOZkuo<9Yvc3-CM?bPt3h<1Ixr`p(^e*1lR~c@f9Le`N zyo5ik+Uw|XaK?r)Vywa?Lf8yIl23#G%+oq=joFxZDyYW#de#?9X%EcAiI~`Pc;43^ zz=#xZbpB^-7$e3iTsno%04UM_P2hbjFaC;9LrtUcI$ z>QDPgsh)En4I-&4=DfT#tcO?cO62RZgDsa%b&6`F_LKEl&+Yt_{uPSpb})g{;2Rrt!C;x{k!Li%`S(r@KPS^DYMX_3$>Vp(Degt!ypzkc z?KiKv*^WN~?+BjOe+*p@M~#87<%>>DsAnTAlgnY5Tm|Vs|BUR(*4$Au0O>wCcbT*t;^!tOUns%dU`AkNEE^F(&PH(Qi@XC6WV$a6 zfn?z5AmME3wkIBkmph0+TvpFlec7kk>rsVNwv)qoJ~f=%2S%70+23OM_x&0IocD$>R&9G|z)hPX)0M zqANXXi2lZwXZ=$jdnnd+M`Q@Hs;pas{wTozD>p2e;!6Po{FdMvrn!ikgh&8xNX|RH z0!39mAeS=S$(4Z!q9ke=k{hPAnQeO5Gt#yTkpNt2^-aDq%(K(sic|?E zh_oHa{GCz2aPrV9bWNQm3Vg0o7z1!&{>gKHcKVCvtZ0=4bSTLLkt27EW{03qPp_OZ zXJzCig!hE7Re)7>-WBsMmP!6^=mh=XtZ0=jdA%J446iFyJD`#Fv#Oite%$Vs@UL-` z0BtSh4q)f-8u+ShPTC3DsABzTtn*cEQvfuJH~8F&xHAA(Qc>*NCe;V8h6Gq{Faet~ zlj%eXfF{R`Jj>-4`RZEG?8FT5`DHH>YkjWVL#{_)YM%{5yT^FPeSG@WrAjsCdtK*a ziVOUi!6#{=^>J>|9kZ7Kj^JKRByqN?F8FI5=~^w@_5w4zY>bx4*dN~ zRk3f4W!^a5aD)N4FS+k5$b%Q_6Oab8u?NX`{cllhD|^CEw<2SS5AKH9Va{KI0JC!d%f-J z<EiHgjqq`Tp1+UJs~^Aogpj9(=ZXvTsLQpPl|pCiKxXLxS$S%gHL9(c6#Ryb2Ns2$KH{VkWGEGoP4k?n1LO0>^}W`H(g6|C~*1XSfl zS*P28{REgw(>Qpgv2t*l8QMUHa)&9a5kJ@CfZiK!MO{^Pqp!NH4~hEOB^aPR@$y&B z8v*~*YoS3;!cZHFDI%Rm9LK~lripkn1Q2Fl2P2R+`;rFLdK_WijFrqvS=LrpOq%;` z`}QXK_b$f(?Fs6A=l6q2RVToTFcEJ>LqsWkAVhFCfYCZlAb2Bt{kDd>WwhEq9pbw| z75tBvYz#P;$DdqYP({cRX%Cog1Fo$F`0M?*%-J2Hw1mE8bTy${7;fXYN8S^%l8hX8?Q5|e_HJX%C+blQN9^Qgi=tX(z#;i%nYe@Hg1+lHrgWSWcKdPn90sKGrRqvPeo9CG3uKX#J{(IASm?@+di}}l?o-=)F3E6 zwD^Ni=!>T7nL9I?X}YoAW$t|Qo$sD|?zw001?ah|SeB6#0T!CBEf+H4bBB+JJu8re zhoBb*p;u8ID_yBf0ya+zcePvJL&AGs+11_tpRKn>9TgyPA7ZoSs0)aX0r00)%XR^J z`jH<$>RKN5V(7OqK*TS4xZz{h!*f1C3ECFkK$#7nA@pGN!$;%jYv zwjAKwmYb0gKL(K8-kPtb5${A?tlI~wzMrJ6wTdBr=Y%%%EaEMQ&o}4FQ^DA)s*}Z> z!FI&AHCpoWI|RUqx?7s@$8!5^Q=anY%X@i5{QA6kNcMelpE>R6eCYFpmMsVT zrI(b06~u#xf1yS}_UGdMvD``!0~u->P=lA4?YN`hilQ z|3tHka)7T{2CGqwjZfMwx$5irQN_*|e4l)UHmiYuz74Yp1t^#>hrJ3-SOXDcC_o0^ z7T9R1gAN8V6s;5)ieI5-7aQlmJn}lUna#nz!j%5V$X|o`xX!dHWQRV27P1=rj;t2b zW$~+pTw@bIek?ZvKPDL<64`^#UNTAck#RBsB6*5DP4<%UA_FqU$I>2EH_cM;u)Q~SI+rg`Rn{L z_AC5qq~L$#SMj%U$6Cz0vP{G5Y*=%5RT^yu;}-DInZ=349rJPVM6C3K^oO)8y(fJr{l>k`ead~!ea?NsT>_Ci%bnxC;Vy6= zb6>{xYV#Ue-+LB$7`JEXmTRm^AtP)R9u{)KHsMiWGV&)32xCG~*nyU<>-!d;FP=Re z4r3qYr~6#KE>;1F`>_J_P5xC?ROxV(DIHdCO*p$HRQI@7^PwV@Pvuf+ z5K}u-6REM(K@W$srgorh0{i?O)v0c>QtHxU-hBdD(>iYJ4b2sIOVX2K8m~4gmYVA5 zh^QEb$V`rCQ-|7ZS{nuL-t>?3n=-o(6I(7vocj#GzCZEo`!3>+v;dYIfPu#&ZWzzX z2i^rZ^Mu;6+rb@?NPG+6)c5T6zxpzGe*M(x+{AON=PiJ>H#?ob-|uwRK0yDg0B4PV z0id6JRRdfL?*IS-pGibPR5%fRl1)fdQ547j_rCkana0Ks9LWz*gQ-nM2|+F@12<6= zis`~gaM7w3LEH3cK@je0(Z?pkf-D4A1x6wk)uM$EcXCh! z4?Ny`_xykVd+xah*eetm^s`6oN|ui}5i4xBbI5Z9HyD;-PJ+Mn)9%>H6=!X`-wuQ5 z>%mjZ<`0>$juH$=nb&d-U5E1gpURcjlb8FZw@eBK6=Q>EIOpSx^AhEH@RWor{Vg2= zTtEbDC@EK^l$Vy<`=)f#7(_;$@}R}PFk8eloFnjXB#O4$BdFOI!S8emBlF*}l-VGY z07w%L`10hRPG?1EmmPhEEtGoGj2B} z9?;=*8R{Z@X^tJ>7M#Xb1K*#W!rhq<#-OpZ3SmLJYZYk?Lm6H>dNV}tdO91eOB|xo zo62jD_Hy{RJde6?sWF&dn!~!AC6Lg3bL%xLWT=E82UCepjjIhsAQ*?DN*miLb_MZ3GS&RG7^jFuS9a$2QB zsR>vdHtqT=wTQo)>nOE@7&?9)&zrmOuB8_(l?RdYJ!7u5h9TB$D?Is3W!)qf78Wz> zxbbZqGph?|Dyzon$_7j<#PK7QfY4PEthJiaZiPXr92@9{We+nYt*q;!m^ywSwBHO< zVsi~rDF`8;82913JxlHVFLoH{pXi%`9;#vpwy2PF-_lGXgJoHu@djLPS$h7hWJmw7 z*6S!#kpI;tcVdjQYm{lF`>M{Xc!GL0p*;CC*>O9*rPc3U28HCLBdRxw%@8DtH&Qt3 f{O@2vVLtu=1$7d({1Y0u00000NkvXXu0mjf5~XSy 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 7423ef91440039666f55bc43db333d553e812304..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15943 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 IB8Gwg0e*&Ii2wiq 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 520331d04af5a1dd115ea50710e66d58c1dcab14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2728 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 VBms$R% - - - - 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 9bde0d24159582e4cc84a24a3c9cec2010040328..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11596 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 KMNUMnLSTXr4gyC2 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 d3f07c39c1a784e458922afa7a3f83c6c6d63a13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40048 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 diff --git a/src/res/mac/EspansoNotifyHelper.zip b/src/res/mac/EspansoNotifyHelper.zip deleted file mode 100644 index 036ba7c3d7e0e080c2d98bbb93798f755cedc49e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118226 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
)N%^EtN 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
bKVI}eFVpyMHPp6X=-#V5+n(3xFXo0 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 - - - - 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 cde3e917153f320d197f02c8ae513cc482e96ef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4207 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 Z>HRAYjI(FswRwNdRu*>V6?+IV{{jD#`Bwk{ diff --git a/src/res/mac/icondisabled.png b/src/res/mac/icondisabled.png deleted file mode 100644 index a289156c84f899726076871f472156bb5f81b4b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4099 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 - - - - 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 882c849d2beaf70e291a0fa7eaef04b61bcf6df1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19256 zcmeHPX>c4z6&|}F1e?Sul9Xd&QsxK{YzhJ?Y(f>~GDRxnDxfF|DuF-@CP0e(2&v#C ziWA{DB$j2zvL#EhF6%z6Ti(^Nt9xag-qq?_>s%eyt<`<3Hhin0CheWs*`A)=wSHK& z+IsWm^?UE@p6-7Ax@QkPd*EqqEPmE00tUK0jvG>&G2PQNn#utH6 z2A#)S(Ygl;n}2!zMux+&PHow*f(t7*TKa;Ms`h%sqDUZC|7G+7+hVt0!N?k0wa?Xb zzTxqSRRmKkTw?m;>{7q1BmTIF9(u9tKCyeg;GEZNHCW!hRb$$ittq+sJHEgruM=In zZ!F&*ZAsXuvmBH6%GY;w=D!l|_>i=(FXc7-+}Chu^ruvR*VjGTw%8L_zK6yc|Hxb@ z{`}yi&PS~~F>4Gj{(^siD6-N!mg{xM6SLP8`QT?^??RrOGhHa&=4S{>%?(Uwz0%5S z_J#10yOdhLCGu1AZR#N>RqgYZ^Jj~=GX>n~d@eYjJ6*tiT*!sUE+UlLjVQTWWi+C1 z`@2%3r2Kh}6V!jRx%bkY9}vLH3!E+FPUUgOGoAeZWO65SxsW0b@?>!aI=lX-%uU=s zUc_LLFA}QZKFr~crEx)dTzthNI`co8dV~ANHPiDQb4&emOIF)L_n57@V<_HW2`p^> zO+?9kCoXgEraPM+Wtr`wAVORQ$mq5gOYdc{3vYu9oJga}9;$3VI%YG{SYKOT?inp8 zYJTzKf-Puq*j3ZkGYi*gjFK{Ct$$$wtL4y4_*8WStUFdog_QKnLQkgQNmSRFLUzcR zh96U!{AiZ@j29CpIA zyB1f5#QrmIklNa)J;uHqqx%x2Q4A?c!J@Byl*0w(Zkb>1p|nCH7OLHb;ZCVDf;`ee zG*In|Rrk0@7PFi7aOgU_GT~Y2eN`fsg%47k_YxjJ+U0PXKRKhiANE zbKuM@bYsn1+5UQR^#MG_PzCtN@L&X^cYsDRSE|S5H9k#K)nUw19xnQBW+!(K(5^H1TSVnSU_4|&cbEi}%b_D+=XOM)7 zhXnz80$mRk#f;8f!G`YBn7=Tld1Y2gsQiZ55Sa8Z1xZYlm&*5uwW;S=C$v~of8Y(s z36INUnos6&;lpLjS_R$}^tn#2x5WzrL#o?hr8 zJyArXnW{Qo73*~2q#M4ECMKs;JR6av_jttH(sx=-V#hL)>f*`9rGxn-6;`id+HLmr zWhu3v@^DNJeHI}ZzmUiG(Iif5eEw4jH?itOIafA7YAU5)lX+%e!UmlRr&vq-P{vdiiJw`ziCmh!7i!w${ z{pST?r340E%o?%f56kFhxV5ro3n|*lmWbAyUzWGHvNod>CMnN{5V`fw$Xii#x4HLy zmq3NIC{N=gl}B65qqJZe9l4;%^DN`h69}x@QU7;;l*{rkCslo$#sm&w3Z63%{Ej@P zr^L$r3(IQvzLTmR!^}m7DD*DTpFqoRe1=}Fq$GxG^yRZf+Z%g4Ps)<5u(CFhR(l9x z72A;pEaxTFN;2n{1|?A_Vxli2$j9n6PQTY|jdM68O-VCN1Ci&h?7$p^dSkYgQD*t4!_!W_i?j;Y%qrkMdYNVk-f;RxBK_Jmf!si9iKgtIR6`(r(*e zv^*}EyUa-d(gA4PdRYX(6jWu_>+#v~sL|_hy5P*~eq5P>h9=gE)b0 z8^TAmghQb?hC%5(vlw2@_L2XJ)Z*wqmd4B|6ct2TAXM`iMecc5xVGy}MGmVMo?YzC zssEXGQP5~Z#>u-wv7)NLt^{^hY5Y_#6J55G?OhB-h-if228Isxh}>zw3txDEk_`!j)*2BW@tKf$n cTCL&u^%bnOH}gZg32XJXOSfwPHNbKI17P=2{r~^~ diff --git a/src/res/win/espanso.ico b/src/res/win/espanso.ico deleted file mode 100644 index 1d1c06c7ef5bf408ce91222819c303d8c22201a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28522 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 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()); - } -}