Merge pull request #271 from federico-terzi/dev

Version 0.6.0
This commit is contained in:
Federico Terzi 2020-05-10 18:49:17 +02:00 committed by GitHub
commit 279aace2bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 3462 additions and 1729 deletions

90
Cargo.lock generated
View File

@ -366,7 +366,7 @@ dependencies = [
[[package]] [[package]]
name = "espanso" name = "espanso"
version = "0.5.5" version = "0.6.0"
dependencies = [ dependencies = [
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
@ -379,6 +379,7 @@ dependencies = [
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)",
@ -412,6 +413,17 @@ dependencies = [
"synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "filetime"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.11" version = "1.0.11"
@ -449,6 +461,23 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent-sys 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@ -601,6 +630,24 @@ name = "indexmap"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "inotify"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "inotify-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "iovec" name = "iovec"
version = "0.1.2" version = "0.1.2"
@ -629,6 +676,11 @@ name = "lazy_static"
version = "1.4.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lazycell"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.62" version = "0.2.62"
@ -722,6 +774,17 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "miow" name = "miow"
version = "0.2.1" version = "0.2.1"
@ -765,6 +828,23 @@ name = "nodrop"
version = "0.1.13" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "notify"
version = "4.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"filetime 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent-sys 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"inotify 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
"mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.41" version = "0.1.41"
@ -1762,11 +1842,14 @@ dependencies = [
"checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" "checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
"checksum filetime 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f59efc38004c988e4201d11d263b8171f49a2e7ec0bdbb71773433f271504a5e"
"checksum flate2 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "2adaffba6388640136149e18ed080b77a78611c1e1d6de75aedcdf78df5d4682" "checksum flate2 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "2adaffba6388640136149e18ed080b77a78611c1e1d6de75aedcdf78df5d4682"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
"checksum fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" "checksum fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
"checksum fsevent 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
"checksum fsevent-sys 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
@ -1782,10 +1865,13 @@ dependencies = [
"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
"checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" "checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3"
"checksum inotify 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8"
"checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0"
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" "checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba"
"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
"checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" "checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
@ -1798,10 +1884,12 @@ dependencies = [
"checksum mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599" "checksum mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599"
"checksum miniz_oxide 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7108aff85b876d06f22503dcce091e29f76733b2bfdd91eebce81f5e68203a10" "checksum miniz_oxide 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7108aff85b876d06f22503dcce091e29f76733b2bfdd91eebce81f5e68203a10"
"checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" "checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23"
"checksum mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
"checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" "checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e"
"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd"
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" "checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "espanso" name = "espanso"
version = "0.5.5" version = "0.6.0"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"] authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust" description = "Cross-platform Text Expander written in Rust"
@ -30,6 +30,7 @@ tempfile = "3.1.0"
dialoguer = "0.4.0" dialoguer = "0.4.0"
rand = "0.7.2" rand = "0.7.2"
zip = "0.5.3" zip = "0.5.3"
notify = "4.0.13"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.62" libc = "0.2.62"

View File

@ -25,7 +25,7 @@ fn get_config() -> PathBuf {
*/ */
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn print_config() { fn print_config() {
println!("cargo:rustc-link-lib=static=winbridge"); println!("cargo:rustc-link-lib=static=winbridge");
println!("cargo:rustc-link-lib=dylib=user32"); println!("cargo:rustc-link-lib=dylib=user32");
} }
@ -47,10 +47,9 @@ fn print_config() {
println!("cargo:rustc-link-lib=framework=IOKit"); println!("cargo:rustc-link-lib=framework=IOKit");
} }
fn main() fn main() {
{
let dst = get_config(); let dst = get_config();
println!("cargo:rustc-link-search=native={}", dst.display()); println!("cargo:rustc-link-search=native={}", dst.display());
print_config(); print_config();
} }

View File

@ -325,7 +325,7 @@ void fast_release_all_keys() {
XFlush(xdo_context->xdpy); XFlush(xdo_context->xdpy);
} }
void fast_send_string(const char * string) { void fast_send_string(const char * string, int32_t delay) {
// It may happen that when an expansion is triggered, some keys are still pressed. // 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 // 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). // will not be able to register that keypress (as it is already pressed).
@ -337,10 +337,15 @@ void fast_send_string(const char * string) {
int revert_to; int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to); XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
fast_enter_text_window(xdo_context, focused, string, 1); 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) { void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count, int32_t delay) {
int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode); int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode);
Window focused; Window focused;
@ -350,13 +355,18 @@ void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count) {
for (int i = 0; i<count; i++) { for (int i = 0; i<count; i++) {
fast_send_event(xdo_context, focused, keycode, 1); fast_send_event(xdo_context, focused, keycode, 1);
fast_send_event(xdo_context, focused, keycode, 0); fast_send_event(xdo_context, focused, keycode, 0);
if (delay > 0) {
usleep(delay * 1000);
XFlush(xdo_context->xdpy);
}
} }
XFlush(xdo_context->xdpy); XFlush(xdo_context->xdpy);
} }
void fast_send_enter() { void fast_send_enter() {
_fast_send_keycode_to_focused_window(XK_Return, 1); _fast_send_keycode_to_focused_window(XK_Return, 1, 0);
} }
void delete_string(int32_t count) { void delete_string(int32_t count) {
@ -365,8 +375,8 @@ void delete_string(int32_t count) {
} }
} }
void fast_delete_string(int32_t count) { void fast_delete_string(int32_t count, int32_t delay) {
_fast_send_keycode_to_focused_window(XK_BackSpace, count); _fast_send_keycode_to_focused_window(XK_BackSpace, count, delay);
} }
void left_arrow(int32_t count) { void left_arrow(int32_t count) {
@ -376,7 +386,7 @@ void left_arrow(int32_t count) {
} }
void fast_left_arrow(int32_t count) { void fast_left_arrow(int32_t count) {
_fast_send_keycode_to_focused_window(XK_Left, count); _fast_send_keycode_to_focused_window(XK_Left, count, 0);
} }
void trigger_paste() { void trigger_paste() {

View File

@ -65,7 +65,7 @@ extern "C" void send_string(const char * string);
/* /*
* Type the given string by simulating Key Presses using a faster inject method * Type the given string by simulating Key Presses using a faster inject method
*/ */
extern "C" void fast_send_string(const char * string); extern "C" void fast_send_string(const char * string, int32_t delay);
/* /*
* Send the backspace keypress, *count* times. * Send the backspace keypress, *count* times.
@ -75,7 +75,7 @@ extern "C" void delete_string(int32_t count);
/* /*
* Send the backspace keypress, *count* times using a faster inject method * Send the backspace keypress, *count* times using a faster inject method
*/ */
extern "C" void fast_delete_string(int32_t count); extern "C" void fast_delete_string(int32_t count, int32_t delay);
/* /*
* Send an Enter key press * Send an Enter key press

View File

@ -215,8 +215,7 @@ int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string,
key.needs_binding = 0; key.needs_binding = 0;
fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2); fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2);
/* XXX: Flush here or at the end? or never? */ XFlush(xdo->xdpy);
//XFlush(xdo->xdpy);
} /* walk string generating a keysequence */ } /* walk string generating a keysequence */
//free(keys); //free(keys);

View File

@ -495,8 +495,12 @@ void send_string(const wchar_t * string) {
/* /*
* Send the backspace keypress, *count* times. * Send the backspace keypress, *count* times.
*/ */
void delete_string(int32_t count) { void delete_string(int32_t count, int32_t delay) {
send_multi_vkey(VK_BACK, count); 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) { void send_vkey(int32_t vk) {
@ -539,6 +543,27 @@ void send_multi_vkey(int32_t vk, int32_t count) {
SendInput(vec.size(), vec.data(), sizeof(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_paste() { void trigger_paste() {
std::vector<INPUT> vec; std::vector<INPUT> vec;
@ -685,6 +710,35 @@ int32_t start_daemon_process() {
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 // CLIPBOARD
int32_t set_clipboard(wchar_t *text) { int32_t set_clipboard(wchar_t *text) {

View File

@ -72,10 +72,15 @@ extern "C" void send_vkey(int32_t vk);
*/ */
extern "C" void send_multi_vkey(int32_t vk, int32_t count); 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. * Send the backspace keypress, *count* times.
*/ */
extern "C" void delete_string(int32_t count); extern "C" void delete_string(int32_t count, int32_t delay);
/* /*
* Send the Paste keyboard shortcut (CTRL+V) * Send the Paste keyboard shortcut (CTRL+V)
@ -159,4 +164,8 @@ extern "C" int32_t set_clipboard(wchar_t * text);
*/ */
extern "C" int32_t set_clipboard_image(wchar_t * path); extern "C" int32_t set_clipboard_image(wchar_t * path);
// PROCESSES
extern "C" int32_t start_process(wchar_t * cmd);
#endif //ESPANSO_BRIDGE_H #endif //ESPANSO_BRIDGE_H

View File

@ -1,5 +1,5 @@
name: espanso name: espanso
version: 0.5.5 version: 0.6.0
summary: A Cross-platform Text Expander written in Rust summary: A Cross-platform Text Expander written in Rust
description: | description: |
espanso is a Cross-platform, Text Expander written in Rust. espanso is a Cross-platform, Text Expander written in Rust.

View File

@ -17,11 +17,11 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::os::raw::{c_void, c_char}; use std::os::raw::{c_char, c_void};
#[allow(improper_ctypes)] #[allow(improper_ctypes)]
#[link(name="linuxbridge", kind="static")] #[link(name = "linuxbridge", kind = "static")]
extern { extern "C" {
pub fn check_x11() -> i32; pub fn check_x11() -> i32;
pub fn initialize(s: *const c_void) -> i32; pub fn initialize(s: *const c_void) -> i32;
pub fn eventloop(); pub fn eventloop();
@ -34,8 +34,9 @@ extern {
pub fn is_current_window_special() -> i32; pub fn is_current_window_special() -> i32;
// Keyboard // Keyboard
pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u8, pub fn register_keypress_callback(
i32, i32, i32)); cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32),
);
pub fn send_string(string: *const c_char); pub fn send_string(string: *const c_char);
pub fn delete_string(count: i32); pub fn delete_string(count: i32);
@ -48,8 +49,8 @@ extern {
pub fn trigger_ctrl_alt_paste(); pub fn trigger_ctrl_alt_paste();
pub fn trigger_copy(); pub fn trigger_copy();
pub fn fast_send_string(string: *const c_char); pub fn fast_send_string(string: *const c_char, delay: i32);
pub fn fast_delete_string(count: i32); pub fn fast_delete_string(count: i32, delay: i32);
pub fn fast_left_arrow(count: i32); pub fn fast_left_arrow(count: i32);
pub fn fast_send_enter(); pub fn fast_send_enter();
} }

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::os::raw::{c_void, c_char}; use std::os::raw::{c_char, c_void};
#[repr(C)] #[repr(C)]
pub struct MacMenuItem { pub struct MacMenuItem {
@ -27,8 +27,8 @@ pub struct MacMenuItem {
} }
#[allow(improper_ctypes)] #[allow(improper_ctypes)]
#[link(name="macbridge", kind="static")] #[link(name = "macbridge", kind = "static")]
extern { extern "C" {
pub fn initialize(s: *const c_void, icon_path: *const c_char, show_icon: i32); pub fn initialize(s: *const c_void, icon_path: *const c_char, show_icon: i32);
pub fn eventloop(); pub fn eventloop();
pub fn headless_eventloop(); pub fn headless_eventloop();
@ -39,8 +39,8 @@ extern {
pub fn open_settings_panel(); pub fn open_settings_panel();
pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32; 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_active_app_identifier(buffer: *mut c_char, size: i32) -> i32;
pub fn get_secure_input_process(pid:*mut i64) -> 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; pub fn get_path_from_pid(pid: i64, buffer: *mut c_char, size: i32) -> i32;
// Clipboard // Clipboard
pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32; pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;
@ -48,13 +48,14 @@ extern {
pub fn set_clipboard_image(path: *const c_char) -> i32; pub fn set_clipboard_image(path: *const c_char) -> i32;
// UI // UI
pub fn register_icon_click_callback(cb: extern fn(_self: *mut c_void)); 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 show_context_menu(items: *const MacMenuItem, count: i32) -> i32;
pub fn register_context_menu_click_callback(cb: extern fn(_self: *mut c_void, id: i32)); pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
// Keyboard // Keyboard
pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u8, pub fn register_keypress_callback(
i32, i32, i32)); cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32),
);
pub fn send_string(string: *const c_char); pub fn send_string(string: *const c_char);
pub fn send_vkey(vk: i32); pub fn send_vkey(vk: i32);
@ -62,4 +63,4 @@ extern {
pub fn delete_string(count: i32); pub fn delete_string(count: i32);
pub fn trigger_paste(); pub fn trigger_paste();
pub fn trigger_copy(); pub fn trigger_copy();
} }

View File

@ -24,4 +24,4 @@ pub(crate) mod windows;
pub(crate) mod linux; pub(crate) mod linux;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub(crate) mod macos; pub(crate) mod macos;

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::os::raw::{c_void}; use std::os::raw::c_void;
#[repr(C)] #[repr(C)]
pub struct WindowsMenuItem { pub struct WindowsMenuItem {
@ -27,10 +27,15 @@ pub struct WindowsMenuItem {
} }
#[allow(improper_ctypes)] #[allow(improper_ctypes)]
#[link(name="winbridge", kind="static")] #[link(name = "winbridge", kind = "static")]
extern { extern "C" {
pub fn start_daemon_process() -> i32; pub fn start_daemon_process() -> i32;
pub fn initialize(s: *const c_void, ico_path: *const u16, bmp_path: *const u16, show_icon: i32) -> i32; pub fn initialize(
s: *const c_void,
ico_path: *const u16,
bmp_path: *const u16,
show_icon: i32,
) -> i32;
// SYSTEM // SYSTEM
pub fn get_active_window_name(buffer: *mut u16, size: i32) -> i32; pub fn get_active_window_name(buffer: *mut u16, size: i32) -> i32;
@ -40,8 +45,8 @@ extern {
pub fn show_notification(message: *const u16) -> i32; pub fn show_notification(message: *const u16) -> i32;
pub fn close_notification(); pub fn close_notification();
pub fn show_context_menu(items: *const WindowsMenuItem, count: i32) -> i32; pub fn show_context_menu(items: *const WindowsMenuItem, count: i32) -> i32;
pub fn register_icon_click_callback(cb: extern fn(_self: *mut c_void)); pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
pub fn register_context_menu_click_callback(cb: extern fn(_self: *mut c_void, id: i32)); pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
pub fn cleanup_ui(); pub fn cleanup_ui();
// CLIPBOARD // CLIPBOARD
@ -50,14 +55,19 @@ extern {
pub fn set_clipboard_image(path: *const u16) -> i32; pub fn set_clipboard_image(path: *const u16) -> i32;
// KEYBOARD // KEYBOARD
pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u16, pub fn register_keypress_callback(
i32, i32, i32, i32, i32)); cb: extern "C" fn(_self: *mut c_void, *const u16, i32, i32, i32, i32, i32),
);
pub fn eventloop(); pub fn eventloop();
pub fn send_string(string: *const u16); pub fn send_string(string: *const u16);
pub fn send_vkey(vk: i32); pub fn send_vkey(vk: i32);
pub fn send_multi_vkey(vk: i32, count: i32); pub fn send_multi_vkey(vk: i32, count: i32);
pub fn delete_string(count: i32); pub fn delete_string(count: i32, delay: i32);
pub fn trigger_paste(); pub fn trigger_paste();
pub fn trigger_copy(); pub fn trigger_copy();
}
// PROCESSES
pub fn start_process(cmd: *const u16) -> i32;
}

View File

@ -27,20 +27,18 @@ pub fn check_preconditions() -> bool {
let mut result = true; let mut result = true;
// Make sure notify-send is installed // Make sure notify-send is installed
let status = Command::new("notify-send") let status = Command::new("notify-send").arg("-v").output();
.arg("-v")
.output();
if status.is_err() { if status.is_err() {
println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it."); println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it.");
result = false; result = false;
} }
// Make sure xclip is installed // Make sure xclip is installed
let status = Command::new("xclip") let status = Command::new("xclip").arg("-version").output();
.arg("-version")
.output();
if status.is_err() { if status.is_err() {
println!("Error: 'xclip' command is needed for espanso to work correctly, please install it."); println!(
"Error: 'xclip' command is needed for espanso to work correctly, please install it."
);
result = false; result = false;
} }
@ -70,4 +68,4 @@ pub fn check_preconditions() -> bool {
pub fn check_preconditions() -> bool { pub fn check_preconditions() -> bool {
// Nothing needed on windows // Nothing needed on windows
true true
} }

View File

@ -17,18 +17,16 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::process::{Command, Stdio}; use log::error;
use std::io::{Write}; use std::io::Write;
use log::{error};
use std::path::Path; use std::path::Path;
use std::process::{Command, Stdio};
pub struct LinuxClipboardManager {} pub struct LinuxClipboardManager {}
impl super::ClipboardManager for LinuxClipboardManager { impl super::ClipboardManager for LinuxClipboardManager {
fn get_clipboard(&self) -> Option<String> { fn get_clipboard(&self) -> Option<String> {
let res = Command::new("xclip") let res = Command::new("xclip").args(&["-o", "-sel", "clip"]).output();
.args(&["-o", "-sel", "clip"])
.output();
if let Ok(output) = res { if let Ok(output) = res {
if output.status.success() { if output.status.success() {
@ -71,14 +69,14 @@ impl super::ClipboardManager for LinuxClipboardManager {
Some(ext) => { Some(ext) => {
let ext = ext.to_string_lossy().to_lowercase(); let ext = ext.to_string_lossy().to_lowercase();
match ext.as_ref() { match ext.as_ref() {
"png" => {"image/png"}, "png" => "image/png",
"jpg" | "jpeg" => {"image/jpeg"}, "jpg" | "jpeg" => "image/jpeg",
"gif" => {"image/gif"}, "gif" => "image/gif",
"svg" => {"image/svg"}, "svg" => "image/svg",
_ => {"image/png"}, _ => "image/png",
} }
}, }
None => {"image/png"}, None => "image/png",
}; };
let image_path = image_path.to_string_lossy().into_owned(); let image_path = image_path.to_string_lossy().into_owned();
@ -95,6 +93,6 @@ impl super::ClipboardManager for LinuxClipboardManager {
impl LinuxClipboardManager { impl LinuxClipboardManager {
pub fn new() -> LinuxClipboardManager { pub fn new() -> LinuxClipboardManager {
LinuxClipboardManager{} LinuxClipboardManager {}
} }
} }

View File

@ -17,20 +17,18 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::os::raw::c_char;
use crate::bridge::macos::*; use crate::bridge::macos::*;
use std::ffi::{CStr, CString};
use std::path::Path;
use log::{error, warn}; use log::{error, warn};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::path::Path;
pub struct MacClipboardManager { pub struct MacClipboardManager {}
}
impl super::ClipboardManager for MacClipboardManager { impl super::ClipboardManager for MacClipboardManager {
fn get_clipboard(&self) -> Option<String> { fn get_clipboard(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [c_char; 2000] = [0; 2000]; let mut buffer: [c_char; 2000] = [0; 2000];
let res = get_clipboard(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_clipboard(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -71,6 +69,6 @@ impl super::ClipboardManager for MacClipboardManager {
impl MacClipboardManager { impl MacClipboardManager {
pub fn new() -> MacClipboardManager { pub fn new() -> MacClipboardManager {
MacClipboardManager{} MacClipboardManager {}
} }
} }

View File

@ -50,4 +50,4 @@ pub fn get_manager() -> impl ClipboardManager {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn get_manager() -> impl ClipboardManager { pub fn get_manager() -> impl ClipboardManager {
macos::MacClipboardManager::new() macos::MacClipboardManager::new()
} }

View File

@ -17,24 +17,22 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use widestring::U16CString; use crate::bridge::windows::{get_clipboard, set_clipboard, set_clipboard_image};
use crate::bridge::windows::{set_clipboard, get_clipboard, set_clipboard_image};
use std::path::Path; use std::path::Path;
use widestring::U16CString;
pub struct WindowsClipboardManager { pub struct WindowsClipboardManager {}
}
impl WindowsClipboardManager { impl WindowsClipboardManager {
pub fn new() -> WindowsClipboardManager { pub fn new() -> WindowsClipboardManager {
WindowsClipboardManager{} WindowsClipboardManager {}
} }
} }
impl super::ClipboardManager for WindowsClipboardManager { impl super::ClipboardManager for WindowsClipboardManager {
fn get_clipboard(&self) -> Option<String> { fn get_clipboard(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [u16; 2000] = [0; 2000]; let mut buffer: [u16; 2000] = [0; 2000];
let res = get_clipboard(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_clipboard(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -62,4 +60,4 @@ impl super::ClipboardManager for WindowsClipboardManager {
set_clipboard_image(payload_c.as_ptr()); set_clipboard_image(payload_c.as_ptr());
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -17,13 +17,13 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use regex::Regex; use super::{ConfigSet, Configs};
use crate::matcher::Match;
use crate::system::SystemManager; use crate::system::SystemManager;
use log::{debug, warn};
use regex::Regex;
use std::cell::RefCell; use std::cell::RefCell;
use std::time::SystemTime; use std::time::SystemTime;
use log::{debug, warn};
use super::{Configs, ConfigSet};
use crate::matcher::Match;
pub struct RuntimeConfigManager<'a, S: SystemManager> { pub struct RuntimeConfigManager<'a, S: SystemManager> {
set: ConfigSet, set: ConfigSet,
@ -37,10 +37,10 @@ pub struct RuntimeConfigManager<'a, S: SystemManager> {
// Cache // Cache
last_config_update: RefCell<SystemTime>, last_config_update: RefCell<SystemTime>,
last_config: RefCell<Option<&'a Configs>> last_config: RefCell<Option<&'a Configs>>,
} }
impl <'a, S: SystemManager> RuntimeConfigManager<'a, S> { impl<'a, S: SystemManager> RuntimeConfigManager<'a, S> {
pub fn new<'b>(set: ConfigSet, system_manager: S) -> RuntimeConfigManager<'b, S> { pub fn new<'b>(set: ConfigSet, system_manager: S) -> RuntimeConfigManager<'b, S> {
// Compile all the regexps // Compile all the regexps
let title_regexps = set.specific.iter().map( let title_regexps = set.specific.iter().map(
@ -101,7 +101,7 @@ impl <'a, S: SystemManager> RuntimeConfigManager<'a, S> {
exec_regexps, exec_regexps,
system_manager, system_manager,
last_config_update, last_config_update,
last_config last_config,
} }
} }
@ -118,10 +118,12 @@ impl <'a, S: SystemManager> RuntimeConfigManager<'a, S> {
for (i, regex) in self.title_regexps.iter().enumerate() { for (i, regex) in self.title_regexps.iter().enumerate() {
if let Some(regex) = regex { if let Some(regex) = regex {
if regex.is_match(&title) { if regex.is_match(&title) {
debug!("Matched 'filter_title' for '{}' config, using custom settings.", debug!(
self.set.specific[i].name); "Matched 'filter_title' for '{}' config, using custom settings.",
self.set.specific[i].name
);
return &self.set.specific[i] return &self.set.specific[i];
} }
} }
} }
@ -135,10 +137,12 @@ impl <'a, S: SystemManager> RuntimeConfigManager<'a, S> {
for (i, regex) in self.exec_regexps.iter().enumerate() { for (i, regex) in self.exec_regexps.iter().enumerate() {
if let Some(regex) = regex { if let Some(regex) = regex {
if regex.is_match(&executable) { if regex.is_match(&executable) {
debug!("Matched 'filter_exec' for '{}' config, using custom settings.", debug!(
self.set.specific[i].name); "Matched 'filter_exec' for '{}' config, using custom settings.",
self.set.specific[i].name
);
return &self.set.specific[i] return &self.set.specific[i];
} }
} }
} }
@ -152,10 +156,12 @@ impl <'a, S: SystemManager> RuntimeConfigManager<'a, S> {
for (i, regex) in self.class_regexps.iter().enumerate() { for (i, regex) in self.class_regexps.iter().enumerate() {
if let Some(regex) = regex { if let Some(regex) = regex {
if regex.is_match(&class) { if regex.is_match(&class) {
debug!("Matched 'filter_class' for '{}' config, using custom settings.", debug!(
self.set.specific[i].name); "Matched 'filter_class' for '{}' config, using custom settings.",
self.set.specific[i].name
);
return &self.set.specific[i] return &self.set.specific[i];
} }
} }
} }
@ -167,7 +173,7 @@ impl <'a, S: SystemManager> RuntimeConfigManager<'a, S> {
} }
} }
impl <'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a, S> { impl<'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a, S> {
fn active_config(&'a self) -> &'a Configs { fn active_config(&'a self) -> &'a Configs {
let mut last_config_update = self.last_config_update.borrow_mut(); let mut last_config_update = self.last_config_update.borrow_mut();
if let Ok(elapsed) = (*last_config_update).elapsed() { if let Ok(elapsed) = (*last_config_update).elapsed() {
@ -204,8 +210,8 @@ impl <'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::config::ConfigManager;
use crate::config::tests::{create_temp_espanso_directories, create_user_config_file}; use crate::config::tests::{create_temp_espanso_directories, create_user_config_file};
use crate::config::ConfigManager;
struct DummySystemManager { struct DummySystemManager {
title: RefCell<String>, title: RefCell<String>,
@ -225,10 +231,10 @@ mod tests {
} }
impl DummySystemManager { impl DummySystemManager {
pub fn new_custom(title: &str, class: &str, exec: &str) -> DummySystemManager { pub fn new_custom(title: &str, class: &str, exec: &str) -> DummySystemManager {
DummySystemManager{ DummySystemManager {
title: RefCell::new(title.to_owned()), title: RefCell::new(title.to_owned()),
class: RefCell::new(class.to_owned()), class: RefCell::new(class.to_owned()),
exec: RefCell::new(exec.to_owned()) exec: RefCell::new(exec.to_owned()),
} }
} }
@ -247,21 +253,33 @@ mod tests {
fn test_runtime_constructor_regex_load_correctly() { fn test_runtime_constructor_regex_load_correctly() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: myname1 name: myname1
filter_exec: "Title" filter_exec: "Title"
"###); "###,
);
create_user_config_file(&data_dir.path(), "specific2.yml", r###" create_user_config_file(
&data_dir.path(),
"specific2.yml",
r###"
name: myname2 name: myname2
filter_title: "Yeah" filter_title: "Yeah"
filter_class: "Car" filter_class: "Car"
"###); "###,
);
create_user_config_file(&data_dir.path(), "specific3.yml", r###" create_user_config_file(
&data_dir.path(),
"specific3.yml",
r###"
name: myname3 name: myname3
filter_title: "Nice" filter_title: "Nice"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
@ -270,12 +288,24 @@ mod tests {
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
let sp1index = config_manager.set.specific let sp1index = config_manager
.iter().position(|x| x.name == "myname1").unwrap(); .set
let sp2index = config_manager.set.specific .specific
.iter().position(|x| x.name == "myname2").unwrap(); .iter()
let sp3index = config_manager.set.specific .position(|x| x.name == "myname1")
.iter().position(|x| x.name == "myname3").unwrap(); .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.exec_regexps.len(), 3);
assert_eq!(config_manager.title_regexps.len(), 3); assert_eq!(config_manager.title_regexps.len(), 3);
@ -298,21 +328,33 @@ mod tests {
fn test_runtime_constructor_malformed_regexes_are_ignored() { fn test_runtime_constructor_malformed_regexes_are_ignored() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: myname1 name: myname1
filter_exec: "[`-_]" filter_exec: "[`-_]"
"###); "###,
);
create_user_config_file(&data_dir.path(), "specific2.yml", r###" create_user_config_file(
&data_dir.path(),
"specific2.yml",
r###"
name: myname2 name: myname2
filter_title: "[`-_]" filter_title: "[`-_]"
filter_class: "Car" filter_class: "Car"
"###); "###,
);
create_user_config_file(&data_dir.path(), "specific3.yml", r###" create_user_config_file(
&data_dir.path(),
"specific3.yml",
r###"
name: myname3 name: myname3
filter_title: "Nice" filter_title: "Nice"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
@ -321,12 +363,24 @@ mod tests {
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
let sp1index = config_manager.set.specific let sp1index = config_manager
.iter().position(|x| x.name == "myname1").unwrap(); .set
let sp2index = config_manager.set.specific .specific
.iter().position(|x| x.name == "myname2").unwrap(); .iter()
let sp3index = config_manager.set.specific .position(|x| x.name == "myname1")
.iter().position(|x| x.name == "myname3").unwrap(); .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.exec_regexps.len(), 3);
assert_eq!(config_manager.title_regexps.len(), 3); assert_eq!(config_manager.title_regexps.len(), 3);
@ -349,15 +403,20 @@ mod tests {
fn test_runtime_calculate_active_config_specific_title_match() { fn test_runtime_calculate_active_config_specific_title_match() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: chrome name: chrome
filter_title: "Chrome" filter_title: "Chrome"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); 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); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
@ -368,15 +427,20 @@ mod tests {
fn test_runtime_calculate_active_config_specific_class_match() { fn test_runtime_calculate_active_config_specific_class_match() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: chrome name: chrome
filter_class: "Chrome" filter_class: "Chrome"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); 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); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
@ -387,15 +451,20 @@ mod tests {
fn test_runtime_calculate_active_config_specific_exec_match() { fn test_runtime_calculate_active_config_specific_exec_match() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: chrome name: chrome
filter_exec: "chrome.exe" filter_exec: "chrome.exe"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); 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); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
@ -406,16 +475,21 @@ mod tests {
fn test_runtime_calculate_active_config_specific_multi_filter_match() { fn test_runtime_calculate_active_config_specific_multi_filter_match() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: chrome name: chrome
filter_class: Browser filter_class: Browser
filter_exec: "firefox.exe" filter_exec: "firefox.exe"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Browser", "C:\\Path\\chrome.exe"); 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); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
@ -426,15 +500,20 @@ mod tests {
fn test_runtime_calculate_active_config_no_match() { fn test_runtime_calculate_active_config_no_match() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: firefox name: firefox
filter_title: "Firefox" filter_title: "Firefox"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); 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); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
@ -445,25 +524,32 @@ mod tests {
fn test_runtime_active_config_cache() { fn test_runtime_active_config_cache() {
let (data_dir, package_dir) = create_temp_espanso_directories(); let (data_dir, package_dir) = create_temp_espanso_directories();
create_user_config_file(&data_dir.path(), "specific.yml", r###" create_user_config_file(
&data_dir.path(),
"specific.yml",
r###"
name: firefox name: firefox
filter_title: "Firefox" filter_title: "Firefox"
"###); "###,
);
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
assert!(config_set.is_ok()); assert!(config_set.is_ok());
let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); 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); let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
assert_eq!(config_manager.active_config().name, "default"); assert_eq!(config_manager.active_config().name, "default");
assert_eq!(config_manager.calculate_active_config().name, "default"); assert_eq!(config_manager.calculate_active_config().name, "default");
config_manager.system_manager.change("Firefox", "Browser", "C\\Path\\firefox.exe"); config_manager
.system_manager
.change("Firefox", "Browser", "C\\Path\\firefox.exe");
// Active config should have changed, but not cached one // Active config should have changed, but not cached one
assert_eq!(config_manager.calculate_active_config().name, "firefox"); assert_eq!(config_manager.calculate_active_config().name, "firefox");
assert_eq!(config_manager.active_config().name, "default"); assert_eq!(config_manager.active_config().name, "default");
} }
} }

View File

@ -17,18 +17,18 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::sync::mpsc::Sender;
use std::os::raw::{c_void, c_char};
use crate::event::*;
use crate::event::KeyModifier::*;
use crate::bridge::linux::*; use crate::bridge::linux::*;
use std::process::exit; use crate::config::Configs;
use crate::event::KeyModifier::*;
use crate::event::*;
use log::{debug, error}; use log::{debug, error};
use std::ffi::CStr; 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::AtomicBool;
use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire; use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs; use std::sync::mpsc::Sender;
use std::sync::Arc;
#[repr(C)] #[repr(C)]
pub struct LinuxContext { pub struct LinuxContext {
@ -37,11 +37,13 @@ pub struct LinuxContext {
} }
impl LinuxContext { impl LinuxContext {
pub fn new(_: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<LinuxContext> { pub fn new(
_: Configs,
send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
) -> Box<LinuxContext> {
// Check if the X11 context is available // Check if the X11 context is available
let x11_available = unsafe { let x11_available = unsafe { check_x11() };
check_x11()
};
if x11_available < 0 { if x11_available < 0 {
error!("Error, can't connect to X11 context"); error!("Error, can't connect to X11 context");
@ -50,7 +52,7 @@ impl LinuxContext {
let context = Box::new(LinuxContext { let context = Box::new(LinuxContext {
send_channel, send_channel,
is_injecting is_injecting,
}); });
unsafe { unsafe {
@ -79,14 +81,21 @@ impl super::Context for LinuxContext {
impl Drop for LinuxContext { impl Drop for LinuxContext {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { cleanup(); } unsafe {
cleanup();
}
} }
} }
// Native bridge code // Native bridge code
extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, _len: i32, extern "C" fn keypress_callback(
event_type: i32, key_code: i32) { _self: *mut c_void,
raw_buffer: *const u8,
_len: i32,
event_type: i32,
key_code: i32,
) {
unsafe { unsafe {
let _self = _self as *mut LinuxContext; let _self = _self as *mut LinuxContext;
@ -98,7 +107,8 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, _len: i32
return; return;
} }
if event_type == 0 { // Char event if event_type == 0 {
// Char event
// Convert the received buffer to a string // Convert the received buffer to a string
let c_str = CStr::from_ptr(raw_buffer as *const c_char); let c_str = CStr::from_ptr(raw_buffer as *const c_char);
let char_str = c_str.to_str(); let char_str = c_str.to_str();
@ -108,12 +118,13 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, _len: i32
Ok(char_str) => { Ok(char_str) => {
let event = Event::Key(KeyEvent::Char(char_str.to_owned())); let event = Event::Key(KeyEvent::Char(char_str.to_owned()));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
}, }
Err(e) => { Err(e) => {
debug!("Unable to receive char: {}",e); debug!("Unable to receive char: {}", e);
}, }
} }
}else if event_type == 1 { // Modifier event } else if event_type == 1 {
// Modifier event
let modifier: Option<KeyModifier> = match key_code { let modifier: Option<KeyModifier> = match key_code {
133 => Some(LEFT_META), 133 => Some(LEFT_META),
@ -125,19 +136,22 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, _len: i32
37 => Some(LEFT_CTRL), 37 => Some(LEFT_CTRL),
105 => Some(RIGHT_CTRL), 105 => Some(RIGHT_CTRL),
22 => Some(BACKSPACE), 22 => Some(BACKSPACE),
66 => Some(CAPS_LOCK),
_ => None, _ => None,
}; };
if let Some(modifier) = modifier { if let Some(modifier) = modifier {
let event = Event::Key(KeyEvent::Modifier(modifier)); let event = Event::Key(KeyEvent::Modifier(modifier));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
}else{ // Not one of the default modifiers, send an "other" event } else {
// Not one of the default modifiers, send an "other" event
let event = Event::Key(KeyEvent::Other); let event = Event::Key(KeyEvent::Other);
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
} }
}else{ // Other type of event } else {
// Other type of event
let event = Event::Key(KeyEvent::Other); let event = Event::Key(KeyEvent::Other);
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
} }
} }
} }

View File

@ -17,23 +17,23 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::sync::mpsc::Sender;
use std::os::raw::{c_void, c_char};
use crate::bridge::macos::*; use crate::bridge::macos::*;
use crate::event::{Event, KeyEvent, KeyModifier, ActionType, SystemEvent}; use crate::config::Configs;
use crate::event::KeyModifier::*; use crate::event::KeyModifier::*;
use std::ffi::{CString, CStr}; use crate::event::{ActionType, Event, KeyEvent, KeyModifier, SystemEvent};
use std::{fs, thread}; use crate::system::macos::MacSystemManager;
use log::{info, error, debug}; 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::process::exit;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire; use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs; use std::sync::mpsc::Sender;
use std::cell::RefCell; use std::sync::Arc;
use crate::system::macos::MacSystemManager; use std::{fs, thread};
const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); const STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icon.png");
pub struct MacContext { pub struct MacContext {
pub send_channel: Sender<Event>, pub send_channel: Sender<Event>,
@ -43,14 +43,20 @@ pub struct MacContext {
} }
impl MacContext { impl MacContext {
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<MacContext> { pub fn new(
config: Configs,
send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
) -> Box<MacContext> {
// Check accessibility // Check accessibility
unsafe { unsafe {
let res = prompt_accessibility(); let res = prompt_accessibility();
if res == 0 { if res == 0 {
error!("Accessibility must be enabled to make espanso work on MacOS."); error!("Accessibility must be enabled to make espanso work on MacOS.");
error!("Please allow espanso in the Security & Privacy panel, then restart espanso."); error!(
"Please allow espanso in the Security & Privacy panel, then restart espanso."
);
error!("For more information: https://espanso.org/install/mac/"); error!("For more information: https://espanso.org/install/mac/");
exit(1); exit(1);
} }
@ -69,9 +75,12 @@ impl MacContext {
if status_icon_target.exists() { if status_icon_target.exists() {
info!("Status icon already initialized, skipping."); info!("Status icon already initialized, skipping.");
}else { } else {
fs::write(&status_icon_target, STATUS_ICON_BINARY).unwrap_or_else(|e| { fs::write(&status_icon_target, STATUS_ICON_BINARY).unwrap_or_else(|e| {
error!("Error copying the Status Icon to the espanso data directory: {}", e); error!(
"Error copying the Status Icon to the espanso data directory: {}",
e
);
}); });
} }
@ -82,12 +91,9 @@ impl MacContext {
register_icon_click_callback(icon_click_callback); register_icon_click_callback(icon_click_callback);
register_context_menu_click_callback(context_menu_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 status_icon_path =
let show_icon = if config.show_icon { CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default();
1 let show_icon = if config.show_icon { 1 } else { 0 };
}else{
0
};
initialize(context_ptr, status_icon_path.as_ptr(), show_icon); initialize(context_ptr, status_icon_path.as_ptr(), show_icon);
} }
@ -155,8 +161,13 @@ impl super::Context for MacContext {
// Native bridge code // Native bridge code
extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, extern "C" fn keypress_callback(
event_type: i32, key_code: i32) { _self: *mut c_void,
raw_buffer: *const u8,
len: i32,
event_type: i32,
key_code: i32,
) {
unsafe { unsafe {
let _self = _self as *mut MacContext; let _self = _self as *mut MacContext;
@ -168,7 +179,8 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
return; return;
} }
if event_type == 0 { // Char event if event_type == 0 {
// Char event
// Convert the received buffer to a string // Convert the received buffer to a string
let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
let char_str = c_str.to_str(); let char_str = c_str.to_str();
@ -178,12 +190,13 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
Ok(char_str) => { Ok(char_str) => {
let event = Event::Key(KeyEvent::Char(char_str.to_owned())); let event = Event::Key(KeyEvent::Char(char_str.to_owned()));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
}, }
Err(e) => { Err(e) => {
error!("Unable to receive char: {}",e); error!("Unable to receive char: {}", e);
}, }
} }
}else if event_type == 1 { // Modifier event } else if event_type == 1 {
// Modifier event
let modifier: Option<KeyModifier> = match key_code { let modifier: Option<KeyModifier> = match key_code {
0x37 => Some(LEFT_META), 0x37 => Some(LEFT_META),
0x36 => Some(RIGHT_META), 0x36 => Some(RIGHT_META),
@ -194,24 +207,27 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
0x3B => Some(LEFT_CTRL), 0x3B => Some(LEFT_CTRL),
0x3E => Some(RIGHT_CTRL), 0x3E => Some(RIGHT_CTRL),
0x33 => Some(BACKSPACE), 0x33 => Some(BACKSPACE),
0x39 => Some(CAPS_LOCK),
_ => None, _ => None,
}; };
if let Some(modifier) = modifier { if let Some(modifier) = modifier {
let event = Event::Key(KeyEvent::Modifier(modifier)); let event = Event::Key(KeyEvent::Modifier(modifier));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
}else{ // Not one of the default modifiers, send an "other" event } else {
// Not one of the default modifiers, send an "other" event
let event = Event::Key(KeyEvent::Other); let event = Event::Key(KeyEvent::Other);
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
} }
}else{ // Other type of event } else {
// Other type of event
let event = Event::Key(KeyEvent::Other); let event = Event::Key(KeyEvent::Other);
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
} }
} }
} }
extern fn icon_click_callback(_self: *mut c_void) { extern "C" fn icon_click_callback(_self: *mut c_void) {
unsafe { unsafe {
let _self = _self as *mut MacContext; let _self = _self as *mut MacContext;
@ -220,7 +236,7 @@ extern fn icon_click_callback(_self: *mut c_void) {
} }
} }
extern fn context_menu_click_callback(_self: *mut c_void, id: i32) { extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) {
unsafe { unsafe {
let _self = _self as *mut MacContext; let _self = _self as *mut MacContext;

View File

@ -26,13 +26,13 @@ mod linux;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub(crate) mod macos; pub(crate) mod macos;
use std::sync::mpsc::Sender;
use crate::event::Event;
use std::path::PathBuf;
use std::fs::create_dir_all;
use std::sync::{Once, Arc};
use std::sync::atomic::AtomicBool;
use crate::config::Configs; 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 { pub trait Context {
fn eventloop(&self); fn eventloop(&self);
@ -40,25 +40,37 @@ pub trait Context {
// MAC IMPLEMENTATION // MAC IMPLEMENTATION
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> { pub fn new(
config: Configs,
send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
) -> Box<dyn Context> {
macos::MacContext::new(config, send_channel, is_injecting) macos::MacContext::new(config, send_channel, is_injecting)
} }
// LINUX IMPLEMENTATION // LINUX IMPLEMENTATION
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> { pub fn new(
config: Configs,
send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
) -> Box<dyn Context> {
linux::LinuxContext::new(config, send_channel, is_injecting) linux::LinuxContext::new(config, send_channel, is_injecting)
} }
// WINDOWS IMPLEMENTATION // WINDOWS IMPLEMENTATION
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> { pub fn new(
config: Configs,
send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
) -> Box<dyn Context> {
windows::WindowsContext::new(config, send_channel, is_injecting) windows::WindowsContext::new(config, send_channel, is_injecting)
} }
// espanso directories // espanso directories
static WARING_INIT : Once = Once::new(); static WARING_INIT: Once = Once::new();
pub fn get_data_dir() -> PathBuf { pub fn get_data_dir() -> PathBuf {
let data_dir = dirs::data_local_dir().expect("Can't obtain data_local_dir(), terminating."); let data_dir = dirs::data_local_dir().expect("Can't obtain data_local_dir(), terminating.");
@ -75,7 +87,10 @@ pub fn get_config_dir() -> PathBuf {
if let Some(parent) = exe_dir { if let Some(parent) = exe_dir {
let config_dir = parent.join(".espanso"); let config_dir = parent.join(".espanso");
if config_dir.exists() { if config_dir.exists() {
println!("PORTABLE MODE, using config folder: '{}'", config_dir.to_string_lossy()); println!(
"PORTABLE MODE, using config folder: '{}'",
config_dir.to_string_lossy()
);
return config_dir; return config_dir;
} }
} }
@ -110,7 +125,7 @@ pub fn get_config_dir() -> PathBuf {
espanso_dir espanso_dir
} }
const PACKAGES_FOLDER_NAME : &str = "packages"; const PACKAGES_FOLDER_NAME: &str = "packages";
pub fn get_package_dir() -> PathBuf { pub fn get_package_dir() -> PathBuf {
// Deprecated $HOME/.espanso/packages directory compatibility check // Deprecated $HOME/.espanso/packages directory compatibility check
@ -125,4 +140,4 @@ pub fn get_package_dir() -> PathBuf {
let package_dir = data_dir.join(PACKAGES_FOLDER_NAME); let package_dir = data_dir.join(PACKAGES_FOLDER_NAME);
create_dir_all(&package_dir).expect("Error creating espanso packages directory"); create_dir_all(&package_dir).expect("Error creating espanso packages directory");
package_dir package_dir
} }

View File

@ -17,21 +17,21 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::sync::mpsc::Sender;
use crate::bridge::windows::*; use crate::bridge::windows::*;
use crate::event::{Event, KeyEvent, KeyModifier, ActionType};
use crate::event::KeyModifier::*;
use std::ffi::c_void;
use std::{fs};
use widestring::{U16CString, U16CStr};
use log::{info, error, debug};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs; 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::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 BMP_BINARY: &[u8] = include_bytes!("../res/win/espanso.bmp");
const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico"); const ICO_BINARY: &[u8] = include_bytes!("../res/win/espanso.ico");
pub struct WindowsContext { pub struct WindowsContext {
send_channel: Sender<Event>, send_channel: Sender<Event>,
@ -39,31 +39,42 @@ pub struct WindowsContext {
} }
impl WindowsContext { impl WindowsContext {
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<WindowsContext> { pub fn new(
config: Configs,
send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
) -> Box<WindowsContext> {
// Initialize image resources // Initialize image resources
let espanso_dir = super::get_data_dir(); let espanso_dir = super::get_data_dir();
info!("Initializing Espanso resources in {}", espanso_dir.as_path().display()); info!(
"Initializing Espanso resources in {}",
espanso_dir.as_path().display()
);
let espanso_bmp_image = espanso_dir.join("espansoicon.bmp"); let espanso_bmp_image = espanso_dir.join("espansoicon.bmp");
if espanso_bmp_image.exists() { if espanso_bmp_image.exists() {
info!("BMP already initialized, skipping."); info!("BMP already initialized, skipping.");
}else { } else {
fs::write(&espanso_bmp_image, BMP_BINARY) fs::write(&espanso_bmp_image, BMP_BINARY).expect("Unable to write windows bmp file");
.expect("Unable to write windows bmp file");
info!("Extracted bmp icon to: {}", espanso_bmp_image.to_str().unwrap_or("error")); info!(
"Extracted bmp icon to: {}",
espanso_bmp_image.to_str().unwrap_or("error")
);
} }
let espanso_ico_image = espanso_dir.join("espanso.ico"); let espanso_ico_image = espanso_dir.join("espanso.ico");
if espanso_ico_image.exists() { if espanso_ico_image.exists() {
info!("ICO already initialized, skipping."); info!("ICO already initialized, skipping.");
}else { } else {
fs::write(&espanso_ico_image, ICO_BINARY) fs::write(&espanso_ico_image, ICO_BINARY).expect("Unable to write windows ico file");
.expect("Unable to write windows ico file");
info!("Extracted 'ico' icon to: {}", espanso_ico_image.to_str().unwrap_or("error")); info!(
"Extracted 'ico' icon to: {}",
espanso_ico_image.to_str().unwrap_or("error")
);
} }
let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default(); let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default();
@ -71,7 +82,7 @@ impl WindowsContext {
let send_channel = send_channel; let send_channel = send_channel;
let context = Box::new(WindowsContext{ let context = Box::new(WindowsContext {
send_channel, send_channel,
is_injecting, is_injecting,
}); });
@ -87,14 +98,15 @@ impl WindowsContext {
let ico_file_c = U16CString::from_str(ico_icon).unwrap(); let ico_file_c = U16CString::from_str(ico_icon).unwrap();
let bmp_file_c = U16CString::from_str(bmp_icon).unwrap(); let bmp_file_c = U16CString::from_str(bmp_icon).unwrap();
let show_icon = if config.show_icon { let show_icon = if config.show_icon { 1 } else { 0 };
1
}else{
0
};
// Initialize the windows // Initialize the windows
let res = initialize(context_ptr, ico_file_c.as_ptr(), bmp_file_c.as_ptr(), show_icon); let res = initialize(
context_ptr,
ico_file_c.as_ptr(),
bmp_file_c.as_ptr(),
show_icon,
);
if res != 1 { if res != 1 {
panic!("Can't initialize Windows context") panic!("Can't initialize Windows context")
} }
@ -114,8 +126,15 @@ impl super::Context for WindowsContext {
// Native bridge code // Native bridge code
extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32, extern "C" fn keypress_callback(
event_type: i32, key_code: i32, variant: i32, is_key_down: i32) { _self: *mut c_void,
raw_buffer: *const u16,
len: i32,
event_type: i32,
key_code: i32,
variant: i32,
is_key_down: i32,
) {
unsafe { unsafe {
let _self = _self as *mut WindowsContext; let _self = _self as *mut WindowsContext;
@ -127,8 +146,10 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32
return; return;
} }
if is_key_down != 0 { // KEY DOWN EVENT if event_type == 0 {
if event_type == 0 { // Char event // Char event
if is_key_down != 0 {
// KEY DOWN EVENT
// Convert the received buffer to a string // Convert the received buffer to a string
let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); let buffer = std::slice::from_raw_parts(raw_buffer, len as usize);
let c_string = U16CStr::from_slice_with_nul(buffer); let c_string = U16CStr::from_slice_with_nul(buffer);
@ -141,17 +162,19 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32
Ok(string) => { Ok(string) => {
let event = Event::Key(KeyEvent::Char(string)); let event = Event::Key(KeyEvent::Char(string));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
}, }
Err(e) => { Err(e) => {
error!("Unable to receive char: {}",e); error!("Unable to receive char: {}", e);
}, }
} }
}else{ } else {
error!("unable to decode widechar"); error!("unable to decode widechar");
} }
} }
}else{ // KEY UP event } else if event_type == 1 {
if event_type == 1 { // Modifier event // Modifier event
if is_key_down == 1 {
// Keyup event
let modifier: Option<KeyModifier> = match (key_code, variant) { let modifier: Option<KeyModifier> = match (key_code, variant) {
(0x5B, _) => Some(LEFT_META), (0x5B, _) => Some(LEFT_META),
(0x5C, _) => Some(RIGHT_META), (0x5C, _) => Some(RIGHT_META),
@ -161,27 +184,29 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32
(0x12, 2) => Some(RIGHT_ALT), (0x12, 2) => Some(RIGHT_ALT),
(0x11, 1) => Some(LEFT_CTRL), (0x11, 1) => Some(LEFT_CTRL),
(0x11, 2) => Some(RIGHT_CTRL), (0x11, 2) => Some(RIGHT_CTRL),
(0x08, _) => Some(BACKSPACE), (0x08, _) => Some(BACKSPACE),
(0x14, _) => Some(CAPS_LOCK),
_ => None, _ => None,
}; };
if let Some(modifier) = modifier { if let Some(modifier) = modifier {
let event = Event::Key(KeyEvent::Modifier(modifier)); let event = Event::Key(KeyEvent::Modifier(modifier));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
}else{ // Not one of the default modifiers, send an "other" event } else {
// Not one of the default modifiers, send an "other" event
let event = Event::Key(KeyEvent::Other); let event = Event::Key(KeyEvent::Other);
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
} }
}else{
// Other type of 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 fn icon_click_callback(_self: *mut c_void) { extern "C" fn icon_click_callback(_self: *mut c_void) {
unsafe { unsafe {
let _self = _self as *mut WindowsContext; let _self = _self as *mut WindowsContext;
@ -190,12 +215,11 @@ extern fn icon_click_callback(_self: *mut c_void) {
} }
} }
extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) {
extern fn context_menu_click_callback(_self: *mut c_void, id: i32) {
unsafe { unsafe {
let _self = _self as *mut WindowsContext; let _self = _self as *mut WindowsContext;
let event = Event::Action(ActionType::from(id)); let event = Event::Action(ActionType::from(id));
(*_self).send_channel.send(event).unwrap(); (*_self).send_channel.send(event).unwrap();
} }
} }

View File

@ -20,11 +20,17 @@
use std::path::Path; use std::path::Path;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn default_editor() -> String{ "/bin/nano".to_owned() } fn default_editor() -> String {
"/bin/nano".to_owned()
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn default_editor() -> String{ "/usr/bin/nano".to_owned() } fn default_editor() -> String {
"/usr/bin/nano".to_owned()
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn default_editor() -> String{ "C:\\Windows\\System32\\notepad.exe".to_owned() } fn default_editor() -> String {
"C:\\Windows\\System32\\notepad.exe".to_owned()
}
pub fn open_editor(file_path: &Path) -> bool { pub fn open_editor(file_path: &Path) -> bool {
use std::process::Command; use std::process::Command;
@ -34,18 +40,25 @@ pub fn open_editor(file_path: &Path) -> bool {
let visual_var = std::env::var_os("VISUAL"); let visual_var = std::env::var_os("VISUAL");
// Prioritize the editors specified by the environment variable, use the default one // Prioritize the editors specified by the environment variable, use the default one
let editor : String = if let Some(editor_var) = editor_var { let editor: String = if let Some(editor_var) = editor_var {
editor_var.to_string_lossy().to_string() editor_var.to_string_lossy().to_string()
}else if let Some(visual_var) = visual_var { } else if let Some(visual_var) = visual_var {
visual_var.to_string_lossy().to_string() visual_var.to_string_lossy().to_string()
}else{ } else {
default_editor() default_editor()
}; };
// Start the editor and wait for its termination // Start the editor and wait for its termination
let status = Command::new(&editor) let status = if cfg!(target_os = "windows") {
.arg(file_path) Command::new(&editor).arg(file_path).spawn()
.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 { if let Ok(mut child) = status {
// Wait for the user to edit the configuration // Wait for the user to edit the configuration
@ -53,11 +66,11 @@ pub fn open_editor(file_path: &Path) -> bool {
if let Ok(exit_status) = result { if let Ok(exit_status) = result {
exit_status.success() exit_status.success()
}else{ } else {
false false
} }
}else{ } else {
println!("Error: could not start editor at: {}", &editor); println!("Error: could not start editor at: {}", &editor);
false false
} }
} }

View File

@ -17,24 +17,31 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::matcher::{Match, MatchReceiver};
use crate::keyboard::KeyboardManager;
use crate::config::ConfigManager;
use crate::config::BackendType;
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use log::{info, warn, debug, error}; use crate::config::BackendType;
use crate::ui::{UIManager, MenuItem, MenuItemType}; use crate::config::ConfigManager;
use crate::event::{ActionEventReceiver, ActionType, SystemEventReceiver, SystemEvent}; use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
use crate::render::{Renderer, RenderResult}; 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::ui::{MenuItem, MenuItemType, UIManager};
use log::{debug, error, info, warn};
use regex::Regex;
use std::cell::RefCell; use std::cell::RefCell;
use std::process::exit; use std::process::exit;
use regex::{Regex};
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::Release; use std::sync::atomic::Ordering::Release;
use std::sync::Arc;
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, pub struct Engine<
U: UIManager, R: Renderer> { 'a,
S: KeyboardManager,
C: ClipboardManager,
M: ConfigManager<'a>,
U: UIManager,
R: Renderer,
> {
keyboard_manager: &'a S, keyboard_manager: &'a S,
clipboard_manager: &'a C, clipboard_manager: &'a C,
config_manager: &'a M, config_manager: &'a M,
@ -45,14 +52,27 @@ pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<
enabled: RefCell<bool>, enabled: RefCell<bool>,
} }
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> impl<
Engine<'a, S, C, M, U, R> { 'a,
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C, S: KeyboardManager,
config_manager: &'a M, ui_manager: &'a U, C: ClipboardManager,
renderer: &'a R, is_injecting: Arc<AtomicBool>) -> Engine<'a, S, C, M, U, R> { 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<AtomicBool>,
) -> Engine<'a, S, C, M, U, R> {
let enabled = RefCell::new(true); let enabled = RefCell::new(true);
Engine{keyboard_manager, Engine {
keyboard_manager,
clipboard_manager, clipboard_manager,
config_manager, config_manager,
ui_manager, ui_manager,
@ -66,24 +86,32 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
let mut menu = Vec::new(); let mut menu = Vec::new();
let enabled = self.enabled.borrow(); let enabled = self.enabled.borrow();
let toggle_text = if *enabled { let toggle_text = if *enabled { "Disable" } else { "Enable" }.to_owned();
"Disable" menu.push(MenuItem {
}else{
"Enable"
}.to_owned();
menu.push(MenuItem{
item_type: MenuItemType::Button, item_type: MenuItemType::Button,
item_name: toggle_text, item_name: toggle_text,
item_id: ActionType::Toggle as i32, item_id: ActionType::Toggle as i32,
}); });
menu.push(MenuItem{ 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_type: MenuItemType::Separator,
item_name: "".to_owned(), item_name: "".to_owned(),
item_id: 999, item_id: 999,
}); });
menu.push(MenuItem{ menu.push(MenuItem {
item_type: MenuItemType::Button, item_type: MenuItemType::Button,
item_name: "Exit espanso".to_owned(), item_name: "Exit espanso".to_owned(),
item_id: ActionType::Exit as i32, item_id: ActionType::Exit as i32,
@ -97,10 +125,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
// clipboard content in order to restore it later. // clipboard content in order to restore it later.
if self.config_manager.default_config().preserve_clipboard { if self.config_manager.default_config().preserve_clipboard {
match self.clipboard_manager.get_clipboard() { match self.clipboard_manager.get_clipboard() {
Some(clipboard) => {Some(clipboard)}, Some(clipboard) => Some(clipboard),
None => {None}, None => None,
} }
}else { } else {
None None
} }
} }
@ -110,9 +138,15 @@ lazy_static! {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap(); static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
} }
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> impl<
MatchReceiver for Engine<'a, S, C, M, U, R>{ '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<char>, trigger_offset: usize) { fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
let config = self.config_manager.active_config(); let config = self.config_manager.active_config();
@ -125,23 +159,26 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
let char_count = if trailing_separator.is_none() { let char_count = if trailing_separator.is_none() {
m.triggers[trigger_offset].chars().count() as i32 m.triggers[trigger_offset].chars().count() as i32
}else{ } else {
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
}; };
self.keyboard_manager.delete_string(&config, char_count); self.keyboard_manager.delete_string(&config, char_count);
let mut previous_clipboard_content : Option<String> = None; let mut previous_clipboard_content: Option<String> = None;
let rendered = self.renderer.render_match(m, trigger_offset, config, vec![]); let rendered = self
.renderer
.render_match(m, trigger_offset, config, vec![]);
match rendered { match rendered {
RenderResult::Text(mut target_string) => { RenderResult::Text(mut target_string) => {
// If a trailing separator was counted in the match, add it back to the 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 let Some(trailing_separator) = trailing_separator {
if trailing_separator == '\r' { // If the trailing separator is a carriage return, if trailing_separator == '\r' {
target_string.push('\n'); // convert it to new line // If the trailing separator is a carriage return,
}else{ target_string.push('\n'); // convert it to new line
} else {
target_string.push(trailing_separator); target_string.push(trailing_separator);
} }
} }
@ -164,26 +201,28 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
// Subtract also 3, equal to the number of chars of the placeholder "$|$" // Subtract also 3, equal to the number of chars of the placeholder "$|$"
let moves = (total_size - char_index - 3) as i32; let moves = (total_size - char_index - 3) as i32;
Some(moves) Some(moves)
}else{ } else {
None None
}; };
let backend = if m.force_clipboard { let backend = if m.force_clipboard {
&BackendType::Clipboard &BackendType::Clipboard
}else if config.backend == BackendType::Auto { } else if config.backend == BackendType::Auto {
if cfg!(target_os = "linux") { if cfg!(target_os = "linux") {
let all_ascii = target_string.chars().all(|c| c.is_ascii()); let all_ascii = target_string.chars().all(|c| c.is_ascii());
if all_ascii { if all_ascii {
debug!("All elements of the replacement are ascii, using Inject backend"); debug!(
"All elements of the replacement are ascii, using Inject backend"
);
&BackendType::Inject &BackendType::Inject
}else{ } else {
debug!("There are non-ascii characters, using Clipboard backend"); debug!("There are non-ascii characters, using Clipboard backend");
&BackendType::Clipboard &BackendType::Clipboard
} }
}else{ } else {
&BackendType::Inject &BackendType::Inject
} }
}else{ } else {
&config.backend &config.backend
}; };
@ -199,15 +238,16 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
self.keyboard_manager.send_string(&config, split); self.keyboard_manager.send_string(&config, split);
} }
}, }
BackendType::Clipboard => { BackendType::Clipboard => {
// If the preserve_clipboard option is enabled, save the current // If the preserve_clipboard option is enabled, save the current
// clipboard content to restore it later. // clipboard content to restore it later.
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); previous_clipboard_content =
self.return_content_if_preserve_clipboard_is_enabled();
self.clipboard_manager.set_clipboard(&target_string); self.clipboard_manager.set_clipboard(&target_string);
self.keyboard_manager.trigger_paste(&config); self.keyboard_manager.trigger_paste(&config);
}, }
_ => { _ => {
error!("Unsupported backend type evaluation."); error!("Unsupported backend type evaluation.");
return; return;
@ -218,7 +258,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
// Simulate left arrow key presses to bring the cursor into the desired position // Simulate left arrow key presses to bring the cursor into the desired position
self.keyboard_manager.move_cursor_left(&config, moves); self.keyboard_manager.move_cursor_left(&config, moves);
} }
}, }
RenderResult::Image(image_path) => { RenderResult::Image(image_path) => {
// If the preserve_clipboard option is enabled, save the current // If the preserve_clipboard option is enabled, save the current
// clipboard content to restore it later. // clipboard content to restore it later.
@ -226,19 +266,22 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
self.clipboard_manager.set_clipboard_image(&image_path); self.clipboard_manager.set_clipboard_image(&image_path);
self.keyboard_manager.trigger_paste(&config); self.keyboard_manager.trigger_paste(&config);
}, }
RenderResult::Error => { RenderResult::Error => {
error!("Could not render match: {}", m.triggers[trigger_offset]); error!("Could not render match: {}", m.triggers[trigger_offset]);
}, }
} }
// Restore previous clipboard content // Restore previous clipboard content
if let Some(previous_clipboard_content) = previous_clipboard_content { if let Some(previous_clipboard_content) = previous_clipboard_content {
// Sometimes an expansion gets overwritten before pasting by the previous content // Sometimes an expansion gets overwritten before pasting by the previous content
// A delay is needed to mitigate the problem // A delay is needed to mitigate the problem
std::thread::sleep(std::time::Duration::from_millis(config.restore_clipboard_delay as u64)); std::thread::sleep(std::time::Duration::from_millis(
config.restore_clipboard_delay as u64,
));
self.clipboard_manager.set_clipboard(&previous_clipboard_content); self.clipboard_manager
.set_clipboard(&previous_clipboard_content);
} }
// Re-allow espanso to interpret actions // Re-allow espanso to interpret actions
@ -248,7 +291,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
fn on_enable_update(&self, status: bool) { fn on_enable_update(&self, status: bool) {
let message = if status { let message = if status {
"espanso enabled" "espanso enabled"
}else{ } else {
"espanso disabled" "espanso disabled"
}; };
@ -280,13 +323,13 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
let previous_clipboard = self.clipboard_manager.get_clipboard(); let previous_clipboard = self.clipboard_manager.get_clipboard();
// Sleep for a while, giving time to effectively copy the text // Sleep for a while, giving time to effectively copy the text
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
// Trigger a copy shortcut to transfer the content of the selection to the clipboard // Trigger a copy shortcut to transfer the content of the selection to the clipboard
self.keyboard_manager.trigger_copy(&config); self.keyboard_manager.trigger_copy(&config);
// Sleep for a while, giving time to effectively copy the text // Sleep for a while, giving time to effectively copy the text
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
// Then get the text from the clipboard and render the match output // Then get the text from the clipboard and render the match output
let clipboard = self.clipboard_manager.get_clipboard(); let clipboard = self.clipboard_manager.get_clipboard();
@ -295,7 +338,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
// Don't expand empty clipboards, as usually they are the result of an empty passive selection // Don't expand empty clipboards, as usually they are the result of an empty passive selection
if clipboard.trim().is_empty() { if clipboard.trim().is_empty() {
info!("Avoiding passive expansion, as the user didn't select anything"); info!("Avoiding passive expansion, as the user didn't select anything");
}else{ } else {
if let Some(previous_content) = previous_clipboard { if let Some(previous_content) = previous_clipboard {
// Because of issue #213, we need to make sure the user selected something. // Because of issue #213, we need to make sure the user selected something.
if clipboard == previous_content { if clipboard == previous_content {
@ -303,8 +346,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
} else { } else {
info!("Passive mode activated"); info!("Passive mode activated");
let rendered = self.renderer.render_passive(&clipboard, let rendered = self.renderer.render_passive(&clipboard, &config);
&config);
match rendered { match rendered {
RenderResult::Text(payload) => { RenderResult::Text(payload) => {
@ -313,10 +355,8 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
self.keyboard_manager.trigger_paste(&config); self.keyboard_manager.trigger_paste(&config);
}, }
_ => { _ => warn!("Cannot expand passive match"),
warn!("Cannot expand passive match")
},
} }
} }
} }
@ -328,27 +368,50 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
} }
} }
impl <'a, S: KeyboardManager, C: ClipboardManager, impl<
M: ConfigManager<'a>, U: UIManager, R: Renderer> ActionEventReceiver for Engine<'a, S, C, M, U, R>{ '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) { fn on_action_event(&self, e: ActionType) {
let config = self.config_manager.default_config();
match e { match e {
ActionType::IconClick => { ActionType::IconClick => {
self.ui_manager.show_menu(self.build_menu()); self.ui_manager.show_menu(self.build_menu());
}, }
ActionType::Exit => { ActionType::ExitWorker => {
info!("Terminating espanso."); info!("terminating worker process");
self.ui_manager.cleanup(); self.ui_manager.cleanup();
exit(0); 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, impl<
M: ConfigManager<'a>, U: UIManager, R: Renderer> SystemEventReceiver for Engine<'a, S, C, M, U, R>{ '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) { fn on_system_event(&self, e: SystemEvent) {
match e { match e {
// MacOS specific // MacOS specific
@ -359,10 +422,16 @@ impl <'a, S: KeyboardManager, C: ClipboardManager,
if config.secure_input_notification && config.show_notifications { 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); self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
} }
}, }
SystemEvent::SecureInputDisabled => { SystemEvent::SecureInputDisabled => {
info!("SecureInput has been disabled."); info!("SecureInput has been disabled.");
}, }
SystemEvent::NotifyRequest(message) => {
let config = self.config_manager.default_config();
if config.show_notifications {
self.ui_manager.notify(&message);
}
}
} }
} }
} }

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::event::{KeyEventReceiver, ActionEventReceiver, Event, SystemEventReceiver}; use crate::event::{ActionEventReceiver, Event, KeyEventReceiver, SystemEventReceiver};
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
pub trait EventManager { pub trait EventManager {
@ -32,37 +32,44 @@ pub struct DefaultEventManager<'a> {
} }
impl<'a> DefaultEventManager<'a> { impl<'a> DefaultEventManager<'a> {
pub fn new(receive_channel: Receiver<Event>, key_receivers: Vec<&'a dyn KeyEventReceiver>, pub fn new(
action_receivers: Vec<&'a dyn ActionEventReceiver>, receive_channel: Receiver<Event>,
system_receivers: Vec<&'a dyn SystemEventReceiver>) -> DefaultEventManager<'a> { key_receivers: Vec<&'a dyn KeyEventReceiver>,
action_receivers: Vec<&'a dyn ActionEventReceiver>,
system_receivers: Vec<&'a dyn SystemEventReceiver>,
) -> DefaultEventManager<'a> {
DefaultEventManager { DefaultEventManager {
receive_channel, receive_channel,
key_receivers, key_receivers,
action_receivers, action_receivers,
system_receivers system_receivers,
} }
} }
} }
impl <'a> EventManager for DefaultEventManager<'a> { impl<'a> EventManager for DefaultEventManager<'a> {
fn eventloop(&self) { fn eventloop(&self) {
loop { loop {
match self.receive_channel.recv() { match self.receive_channel.recv() {
Ok(event) => { Ok(event) => match event {
match event { Event::Key(key_event) => {
Event::Key(key_event) => { self.key_receivers
self.key_receivers.iter().for_each(move |&receiver| receiver.on_key_event(key_event.clone())); .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::Action(action_event) => {
} self.action_receivers
Event::System(system_event) => { .iter()
self.system_receivers.iter().for_each(move |&receiver| receiver.on_system_event(system_event.clone())); .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), Err(e) => panic!("Broken event channel {}", e),
} }
} }
} }
} }

View File

@ -19,7 +19,7 @@
pub(crate) mod manager; pub(crate) mod manager;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -37,6 +37,8 @@ pub enum ActionType {
IconClick = 3, IconClick = 3,
Enable = 4, Enable = 4,
Disable = 5, Disable = 5,
RestartWorker = 6,
ExitWorker = 7,
} }
impl From<i32> for ActionType { impl From<i32> for ActionType {
@ -47,6 +49,8 @@ impl From<i32> for ActionType {
3 => ActionType::IconClick, 3 => ActionType::IconClick,
4 => ActionType::Enable, 4 => ActionType::Enable,
5 => ActionType::Disable, 5 => ActionType::Disable,
6 => ActionType::RestartWorker,
7 => ActionType::ExitWorker,
_ => ActionType::Noop, _ => ActionType::Noop,
} }
} }
@ -56,7 +60,7 @@ impl From<i32> for ActionType {
pub enum KeyEvent { pub enum KeyEvent {
Char(String), Char(String),
Modifier(KeyModifier), Modifier(KeyModifier),
Other Other,
} }
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
@ -79,6 +83,9 @@ pub enum KeyModifier {
RIGHT_META, RIGHT_META,
LEFT_SHIFT, LEFT_SHIFT,
RIGHT_SHIFT, RIGHT_SHIFT,
// Special cases, should not be used in config
CAPS_LOCK,
} }
impl KeyModifier { impl KeyModifier {
@ -92,44 +99,24 @@ impl KeyModifier {
match config { match config {
KeyModifier::CTRL => { KeyModifier::CTRL => {
current == &LEFT_CTRL || current == &RIGHT_CTRL || current == &CTRL current == &LEFT_CTRL || current == &RIGHT_CTRL || current == &CTRL
}, }
KeyModifier::SHIFT => { KeyModifier::SHIFT => {
current == &LEFT_SHIFT || current == &RIGHT_SHIFT || current == &SHIFT current == &LEFT_SHIFT || current == &RIGHT_SHIFT || current == &SHIFT
}, }
KeyModifier::ALT => { KeyModifier::ALT => current == &LEFT_ALT || current == &RIGHT_ALT || current == &ALT,
current == &LEFT_ALT || current == &RIGHT_ALT || current == &ALT
},
KeyModifier::META => { KeyModifier::META => {
current == &LEFT_META || current == &RIGHT_META || current == &META current == &LEFT_META || current == &RIGHT_META || current == &META
}, }
KeyModifier::BACKSPACE => { KeyModifier::BACKSPACE => current == &BACKSPACE,
current == &BACKSPACE KeyModifier::LEFT_CTRL => current == &LEFT_CTRL,
}, KeyModifier::RIGHT_CTRL => current == &RIGHT_CTRL,
KeyModifier::LEFT_CTRL => { KeyModifier::LEFT_ALT => current == &LEFT_ALT,
current == &LEFT_CTRL KeyModifier::RIGHT_ALT => current == &RIGHT_ALT,
}, KeyModifier::LEFT_META => current == &LEFT_META,
KeyModifier::RIGHT_CTRL => { KeyModifier::RIGHT_META => current == &RIGHT_META,
current == &RIGHT_CTRL KeyModifier::LEFT_SHIFT => current == &LEFT_SHIFT,
}, KeyModifier::RIGHT_SHIFT => current == &RIGHT_SHIFT,
KeyModifier::LEFT_ALT => { _ => false,
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},
} }
} }
} }
@ -138,8 +125,11 @@ impl KeyModifier {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SystemEvent { pub enum SystemEvent {
// MacOS specific // MacOS specific
SecureInputEnabled(String, String), // AppName, App Path SecureInputEnabled(String, String), // AppName, App Path
SecureInputDisabled, SecureInputDisabled,
// Notification
NotifyRequest(String),
} }
// Receivers // Receivers
@ -160,8 +150,8 @@ pub trait SystemEventReceiver {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use super::KeyModifier::*; use super::KeyModifier::*;
use super::*;
#[test] #[test]
fn test_shallow_equals_ctrl() { fn test_shallow_equals_ctrl() {
@ -215,4 +205,4 @@ mod tests {
assert!(!KeyModifier::shallow_equals(&OFF, &META)); assert!(!KeyModifier::shallow_equals(&OFF, &META));
assert!(!KeyModifier::shallow_equals(&OFF, &SHIFT)); assert!(!KeyModifier::shallow_equals(&OFF, &SHIFT));
} }
} }

View File

@ -17,8 +17,8 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde_yaml::{Mapping};
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use serde_yaml::Mapping;
pub struct ClipboardExtension { pub struct ClipboardExtension {
clipboard_manager: Box<dyn ClipboardManager>, clipboard_manager: Box<dyn ClipboardManager>,
@ -26,9 +26,7 @@ pub struct ClipboardExtension {
impl ClipboardExtension { impl ClipboardExtension {
pub fn new(clipboard_manager: Box<dyn ClipboardManager>) -> ClipboardExtension { pub fn new(clipboard_manager: Box<dyn ClipboardManager>) -> ClipboardExtension {
ClipboardExtension{ ClipboardExtension { clipboard_manager }
clipboard_manager
}
} }
} }
@ -40,4 +38,4 @@ impl super::Extension for ClipboardExtension {
fn calculate(&self, _: &Mapping, _: &Vec<String>) -> Option<String> { fn calculate(&self, _: &Mapping, _: &Vec<String>) -> Option<String> {
self.clipboard_manager.get_clipboard() self.clipboard_manager.get_clipboard()
} }
} }

View File

@ -17,14 +17,14 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde_yaml::{Mapping, Value};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use serde_yaml::{Mapping, Value};
pub struct DateExtension {} pub struct DateExtension {}
impl DateExtension { impl DateExtension {
pub fn new() -> DateExtension { pub fn new() -> DateExtension {
DateExtension{} DateExtension {}
} }
} }
@ -40,10 +40,10 @@ impl super::Extension for DateExtension {
let date = if let Some(format) = format { let date = if let Some(format) = format {
now.format(format.as_str().unwrap()).to_string() now.format(format.as_str().unwrap()).to_string()
}else{ } else {
now.to_rfc2822() now.to_rfc2822()
}; };
Some(date) Some(date)
} }
} }

View File

@ -23,7 +23,7 @@ pub struct DummyExtension {}
impl DummyExtension { impl DummyExtension {
pub fn new() -> DummyExtension { pub fn new() -> DummyExtension {
DummyExtension{} DummyExtension {}
} }
} }
@ -37,8 +37,8 @@ impl super::Extension for DummyExtension {
if let Some(echo) = echo { if let Some(echo) = echo {
Some(echo.as_str().unwrap_or_default().to_owned()) Some(echo.as_str().unwrap_or_default().to_owned())
}else{ } else {
None None
} }
} }
} }

View File

@ -17,22 +17,22 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde_yaml::Mapping;
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use serde_yaml::Mapping;
mod date;
mod shell;
mod script;
mod random;
mod clipboard; mod clipboard;
mod date;
pub mod dummy; pub mod dummy;
mod random;
mod script;
mod shell;
pub trait Extension { pub trait Extension {
fn name(&self) -> String; fn name(&self) -> String;
fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>; fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>;
} }
pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<dyn Extension>>{ pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<dyn Extension>> {
vec![ vec![
Box::new(date::DateExtension::new()), Box::new(date::DateExtension::new()),
Box::new(shell::ShellExtension::new()), Box::new(shell::ShellExtension::new()),
@ -41,4 +41,4 @@ pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<d
Box::new(dummy::DummyExtension::new()), Box::new(dummy::DummyExtension::new()),
Box::new(clipboard::ClipboardExtension::new(clipboard_manager)), Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
] ]
} }

View File

@ -17,15 +17,15 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde_yaml::{Mapping, Value}; use log::{error, warn};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use log::{warn, error}; use serde_yaml::{Mapping, Value};
pub struct RandomExtension {} pub struct RandomExtension {}
impl RandomExtension { impl RandomExtension {
pub fn new() -> RandomExtension { pub fn new() -> RandomExtension {
RandomExtension{} RandomExtension {}
} }
} }
@ -38,13 +38,14 @@ impl super::Extension for RandomExtension {
let choices = params.get(&Value::from("choices")); let choices = params.get(&Value::from("choices"));
if choices.is_none() { if choices.is_none() {
warn!("No 'choices' parameter specified for random variable"); warn!("No 'choices' parameter specified for random variable");
return None return None;
} }
let choices = choices.unwrap().as_sequence(); let choices = choices.unwrap().as_sequence();
if let Some(choices) = choices { if let Some(choices) = choices {
let str_choices = choices.iter().map(|arg| { let str_choices = choices
arg.as_str().unwrap_or_default().to_string() .iter()
}).collect::<Vec<String>>(); .map(|arg| arg.as_str().unwrap_or_default().to_string())
.collect::<Vec<String>>();
// Select a random choice between the possibilities // Select a random choice between the possibilities
let choice = str_choices.choose(&mut rand::thread_rng()); let choice = str_choices.choose(&mut rand::thread_rng());
@ -54,14 +55,13 @@ impl super::Extension for RandomExtension {
// Render arguments // Render arguments
let output = crate::render::utils::render_args(output, args); let output = crate::render::utils::render_args(output, args);
return Some(output) return Some(output);
}, }
None => { None => {
error!("Could not select a random choice."); error!("Could not select a random choice.");
return None return None;
}, }
} }
} }
error!("choices array have an invalid format '{:?}'", choices); error!("choices array have an invalid format '{:?}'", choices);
@ -77,11 +77,7 @@ mod tests {
#[test] #[test]
fn test_random_basic() { fn test_random_basic() {
let mut params = Mapping::new(); let mut params = Mapping::new();
let choices = vec!( let choices = vec!["first", "second", "third"];
"first",
"second",
"third",
);
params.insert(Value::from("choices"), Value::from(choices.clone())); params.insert(Value::from("choices"), Value::from(choices.clone()));
let extension = RandomExtension::new(); let extension = RandomExtension::new();
@ -97,11 +93,7 @@ mod tests {
#[test] #[test]
fn test_random_with_args() { fn test_random_with_args() {
let mut params = Mapping::new(); let mut params = Mapping::new();
let choices = vec!( let choices = vec!["first $0$", "second $0$", "$0$ third"];
"first $0$",
"second $0$",
"$0$ third",
);
params.insert(Value::from("choices"), Value::from(choices.clone())); params.insert(Value::from("choices"), Value::from(choices.clone()));
let extension = RandomExtension::new(); let extension = RandomExtension::new();
@ -111,12 +103,8 @@ mod tests {
let output = output.unwrap(); let output = output.unwrap();
let rendered_choices = vec!( let rendered_choices = vec!["first test", "second test", "test third"];
"first test",
"second test",
"test third",
);
assert!(rendered_choices.iter().any(|x| x == &output)); assert!(rendered_choices.iter().any(|x| x == &output));
} }
} }

View File

@ -17,15 +17,16 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use log::{error, warn};
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use log::{warn, error};
pub struct ScriptExtension {} pub struct ScriptExtension {}
impl ScriptExtension { impl ScriptExtension {
pub fn new() -> ScriptExtension { pub fn new() -> ScriptExtension {
ScriptExtension{} ScriptExtension {}
} }
} }
@ -38,41 +39,66 @@ impl super::Extension for ScriptExtension {
let args = params.get(&Value::from("args")); let args = params.get(&Value::from("args"));
if args.is_none() { if args.is_none() {
warn!("No 'args' parameter specified for script variable"); warn!("No 'args' parameter specified for script variable");
return None return None;
} }
let args = args.unwrap().as_sequence(); let args = args.unwrap().as_sequence();
if let Some(args) = args { if let Some(args) = args {
let mut str_args = args.iter().map(|arg| { let mut str_args = args
arg.as_str().unwrap_or_default().to_string() .iter()
}).collect::<Vec<String>>(); .map(|arg| arg.as_str().unwrap_or_default().to_string())
.collect::<Vec<String>>();
// The user has to enable argument concatenation explicitly // The user has to enable argument concatenation explicitly
let inject_args = params.get(&Value::from("inject_args")) let inject_args = params
.unwrap_or(&Value::from(false)).as_bool().unwrap_or(false); .get(&Value::from("inject_args"))
.unwrap_or(&Value::from(false))
.as_bool()
.unwrap_or(false);
if inject_args { if inject_args {
str_args.extend(user_args.clone()); str_args.extend(user_args.clone());
} }
// Replace %HOME% with current user home directory to
// create cross-platform paths. See issue #265
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());
}
// 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 output = if str_args.len() > 1 { let output = if str_args.len() > 1 {
Command::new(&str_args[0]) Command::new(&str_args[0]).args(&str_args[1..]).output()
.args(&str_args[1..]) } else {
.output() Command::new(&str_args[0]).output()
}else{
Command::new(&str_args[0])
.output()
}; };
println!("{:?}", output);
match output { match output {
Ok(output) => { Ok(output) => {
let output_str = String::from_utf8_lossy(output.stdout.as_slice()); let output_str = String::from_utf8_lossy(output.stdout.as_slice());
let error_str = String::from_utf8_lossy(output.stderr.as_slice());
let error_str = error_str.to_string();
let error_str = error_str.trim();
return Some(output_str.into_owned()) // Print stderror if present
}, if !error_str.is_empty() {
warn!("Script command reported error: \n{}", error_str);
}
return Some(output_str.into_owned());
}
Err(e) => { Err(e) => {
error!("Could not execute script '{:?}', error: {}", args, e); error!("Could not execute script '{:?}', error: {}", args, e);
return None return None;
}, }
} }
} }
@ -90,7 +116,10 @@ mod tests {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
fn test_script_basic() { fn test_script_basic() {
let mut params = Mapping::new(); let mut params = Mapping::new();
params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"])); params.insert(
Value::from("args"),
Value::from(vec!["echo", "hello world"]),
);
let extension = ScriptExtension::new(); let extension = ScriptExtension::new();
let output = extension.calculate(&params, &vec![]); let output = extension.calculate(&params, &vec![]);
@ -103,7 +132,10 @@ mod tests {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
fn test_script_inject_args_off() { fn test_script_inject_args_off() {
let mut params = Mapping::new(); let mut params = Mapping::new();
params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"])); params.insert(
Value::from("args"),
Value::from(vec!["echo", "hello world"]),
);
let extension = ScriptExtension::new(); let extension = ScriptExtension::new();
let output = extension.calculate(&params, &vec!["jon".to_owned()]); let output = extension.calculate(&params, &vec!["jon".to_owned()]);
@ -116,7 +148,10 @@ mod tests {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
fn test_script_inject_args_on() { fn test_script_inject_args_on() {
let mut params = Mapping::new(); let mut params = Mapping::new();
params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"])); params.insert(
Value::from("args"),
Value::from(vec!["echo", "hello world"]),
);
params.insert(Value::from("inject_args"), Value::from(true)); params.insert(Value::from("inject_args"), Value::from(true));
let extension = ScriptExtension::new(); let extension = ScriptExtension::new();
@ -125,4 +160,4 @@ mod tests {
assert!(output.is_some()); assert!(output.is_some());
assert_eq!(output.unwrap(), "hello world jon\n"); assert_eq!(output.unwrap(), "hello world jon\n");
} }
} }

View File

@ -17,24 +17,71 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use log::{error, warn};
use regex::{Captures, Regex};
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use std::process::Command; use std::process::{Command, Output};
use log::{warn, error};
use regex::{Regex, Captures};
lazy_static! { lazy_static! {
static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") { static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") {
Regex::new("%(?P<pos>\\d+)").unwrap() Regex::new("%(?P<pos>\\d+)").unwrap()
}else{ } else {
Regex::new("\\$(?P<pos>\\d+)").unwrap() Regex::new("\\$(?P<pos>\\d+)").unwrap()
}; };
} }
pub enum Shell {
Cmd,
Powershell,
WSL,
Bash,
Sh,
}
impl Shell {
fn execute_cmd(&self, cmd: &str) -> std::io::Result<Output> {
match self {
Shell::Cmd => Command::new("cmd").args(&["/C", &cmd]).output(),
Shell::Powershell => Command::new("powershell")
.args(&["-Command", &cmd])
.output(),
Shell::WSL => Command::new("wsl").args(&["bash", "-c", &cmd]).output(),
Shell::Bash => Command::new("bash").args(&["-c", &cmd]).output(),
Shell::Sh => Command::new("sh").args(&["-c", &cmd]).output(),
}
}
fn from_string(shell: &str) -> Option<Shell> {
match shell {
"cmd" => Some(Shell::Cmd),
"powershell" => Some(Shell::Powershell),
"wsl" => Some(Shell::WSL),
"bash" => Some(Shell::Bash),
"sh" => Some(Shell::Sh),
_ => None,
}
}
}
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 {} pub struct ShellExtension {}
impl ShellExtension { impl ShellExtension {
pub fn new() -> ShellExtension { pub fn new() -> ShellExtension {
ShellExtension{} ShellExtension {}
} }
} }
@ -47,36 +94,52 @@ impl super::Extension for ShellExtension {
let cmd = params.get(&Value::from("cmd")); let cmd = params.get(&Value::from("cmd"));
if cmd.is_none() { if cmd.is_none() {
warn!("No 'cmd' parameter specified for shell variable"); warn!("No 'cmd' parameter specified for shell variable");
return None return None;
} }
let cmd = cmd.unwrap().as_str().unwrap(); let cmd = cmd.unwrap().as_str().unwrap();
// Render positional parameters in args // Render positional parameters in args
let cmd = POS_ARG_REGEX.replace_all(&cmd, |caps: &Captures| { let cmd = POS_ARG_REGEX
let position_str = caps.name("pos").unwrap().as_str(); .replace_all(&cmd, |caps: &Captures| {
let position = position_str.parse::<i32>().unwrap_or(-1); let position_str = caps.name("pos").unwrap().as_str();
if position >= 0 && position < args.len() as i32 { let position = position_str.parse::<i32>().unwrap_or(-1);
args[position as usize].to_owned() if position >= 0 && position < args.len() as i32 {
}else{ args[position as usize].to_owned()
"".to_owned() } else {
} "".to_owned()
}).to_string(); }
})
.to_string();
let output = if cfg!(target_os = "windows") { let shell_param = params.get(&Value::from("shell"));
Command::new("cmd") let shell = if let Some(shell_param) = shell_param {
.args(&["/C", &cmd]) let shell_param = shell_param.as_str().expect("invalid shell parameter");
.output() let shell = Shell::from_string(shell_param);
if shell.is_none() {
error!("Invalid shell parameter, please select a valid one.");
return None;
}
shell.unwrap()
} else { } else {
Command::new("sh") Shell::default()
.arg("-c")
.arg(&cmd)
.output()
}; };
let output = shell.execute_cmd(&cmd);
match output { match output {
Ok(output) => { Ok(output) => {
let output_str = String::from_utf8_lossy(output.stdout.as_slice()); let output_str = String::from_utf8_lossy(output.stdout.as_slice());
let mut output_str = output_str.into_owned(); 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);
}
// If specified, trim the output // If specified, trim the output
let trim_opt = params.get(&Value::from("trim")); let trim_opt = params.get(&Value::from("trim"));
@ -90,11 +153,11 @@ impl super::Extension for ShellExtension {
} }
Some(output_str) Some(output_str)
}, }
Err(e) => { Err(e) => {
error!("Could not execute cmd '{}', error: {}", cmd, e); error!("Could not execute cmd '{}', error: {}", cmd, e);
None None
}, }
} }
} }
} }
@ -107,7 +170,7 @@ mod tests {
#[test] #[test]
fn test_shell_basic() { fn test_shell_basic() {
let mut params = Mapping::new(); let mut params = Mapping::new();
params.insert(Value::from("cmd"), Value::from("echo hello world")); params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
let output = extension.calculate(&params, &vec![]); let output = extension.calculate(&params, &vec![]);
@ -116,7 +179,7 @@ mod tests {
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
assert_eq!(output.unwrap(), "hello world\r\n"); assert_eq!(output.unwrap(), "hello world\r\n");
}else{ } else {
assert_eq!(output.unwrap(), "hello world\n"); assert_eq!(output.unwrap(), "hello world\n");
} }
} }
@ -124,7 +187,7 @@ mod tests {
#[test] #[test]
fn test_shell_trimmed() { fn test_shell_trimmed() {
let mut params = Mapping::new(); let mut params = Mapping::new();
params.insert(Value::from("cmd"), Value::from("echo hello world")); params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
params.insert(Value::from("trim"), Value::from(true)); params.insert(Value::from("trim"), Value::from(true));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
@ -137,11 +200,10 @@ mod tests {
#[test] #[test]
fn test_shell_trimmed_2() { fn test_shell_trimmed_2() {
let mut params = Mapping::new(); let mut params = Mapping::new();
if cfg!(target_os = "windows") { params.insert(
params.insert(Value::from("cmd"), Value::from("echo hello world ")); Value::from("cmd"),
}else{ Value::from("echo \" hello world \""),
params.insert(Value::from("cmd"), Value::from("echo \" hello world \"")); );
}
params.insert(Value::from("trim"), Value::from(true)); params.insert(Value::from("trim"), Value::from(true));
@ -155,7 +217,7 @@ mod tests {
#[test] #[test]
fn test_shell_trimmed_malformed() { fn test_shell_trimmed_malformed() {
let mut params = Mapping::new(); let mut params = Mapping::new();
params.insert(Value::from("cmd"), Value::from("echo hello world")); params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
params.insert(Value::from("trim"), Value::from("error")); params.insert(Value::from("trim"), Value::from("error"));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
@ -164,7 +226,7 @@ mod tests {
assert!(output.is_some()); assert!(output.is_some());
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
assert_eq!(output.unwrap(), "hello world\r\n"); assert_eq!(output.unwrap(), "hello world\r\n");
}else{ } else {
assert_eq!(output.unwrap(), "hello world\n"); assert_eq!(output.unwrap(), "hello world\n");
} }
} }
@ -210,4 +272,4 @@ mod tests {
assert_eq!(output.unwrap(), "hello\r\n"); assert_eq!(output.unwrap(), "hello\r\n");
} }
} }

View File

@ -17,14 +17,13 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::ffi::CString;
use crate::bridge::linux::*;
use super::PasteShortcut; use super::PasteShortcut;
use log::error; use crate::bridge::linux::*;
use crate::config::Configs; use crate::config::Configs;
use log::error;
use std::ffi::CString;
pub struct LinuxKeyboardManager { pub struct LinuxKeyboardManager {}
}
impl super::KeyboardManager for LinuxKeyboardManager { impl super::KeyboardManager for LinuxKeyboardManager {
fn send_string(&self, active_config: &Configs, s: &str) { fn send_string(&self, active_config: &Configs, s: &str) {
@ -32,12 +31,12 @@ impl super::KeyboardManager for LinuxKeyboardManager {
match res { match res {
Ok(cstr) => unsafe { Ok(cstr) => unsafe {
if active_config.fast_inject { if active_config.fast_inject {
fast_send_string(cstr.as_ptr()); fast_send_string(cstr.as_ptr(), active_config.inject_delay);
}else{ } else {
send_string(cstr.as_ptr()); send_string(cstr.as_ptr());
} }
} },
Err(e) => panic!(e.to_string()) Err(e) => panic!(e.to_string()),
} }
} }
@ -45,7 +44,7 @@ impl super::KeyboardManager for LinuxKeyboardManager {
unsafe { unsafe {
if active_config.fast_inject { if active_config.fast_inject {
fast_send_enter(); fast_send_enter();
}else{ } else {
send_enter(); send_enter();
} }
} }
@ -93,8 +92,8 @@ impl super::KeyboardManager for LinuxKeyboardManager {
fn delete_string(&self, active_config: &Configs, count: i32) { fn delete_string(&self, active_config: &Configs, count: i32) {
unsafe { unsafe {
if active_config.fast_inject { if active_config.fast_inject {
fast_delete_string(count); fast_delete_string(count, active_config.backspace_delay);
}else{ } else {
delete_string(count) delete_string(count)
} }
} }
@ -104,7 +103,7 @@ impl super::KeyboardManager for LinuxKeyboardManager {
unsafe { unsafe {
if active_config.fast_inject { if active_config.fast_inject {
fast_left_arrow(count); fast_left_arrow(count);
}else{ } else {
left_arrow(count); left_arrow(count);
} }
} }
@ -115,4 +114,4 @@ impl super::KeyboardManager for LinuxKeyboardManager {
trigger_copy(); trigger_copy();
} }
} }
} }

View File

@ -17,21 +17,22 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::ffi::CString;
use crate::bridge::macos::*;
use super::PasteShortcut; use super::PasteShortcut;
use log::error; use crate::bridge::macos::*;
use crate::config::Configs; use crate::config::Configs;
use log::error;
use std::ffi::CString;
pub struct MacKeyboardManager { pub struct MacKeyboardManager {}
}
impl super::KeyboardManager for MacKeyboardManager { impl super::KeyboardManager for MacKeyboardManager {
fn send_string(&self, _: &Configs, s: &str) { fn send_string(&self, _: &Configs, s: &str) {
let res = CString::new(s); let res = CString::new(s);
match res { match res {
Ok(cstr) => unsafe { send_string(cstr.as_ptr()); } Ok(cstr) => unsafe {
Err(e) => panic!(e.to_string()) send_string(cstr.as_ptr());
},
Err(e) => panic!(e.to_string()),
} }
} }
@ -64,7 +65,7 @@ impl super::KeyboardManager for MacKeyboardManager {
} }
fn delete_string(&self, _: &Configs, count: i32) { fn delete_string(&self, _: &Configs, count: i32) {
unsafe {delete_string(count)} unsafe { delete_string(count) }
} }
fn move_cursor_left(&self, _: &Configs, count: i32) { fn move_cursor_left(&self, _: &Configs, count: i32) {
@ -73,4 +74,4 @@ impl super::KeyboardManager for MacKeyboardManager {
send_multi_vkey(0x7B, count); send_multi_vkey(0x7B, count);
} }
} }
} }

View File

@ -17,8 +17,8 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde::{Serialize, Deserialize};
use crate::config::Configs; use crate::config::Configs;
use serde::{Deserialize, Serialize};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod windows; mod windows;
@ -40,15 +40,15 @@ pub trait KeyboardManager {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub enum PasteShortcut { pub enum PasteShortcut {
Default, // Default one for the current system Default, // Default one for the current system
CtrlV, // Classic Ctrl+V shortcut CtrlV, // Classic Ctrl+V shortcut
CtrlShiftV, // Could be used to paste without formatting in many applications CtrlShiftV, // Could be used to paste without formatting in many applications
ShiftInsert, // Often used in Linux systems ShiftInsert, // Often used in Linux systems
CtrlAltV, // Used in some Linux terminals (urxvt) CtrlAltV, // Used in some Linux terminals (urxvt)
MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
} }
impl Default for PasteShortcut{ impl Default for PasteShortcut {
fn default() -> Self { fn default() -> Self {
PasteShortcut::Default PasteShortcut::Default
} }
@ -57,17 +57,17 @@ impl Default for PasteShortcut{
// WINDOWS IMPLEMENTATION // WINDOWS IMPLEMENTATION
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_manager() -> impl KeyboardManager { pub fn get_manager() -> impl KeyboardManager {
windows::WindowsKeyboardManager{} windows::WindowsKeyboardManager {}
} }
// LINUX IMPLEMENTATION // LINUX IMPLEMENTATION
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn get_manager() -> impl KeyboardManager { pub fn get_manager() -> impl KeyboardManager {
linux::LinuxKeyboardManager{} linux::LinuxKeyboardManager {}
} }
// MAC IMPLEMENTATION // MAC IMPLEMENTATION
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn get_manager() -> impl KeyboardManager { pub fn get_manager() -> impl KeyboardManager {
macos::MacKeyboardManager{} macos::MacKeyboardManager {}
} }

View File

@ -17,27 +17,23 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use widestring::{U16CString};
use crate::bridge::windows::*;
use super::PasteShortcut; use super::PasteShortcut;
use log::error; use crate::bridge::windows::*;
use crate::config::Configs; use crate::config::Configs;
use log::error;
use widestring::U16CString;
pub struct WindowsKeyboardManager { pub struct WindowsKeyboardManager {}
}
impl super::KeyboardManager for WindowsKeyboardManager { impl super::KeyboardManager for WindowsKeyboardManager {
fn send_string(&self, _: &Configs, s: &str) { fn send_string(&self, _: &Configs, s: &str) {
let res = U16CString::from_str(s); let res = U16CString::from_str(s);
match res { match res {
Ok(s) => { Ok(s) => unsafe {
unsafe { send_string(s.as_ptr());
send_string(s.as_ptr()); },
} Err(e) => println!("Error while sending string: {}", e.to_string()),
}
Err(e) => println!("Error while sending string: {}", e.to_string())
} }
} }
fn send_enter(&self, _: &Configs) { fn send_enter(&self, _: &Configs) {
@ -62,10 +58,8 @@ impl super::KeyboardManager for WindowsKeyboardManager {
} }
} }
fn delete_string(&self, _: &Configs, count: i32) { fn delete_string(&self, config: &Configs, count: i32) {
unsafe { unsafe { delete_string(count, config.backspace_delay) }
delete_string(count)
}
} }
fn move_cursor_left(&self, _: &Configs, count: i32) { fn move_cursor_left(&self, _: &Configs, count: i32) {
@ -80,4 +74,4 @@ impl super::KeyboardManager for WindowsKeyboardManager {
trigger_copy(); trigger_copy();
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -17,13 +17,13 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde::{Serialize, Deserialize, Deserializer};
use crate::event::{KeyEvent, KeyModifier};
use crate::event::KeyEventReceiver; use crate::event::KeyEventReceiver;
use serde_yaml::Mapping; use crate::event::{KeyEvent, KeyModifier};
use regex::Regex; use regex::Regex;
use std::path::PathBuf; use serde::{Deserialize, Deserializer, Serialize};
use serde_yaml::Mapping;
use std::fs; use std::fs;
use std::path::PathBuf;
pub(crate) mod scrolling; pub(crate) mod scrolling;
@ -61,16 +61,17 @@ pub struct ImageContent {
pub path: PathBuf, pub path: PathBuf,
} }
impl <'de> serde::Deserialize<'de> for Match { impl<'de> serde::Deserialize<'de> for Match {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
D: Deserializer<'de> { where
D: Deserializer<'de>,
{
let auto_match = AutoMatch::deserialize(deserializer)?; let auto_match = AutoMatch::deserialize(deserializer)?;
Ok(Match::from(&auto_match)) Ok(Match::from(&auto_match))
} }
} }
impl<'a> From<&'a AutoMatch> for Match{ impl<'a> From<&'a AutoMatch> for Match {
fn from(other: &'a AutoMatch) -> Self { fn from(other: &'a AutoMatch) -> Self {
lazy_static! { lazy_static! {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap(); static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap();
@ -78,9 +79,9 @@ impl<'a> From<&'a AutoMatch> for Match{
let mut triggers = if !other.triggers.is_empty() { let mut triggers = if !other.triggers.is_empty() {
other.triggers.clone() other.triggers.clone()
}else if !other.trigger.is_empty() { } else if !other.trigger.is_empty() {
vec!(other.trigger.clone()) vec![other.trigger.clone()]
}else{ } else {
panic!("Match does not have any trigger defined: {:?}", other) panic!("Match does not have any trigger defined: {:?}", other)
}; };
@ -89,37 +90,48 @@ impl<'a> From<&'a AutoMatch> for Match{
// "hello", "Hello", "HELLO" // "hello", "Hello", "HELLO"
if other.propagate_case { if other.propagate_case {
// List with first letter capitalized // List with first letter capitalized
let first_capitalized : Vec<String> = triggers.iter().map(|trigger| { let first_capitalized: Vec<String> = triggers
let capitalized = trigger.clone(); .iter()
let mut v: Vec<char> = capitalized.chars().collect(); .map(|trigger| {
v[0] = v[0].to_uppercase().nth(0).unwrap(); let capitalized = trigger.clone();
v.into_iter().collect() let mut v: Vec<char> = capitalized.chars().collect();
}).collect();
let all_capitalized : Vec<String> = triggers.iter().map(|trigger| { // Capitalize the first alphabetic letter
trigger.to_uppercase() // See issue #244
}).collect(); 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<String> = triggers
.iter()
.map(|trigger| trigger.to_uppercase())
.collect();
triggers.extend(first_capitalized); triggers.extend(first_capitalized);
triggers.extend(all_capitalized); triggers.extend(all_capitalized);
} }
let trigger_sequences = triggers.iter().map(|trigger| { let trigger_sequences = triggers
// Calculate the trigger sequence .iter()
let mut trigger_sequence = Vec::new(); .map(|trigger| {
let trigger_chars : Vec<char> = trigger.chars().collect(); // Calculate the trigger sequence
trigger_sequence.extend(trigger_chars.into_iter().map(|c| { let mut trigger_sequence = Vec::new();
TriggerEntry::Char(c) let trigger_chars: Vec<char> = 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 if other.word {
trigger_sequence.push(TriggerEntry::WordSeparator); // If it's a word match, end with a word separator
} trigger_sequence.push(TriggerEntry::WordSeparator);
}
trigger_sequence trigger_sequence
}).collect(); })
.collect();
let content = if let Some(replace) = &other.replace {
let content = if let Some(replace) = &other.replace { // Text match // Text match
let new_replace = replace.clone(); let new_replace = replace.clone();
// Check if the match contains variables // Check if the match contains variables
@ -132,11 +144,12 @@ impl<'a> From<&'a AutoMatch> for Match{
}; };
MatchContentType::Text(content) MatchContentType::Text(content)
}else if let Some(image_path) = &other.image_path { // Image match } 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 // On Windows, we have to replace the forward / with the backslash \ in the path
let new_path = if cfg!(target_os = "windows") { let new_path = if cfg!(target_os = "windows") {
image_path.replace("/", "\\") image_path.replace("/", "\\")
}else{ } else {
image_path.to_owned() image_path.to_owned()
}; };
@ -146,20 +159,20 @@ impl<'a> From<&'a AutoMatch> for Match{
let config_path = fs::canonicalize(&config_dir); let config_path = fs::canonicalize(&config_dir);
let config_path = if let Ok(config_path) = config_path { let config_path = if let Ok(config_path) = config_path {
config_path.to_string_lossy().into_owned() config_path.to_string_lossy().into_owned()
}else{ } else {
"".to_owned() "".to_owned()
}; };
new_path.replace("$CONFIG", &config_path) new_path.replace("$CONFIG", &config_path)
}else{ } else {
new_path.to_owned() new_path.to_owned()
}; };
let content = ImageContent { let content = ImageContent {
path: PathBuf::from(new_path) path: PathBuf::from(new_path),
}; };
MatchContentType::Image(content) MatchContentType::Image(content)
}else { } else {
eprintln!("ERROR: no action specified for match {}, please specify either 'replace' or 'image_path'", other.trigger); eprintln!("ERROR: no action specified for match {}, please specify either 'replace' or 'image_path'", other.trigger);
std::process::exit(2); std::process::exit(2);
}; };
@ -207,15 +220,33 @@ struct AutoMatch {
pub force_clipboard: bool, pub force_clipboard: bool,
} }
fn default_trigger() -> String {"".to_owned()} fn default_trigger() -> String {
fn default_triggers() -> Vec<String> {Vec::new()} "".to_owned()
fn default_vars() -> Vec<MatchVariable> {Vec::new()} }
fn default_word() -> bool {false} fn default_triggers() -> Vec<String> {
fn default_passive_only() -> bool {false} Vec::new()
fn default_replace() -> Option<String> {None} }
fn default_image_path() -> Option<String> {None} fn default_vars() -> Vec<MatchVariable> {
fn default_propagate_case() -> bool {false} Vec::new()
fn default_force_clipboard() -> bool {false} }
fn default_word() -> bool {
false
}
fn default_passive_only() -> bool {
false
}
fn default_replace() -> Option<String> {
None
}
fn default_image_path() -> Option<String> {
None
}
fn default_propagate_case() -> bool {
false
}
fn default_force_clipboard() -> bool {
false
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MatchVariable { pub struct MatchVariable {
@ -228,12 +259,14 @@ pub struct MatchVariable {
pub params: Mapping, pub params: Mapping,
} }
fn default_params() -> Mapping {Mapping::new()} fn default_params() -> Mapping {
Mapping::new()
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum TriggerEntry { pub enum TriggerEntry {
Char(char), Char(char),
WordSeparator WordSeparator,
} }
pub trait MatchReceiver { pub trait MatchReceiver {
@ -242,29 +275,28 @@ pub trait MatchReceiver {
fn on_passive(&self); fn on_passive(&self);
} }
pub trait Matcher : KeyEventReceiver { pub trait Matcher: KeyEventReceiver {
fn handle_char(&self, c: &str); fn handle_char(&self, c: &str);
fn handle_modifier(&self, m: KeyModifier); fn handle_modifier(&self, m: KeyModifier);
fn handle_other(&self); fn handle_other(&self);
} }
impl <M: Matcher> KeyEventReceiver for M { impl<M: Matcher> KeyEventReceiver for M {
fn on_key_event(&self, e: KeyEvent) { fn on_key_event(&self, e: KeyEvent) {
match e { match e {
KeyEvent::Char(c) => { KeyEvent::Char(c) => {
self.handle_char(&c); self.handle_char(&c);
}, }
KeyEvent::Modifier(m) => { KeyEvent::Modifier(m) => {
self.handle_modifier(m); self.handle_modifier(m);
}, }
KeyEvent::Other => { KeyEvent::Other => {
self.handle_other(); self.handle_other();
}, }
} }
} }
} }
// TESTS // TESTS
#[cfg(test)] #[cfg(test)]
@ -278,15 +310,15 @@ mod tests {
replace: "There are no variables" replace: "There are no variables"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
match _match.content { match _match.content {
MatchContentType::Text(content) => { MatchContentType::Text(content) => {
assert_eq!(content._has_vars, false); assert_eq!(content._has_vars, false);
}, }
_ => { _ => {
assert!(false); assert!(false);
}, }
} }
} }
@ -297,15 +329,15 @@ mod tests {
replace: "There are {{one}} and {{two}} variables" replace: "There are {{one}} and {{two}} variables"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
match _match.content { match _match.content {
MatchContentType::Text(content) => { MatchContentType::Text(content) => {
assert_eq!(content._has_vars, true); assert_eq!(content._has_vars, true);
}, }
_ => { _ => {
assert!(false); assert!(false);
}, }
} }
} }
@ -316,15 +348,15 @@ mod tests {
replace: "There is {{ one }} variable" replace: "There is {{ one }} variable"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
match _match.content { match _match.content {
MatchContentType::Text(content) => { MatchContentType::Text(content) => {
assert_eq!(content._has_vars, true); assert_eq!(content._has_vars, true);
}, }
_ => { _ => {
assert!(false); assert!(false);
}, }
} }
} }
@ -335,7 +367,7 @@ mod tests {
replace: "This is a test" replace: "This is a test"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); 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][0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e')); assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
@ -351,7 +383,7 @@ mod tests {
word: true word: true
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); 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][0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e')); assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
@ -367,15 +399,15 @@ mod tests {
image_path: "/path/to/file" image_path: "/path/to/file"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
match _match.content { match _match.content {
MatchContentType::Image(content) => { MatchContentType::Image(content) => {
assert_eq!(content.path, PathBuf::from("/path/to/file")); assert_eq!(content.path, PathBuf::from("/path/to/file"));
}, }
_ => { _ => {
assert!(false); assert!(false);
}, }
} }
} }
@ -386,7 +418,7 @@ mod tests {
replace: "This is a test" replace: "This is a test"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec![":test"]) assert_eq!(_match.triggers, vec![":test"])
} }
@ -400,7 +432,7 @@ mod tests {
replace: "This is a test" replace: "This is a test"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec![":test1", ":test2"]) assert_eq!(_match.triggers, vec![":test1", ":test2"])
} }
@ -412,7 +444,7 @@ mod tests {
replace: "This is a test" replace: "This is a test"
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec![":test1", ":test2"]) assert_eq!(_match.triggers, vec![":test1", ":test2"])
} }
@ -425,7 +457,7 @@ mod tests {
propagate_case: true propagate_case: true
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec!["hello", "Hello", "HELLO"]) assert_eq!(_match.triggers, vec!["hello", "Hello", "HELLO"])
} }
@ -438,9 +470,12 @@ mod tests {
propagate_case: true propagate_case: true
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match: Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec!["hello", "hi", "Hello", "Hi", "HELLO", "HI"]) assert_eq!(
_match.triggers,
vec!["hello", "hi", "Hello", "Hi", "HELLO", "HI"]
)
} }
#[test] #[test]
@ -452,7 +487,7 @@ mod tests {
propagate_case: true propagate_case: true
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); 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][0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e')); assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
@ -480,6 +515,32 @@ mod tests {
replace: "" replace: ""
"###; "###;
let _match : Match = serde_yaml::from_str(match_str).unwrap(); 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![":..", ":..", ":.."])
}
}

View File

@ -17,13 +17,13 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::matcher::{Match, MatchReceiver, TriggerEntry};
use std::cell::{RefCell};
use crate::event::{KeyModifier, ActionEventReceiver, ActionType};
use crate::config::ConfigManager; use crate::config::ConfigManager;
use crate::event::KeyModifier::BACKSPACE; use crate::event::KeyModifier::{BACKSPACE, CAPS_LOCK, LEFT_SHIFT, RIGHT_SHIFT};
use std::time::SystemTime; use crate::event::{ActionEventReceiver, ActionType, KeyModifier};
use crate::matcher::{Match, MatchReceiver, TriggerEntry};
use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::time::SystemTime;
pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
config_manager: &'a M, config_manager: &'a M,
@ -40,16 +40,16 @@ struct MatchEntry<'a> {
start: usize, start: usize,
count: usize, count: usize,
trigger_offset: usize, // The index of the trigger in the Match that matched trigger_offset: usize, // The index of the trigger in the Match that matched
_match: &'a Match _match: &'a Match,
} }
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { 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> { pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> {
let current_set_queue = RefCell::new(VecDeque::new()); let current_set_queue = RefCell::new(VecDeque::new());
let toggle_press_time = RefCell::new(SystemTime::now()); let toggle_press_time = RefCell::new(SystemTime::now());
let passive_press_time = RefCell::new(SystemTime::now()); let passive_press_time = RefCell::new(SystemTime::now());
ScrollingMatcher{ ScrollingMatcher {
config_manager, config_manager,
receiver, receiver,
current_set_queue, current_set_queue,
@ -74,19 +74,21 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
self.receiver.on_enable_update(*is_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 { 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] { match mtc._trigger_sequences[trigger_offset][start] {
TriggerEntry::Char(c) => { TriggerEntry::Char(c) => current_char.starts_with(c),
current_char.starts_with(c) TriggerEntry::WordSeparator => is_current_word_separator,
},
TriggerEntry::WordSeparator => {
is_current_word_separator
},
} }
} }
} }
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> { impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> {
fn handle_char(&self, c: &str) { fn handle_char(&self, c: &str) {
// if not enabled, avoid any processing // if not enabled, avoid any processing
if !*(self.is_enabled.borrow()) { if !*(self.is_enabled.borrow()) {
@ -98,9 +100,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
let active_config = self.config_manager.active_config(); let active_config = self.config_manager.active_config();
// Check if the current char is a word separator // Check if the current char is a word separator
let mut is_current_word_separator = active_config.word_separators.contains( let mut is_current_word_separator = active_config
&c.chars().nth(0).unwrap_or_default() .word_separators
); .contains(&c.chars().nth(0).unwrap_or_default());
// Workaround needed on macos to consider espanso replacement key presses as separators. // Workaround needed on macos to consider espanso replacement key presses as separators.
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
@ -118,22 +120,23 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
for m in active_config.matches.iter() { for m in active_config.matches.iter() {
// only active-enabled matches are considered // only active-enabled matches are considered
if m.passive_only { if m.passive_only {
continue continue;
} }
for trigger_offset in 0..m._trigger_sequences.len() { for trigger_offset in 0..m._trigger_sequences.len() {
let mut result = Self::is_matching(m, c, 0, trigger_offset, is_current_word_separator); let mut result =
Self::is_matching(m, c, 0, trigger_offset, is_current_word_separator);
if m.word { if m.word {
result = result && *was_previous_word_separator result = result && *was_previous_word_separator
} }
if result { if result {
new_matches.push(MatchEntry{ new_matches.push(MatchEntry {
start: 1, start: 1,
count: m._trigger_sequences[trigger_offset].len(), count: m._trigger_sequences[trigger_offset].len(),
trigger_offset, trigger_offset,
_match: &m _match: &m,
}); });
} }
} }
@ -142,22 +145,29 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() { let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
Some(last_matches) => { Some(last_matches) => {
let mut updated: Vec<MatchEntry> = last_matches.iter() let mut updated: Vec<MatchEntry> = last_matches
.iter()
.filter(|&x| { .filter(|&x| {
Self::is_matching(x._match, c, x.start, x.trigger_offset, is_current_word_separator) Self::is_matching(
x._match,
c,
x.start,
x.trigger_offset,
is_current_word_separator,
)
}) })
.map(|x | MatchEntry{ .map(|x| MatchEntry {
start: x.start+1, start: x.start + 1,
count: x.count, count: x.count,
trigger_offset: x.trigger_offset, trigger_offset: x.trigger_offset,
_match: &x._match _match: &x._match,
}) })
.collect(); .collect();
updated.extend(new_matches); updated.extend(new_matches);
updated updated
}, }
None => {new_matches}, None => new_matches,
}; };
let mut found_entry = None; let mut found_entry = None;
@ -171,7 +181,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
current_set_queue.push_back(combined_matches); current_set_queue.push_back(combined_matches);
if current_set_queue.len() as i32 > (self.config_manager.default_config().backspace_limit + 1) { if current_set_queue.len() as i32
> (self.config_manager.default_config().backspace_limit + 1)
{
current_set_queue.pop_front(); current_set_queue.pop_front();
} }
@ -189,20 +201,21 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
None None
} else if !is_current_word_separator { } else if !is_current_word_separator {
None None
}else{ } else {
let as_char = c.chars().nth(0); let as_char = c.chars().nth(0);
match as_char { match as_char {
Some(c) => { Some(c) => {
Some(c) // Current char is the trailing separator Some(c) // Current char is the trailing separator
}, }
None => {None}, None => None,
} }
}; };
// Force espanso to consider the last char as a separator // Force espanso to consider the last char as a separator
*was_previous_word_separator = true; *was_previous_word_separator = true;
self.receiver.on_match(mtc, trailing_separator, entry.trigger_offset); self.receiver
.on_match(mtc, trailing_separator, entry.trigger_offset);
} }
} }
@ -212,22 +225,28 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
// TODO: at the moment, activating the passive key triggers the toggle key // TODO: at the moment, activating the passive key triggers the toggle key
// study a mechanism to avoid this problem // study a mechanism to avoid this problem
if KeyModifier::shallow_equals(&m, &config.toggle_key) { if KeyModifier::shallow_equals(&m, &config.toggle_key) {
check_interval(&self.toggle_press_time, check_interval(
u128::from(config.toggle_interval), || { &self.toggle_press_time,
self.toggle(); u128::from(config.toggle_interval),
|| {
self.toggle();
let is_enabled = self.is_enabled.borrow(); let is_enabled = self.is_enabled.borrow();
if !*is_enabled { if !*is_enabled {
self.current_set_queue.borrow_mut().clear(); self.current_set_queue.borrow_mut().clear();
} }
}); },
}else if KeyModifier::shallow_equals(&m, &config.passive_key) { );
check_interval(&self.passive_press_time, } else if KeyModifier::shallow_equals(&m, &config.passive_key) {
u128::from(config.toggle_interval), || { check_interval(
self.receiver.on_passive(); &self.passive_press_time,
}); u128::from(config.toggle_interval),
|| {
self.receiver.on_passive();
},
);
} }
// Backspace handling, basically "rewinding history" // Backspace handling, basically "rewinding history"
@ -237,36 +256,45 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
} }
// Consider modifiers as separators to improve word matches reliability // Consider modifiers as separators to improve word matches reliability
let mut was_previous_char_word_separator = self.was_previous_char_word_separator.borrow_mut(); if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
*was_previous_char_word_separator = true; 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) { fn handle_other(&self) {
// When receiving "other" type of events, we mark them as valid separators. // When receiving "other" type of events, we mark them as valid separators.
// This dramatically improves the reliability of word matches // This dramatically improves the reliability of word matches
let mut was_previous_char_word_separator = self.was_previous_char_word_separator.borrow_mut(); let mut was_previous_char_word_separator =
self.was_previous_char_word_separator.borrow_mut();
*was_previous_char_word_separator = true; *was_previous_char_word_separator = true;
} }
} }
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver for ScrollingMatcher<'a, R, M> { impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver
for ScrollingMatcher<'a, R, M>
{
fn on_action_event(&self, e: ActionType) { fn on_action_event(&self, e: ActionType) {
match e { match e {
ActionType::Toggle => { ActionType::Toggle => {
self.toggle(); self.toggle();
}, }
ActionType::Enable => { ActionType::Enable => {
self.set_enabled(true); self.set_enabled(true);
}, }
ActionType::Disable => { ActionType::Disable => {
self.set_enabled(false); self.set_enabled(false);
}, }
_ => {} _ => {}
} }
} }
} }
fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_callback: F) where F:Fn() { fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_callback: F)
where
F: Fn(),
{
let mut press_time = state_var.borrow_mut(); let mut press_time = state_var.borrow_mut();
if let Ok(elapsed) = press_time.elapsed() { if let Ok(elapsed) = press_time.elapsed() {
if elapsed.as_millis() < interval { if elapsed.as_millis() < interval {
@ -275,4 +303,4 @@ fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_ca
} }
(*press_time) = SystemTime::now(); (*press_time) = SystemTime::now();
} }

View File

@ -17,20 +17,22 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::path::{PathBuf, Path}; use crate::package::InstallResult::{AlreadyInstalled, BlockedExternalPackage, NotFoundInIndex};
use crate::package::{PackageIndex, UpdateResult, Package, InstallResult, RemoveResult, PackageResolver};
use std::error::Error;
use std::fs::{File, create_dir};
use std::io::{BufReader, BufRead};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::package::UpdateResult::{NotOutdated, Updated};
use crate::package::InstallResult::{NotFoundInIndex, AlreadyInstalled, BlockedExternalPackage};
use std::fs;
use regex::Regex;
use crate::package::RemoveResult::Removed; 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::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"; const DEFAULT_PACKAGE_INDEX_FILE: &str = "package_index.json";
pub struct DefaultPackageManager { pub struct DefaultPackageManager {
package_dir: PathBuf, package_dir: PathBuf,
@ -42,18 +44,24 @@ pub struct DefaultPackageManager {
} }
impl DefaultPackageManager { impl DefaultPackageManager {
pub fn new(package_dir: PathBuf, data_dir: PathBuf, package_resolver: Option<Box<dyn PackageResolver>>) -> DefaultPackageManager { pub fn new(
package_dir: PathBuf,
data_dir: PathBuf,
package_resolver: Option<Box<dyn PackageResolver>>,
) -> DefaultPackageManager {
let local_index = Self::load_local_index(&data_dir); let local_index = Self::load_local_index(&data_dir);
DefaultPackageManager{ DefaultPackageManager {
package_dir, package_dir,
data_dir, data_dir,
package_resolver, package_resolver,
local_index local_index,
} }
} }
pub fn new_default(package_resolver: Option<Box<dyn PackageResolver>>) -> DefaultPackageManager { pub fn new_default(
package_resolver: Option<Box<dyn PackageResolver>>,
) -> DefaultPackageManager {
DefaultPackageManager::new( DefaultPackageManager::new(
crate::context::get_package_dir(), crate::context::get_package_dir(),
crate::context::get_data_dir(), crate::context::get_data_dir(),
@ -72,7 +80,7 @@ impl DefaultPackageManager {
let local_index = serde_json::from_reader(reader); let local_index = serde_json::from_reader(reader);
if let Ok(local_index) = local_index { if let Ok(local_index) = local_index {
return local_index return local_index;
} }
} }
@ -81,19 +89,21 @@ impl DefaultPackageManager {
fn request_index() -> Result<super::PackageIndex, Box<dyn Error>> { fn request_index() -> Result<super::PackageIndex, Box<dyn Error>> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let request = client.get("https://hub.espanso.org/json/") let request = client
.get("https://hub.espanso.org/json/")
.header("User-Agent", format!("espanso/{}", crate::VERSION)); .header("User-Agent", format!("espanso/{}", crate::VERSION));
let mut res = request.send()?; let mut res = request.send()?;
let body = res.text()?; let body = res.text()?;
let index : PackageIndex = serde_json::from_str(&body)?; let index: PackageIndex = serde_json::from_str(&body)?;
Ok(index) Ok(index)
} }
fn parse_package_from_readme(readme_path: &Path) -> Option<Package> { fn parse_package_from_readme(readme_path: &Path) -> Option<Package> {
lazy_static! { lazy_static! {
static ref FIELD_REGEX: Regex = Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap(); static ref FIELD_REGEX: Regex =
Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap();
} }
// Read readme line by line // Read readme line by line
@ -101,7 +111,7 @@ impl DefaultPackageManager {
if let Ok(file) = file { if let Ok(file) = file {
let reader = BufReader::new(file); let reader = BufReader::new(file);
let mut fields :HashMap<String, String> = HashMap::new(); let mut fields: HashMap<String, String> = HashMap::new();
let mut started = false; let mut started = false;
@ -109,35 +119,38 @@ impl DefaultPackageManager {
let line = line.unwrap(); let line = line.unwrap();
if line.contains("---") { if line.contains("---") {
if started { if started {
break break;
}else{ } else {
started = true; started = true;
} }
}else if started { } else if started {
let caps = FIELD_REGEX.captures(&line); let caps = FIELD_REGEX.captures(&line);
if let Some(caps) = caps { if let Some(caps) = caps {
let property = caps.get(1); let property = caps.get(1);
let value = caps.get(2); let value = caps.get(2);
if property.is_some() && value.is_some() { if property.is_some() && value.is_some() {
fields.insert(property.unwrap().as_str().to_owned(), fields.insert(
value.unwrap().as_str().to_owned()); property.unwrap().as_str().to_owned(),
value.unwrap().as_str().to_owned(),
);
} }
} }
} }
} }
if !fields.contains_key("package_name") || if !fields.contains_key("package_name")
!fields.contains_key("package_title") || || !fields.contains_key("package_title")
!fields.contains_key("package_version") || || !fields.contains_key("package_version")
!fields.contains_key("package_repo") || || !fields.contains_key("package_repo")
!fields.contains_key("package_desc") || || !fields.contains_key("package_desc")
!fields.contains_key("package_author") { || !fields.contains_key("package_author")
return None {
return None;
} }
let original_repo = if fields.contains_key("package_original_repo") { let original_repo = if fields.contains_key("package_original_repo") {
fields.get("package_original_repo").unwrap().clone() fields.get("package_original_repo").unwrap().clone()
}else{ } else {
fields.get("package_repo").unwrap().clone() fields.get("package_repo").unwrap().clone()
}; };
@ -147,7 +160,7 @@ impl DefaultPackageManager {
"false" => false, "false" => false,
_ => false, _ => false,
} }
}else{ } else {
false false
}; };
@ -159,18 +172,18 @@ impl DefaultPackageManager {
desc: fields.get("package_desc").unwrap().clone(), desc: fields.get("package_desc").unwrap().clone(),
author: fields.get("package_author").unwrap().clone(), author: fields.get("package_author").unwrap().clone(),
is_core, is_core,
original_repo original_repo,
}; };
Some(package) Some(package)
}else{ } else {
None None
} }
} }
fn local_index_timestamp(&self) -> u64 { fn local_index_timestamp(&self) -> u64 {
if let Some(local_index) = &self.local_index { if let Some(local_index) = &self.local_index {
return local_index.last_update return local_index.last_update;
} }
0 0
@ -198,7 +211,8 @@ impl DefaultPackageManager {
fn cache_local_index(&self) { fn cache_local_index(&self) {
if let Some(local_index) = &self.local_index { if let Some(local_index) = &self.local_index {
let serialized = serde_json::to_string(local_index).expect("Unable to serialize 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); 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"); std::fs::write(local_index_file, serialized).expect("Unable to cache local index");
} }
@ -207,13 +221,15 @@ impl DefaultPackageManager {
impl super::PackageManager for DefaultPackageManager { impl super::PackageManager for DefaultPackageManager {
fn is_index_outdated(&self) -> bool { fn is_index_outdated(&self) -> bool {
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards"); let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let current_timestamp = current_time.as_secs(); let current_timestamp = current_time.as_secs();
let local_index_timestamp = self.local_index_timestamp(); let local_index_timestamp = self.local_index_timestamp();
// Local index is outdated if older than a day // Local index is outdated if older than a day
local_index_timestamp + 60*60*24 < current_timestamp local_index_timestamp + 60 * 60 * 24 < current_timestamp
} }
fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>> { fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>> {
@ -225,48 +241,60 @@ impl super::PackageManager for DefaultPackageManager {
self.cache_local_index(); self.cache_local_index();
Ok(Updated) Ok(Updated)
}else{ } else {
Ok(NotOutdated) Ok(NotOutdated)
} }
} }
fn get_package(&self, name: &str) -> Option<Package> { fn get_package(&self, name: &str) -> Option<Package> {
if let Some(local_index) = &self.local_index { if let Some(local_index) = &self.local_index {
let result = local_index.packages.iter().find(|package| { let result = local_index
package.name == name .packages
}); .iter()
.find(|package| package.name == name);
if let Some(package) = result { if let Some(package) = result {
return Some(package.clone()) return Some(package.clone());
} }
} }
None None
} }
fn install_package(&self, name: &str, allow_external: bool) -> Result<InstallResult, Box<dyn Error>> { fn install_package(
&self,
name: &str,
allow_external: bool,
) -> Result<InstallResult, Box<dyn Error>> {
let package = self.get_package(name); let package = self.get_package(name);
match package { match package {
Some(package) => { Some(package) => {
if package.is_core || allow_external { if package.is_core || allow_external {
self.install_package_from_repo(name, &package.repo) self.install_package_from_repo(name, &package.repo)
}else{ } else {
Ok(BlockedExternalPackage(package.original_repo)) Ok(BlockedExternalPackage(package.original_repo))
} }
}, }
None => { None => Ok(NotFoundInIndex),
Ok(NotFoundInIndex)
},
} }
} }
fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result<InstallResult, Box<dyn Error>> { fn install_package_from_repo(
&self,
name: &str,
repo_url: &str,
) -> Result<InstallResult, Box<dyn Error>> {
// Check if package is already installed // Check if package is already installed
let packages = self.list_local_packages_names(); let packages = self.list_local_packages_names();
if packages.iter().any(|p| p == name) { // Package already installed if packages.iter().any(|p| p == name) {
// Package already installed
return Ok(AlreadyInstalled); return Ok(AlreadyInstalled);
} }
let temp_dir = self.package_resolver.as_ref().unwrap().clone_repo_to_temp(repo_url)?; let temp_dir = self
.package_resolver
.as_ref()
.unwrap()
.clone_repo_to_temp(repo_url)?;
let temp_package_dir = temp_dir.path().join(name); let temp_package_dir = temp_dir.path().join(name);
if !temp_package_dir.exists() { if !temp_package_dir.exists() {
@ -329,15 +357,16 @@ impl super::PackageManager for DefaultPackageManager {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use tempfile::{TempDir, NamedTempFile}; use crate::package::zip::ZipPackageResolver;
use std::path::Path; use crate::package::InstallResult::*;
use crate::package::PackageManager; use crate::package::PackageManager;
use std::fs::{create_dir, create_dir_all}; use std::fs::{create_dir, create_dir_all};
use crate::package::InstallResult::*; use std::path::Path;
use crate::package::zip::ZipPackageResolver; use tempfile::{NamedTempFile, TempDir};
const OUTDATED_INDEX_CONTENT : &str = include_str!("../res/test/outdated_index.json"); 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 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 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"); const INSTALL_PACKAGE_INDEX: &str = include_str!("../res/test/install_package_index.json");
@ -347,7 +376,10 @@ mod tests {
package_manager: DefaultPackageManager, package_manager: DefaultPackageManager,
} }
fn create_temp_package_manager<F>(setup: F) -> TempPackageManager where F: Fn(&Path, &Path) -> (){ fn create_temp_package_manager<F>(setup: F) -> TempPackageManager
where
F: Fn(&Path, &Path) -> (),
{
let package_dir = TempDir::new().expect("unable to create temp directory"); let package_dir = TempDir::new().expect("unable to create temp directory");
let data_dir = TempDir::new().expect("unable to create temp directory"); let data_dir = TempDir::new().expect("unable to create temp directory");
@ -362,7 +394,7 @@ mod tests {
TempPackageManager { TempPackageManager {
package_dir, package_dir,
data_dir, data_dir,
package_manager package_manager,
} }
} }
@ -389,26 +421,38 @@ mod tests {
fn test_up_to_date_index_should_not_be_updated() { fn test_up_to_date_index_should_not_be_updated() {
let mut temp = create_temp_package_manager(|_, data_dir| { let mut temp = create_temp_package_manager(|_, data_dir| {
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); 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_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let current_timestamp = current_time.as_secs(); let current_timestamp = current_time.as_secs();
let new_contents = INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp)); let new_contents =
INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
std::fs::write(index_file, new_contents).unwrap(); std::fs::write(index_file, new_contents).unwrap();
}); });
assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::NotOutdated); assert_eq!(
temp.package_manager.update_index(false).unwrap(),
UpdateResult::NotOutdated
);
} }
#[test] #[test]
fn test_up_to_date_index_with_force_should_be_updated() { fn test_up_to_date_index_with_force_should_be_updated() {
let mut temp = create_temp_package_manager(|_, data_dir| { let mut temp = create_temp_package_manager(|_, data_dir| {
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); 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_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let current_timestamp = current_time.as_secs(); let current_timestamp = current_time.as_secs();
let new_contents = INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp)); let new_contents =
INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
std::fs::write(index_file, new_contents).unwrap(); std::fs::write(index_file, new_contents).unwrap();
}); });
assert_eq!(temp.package_manager.update_index(true).unwrap(), UpdateResult::Updated); assert_eq!(
temp.package_manager.update_index(true).unwrap(),
UpdateResult::Updated
);
} }
#[test] #[test]
@ -418,15 +462,25 @@ mod tests {
std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap(); std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap();
}); });
assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::Updated); assert_eq!(
temp.package_manager.update_index(false).unwrap(),
UpdateResult::Updated
);
} }
#[test] #[test]
fn test_update_index_should_create_file() { fn test_update_index_should_create_file() {
let mut temp = create_temp_package_manager(|_, _| {}); let mut temp = create_temp_package_manager(|_, _| {});
assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::Updated); assert_eq!(
assert!(temp.data_dir.path().join(DEFAULT_PACKAGE_INDEX_FILE).exists()) temp.package_manager.update_index(false).unwrap(),
UpdateResult::Updated
);
assert!(temp
.data_dir
.path()
.join(DEFAULT_PACKAGE_INDEX_FILE)
.exists())
} }
#[test] #[test]
@ -436,7 +490,13 @@ mod tests {
std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.get_package("italian-accents").unwrap().title, "Italian Accents"); assert_eq!(
temp.package_manager
.get_package("italian-accents")
.unwrap()
.title,
"Italian Accents"
);
} }
#[test] #[test]
@ -470,7 +530,12 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("doesnotexist", false).unwrap(), NotFoundInIndex); assert_eq!(
temp.package_manager
.install_package("doesnotexist", false)
.unwrap(),
NotFoundInIndex
);
} }
#[test] #[test]
@ -481,7 +546,12 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("italian-accents", false).unwrap(), AlreadyInstalled); assert_eq!(
temp.package_manager
.install_package("italian-accents", false)
.unwrap(),
AlreadyInstalled
);
} }
#[test] #[test]
@ -491,10 +561,23 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("dummy-package", false).unwrap(), Installed); assert_eq!(
temp.package_manager
.install_package("dummy-package", false)
.unwrap(),
Installed
);
assert!(temp.package_dir.path().join("dummy-package").exists()); assert!(temp.package_dir.path().join("dummy-package").exists());
assert!(temp.package_dir.path().join("dummy-package/README.md").exists()); assert!(temp
assert!(temp.package_dir.path().join("dummy-package/package.yml").exists()); .package_dir
.path()
.join("dummy-package/README.md")
.exists());
assert!(temp
.package_dir
.path()
.join("dummy-package/package.yml")
.exists());
} }
#[test] #[test]
@ -504,7 +587,12 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("not-existing", false).unwrap(), NotFoundInRepo); assert_eq!(
temp.package_manager
.install_package("not-existing", false)
.unwrap(),
NotFoundInRepo
);
} }
#[test] #[test]
@ -514,7 +602,12 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("dummy-package2", false).unwrap(), MissingPackageVersion); assert_eq!(
temp.package_manager
.install_package("dummy-package2", false)
.unwrap(),
MissingPackageVersion
);
} }
#[test] #[test]
@ -524,7 +617,12 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("dummy-package3", false).unwrap(), UnableToParsePackageInfo); assert_eq!(
temp.package_manager
.install_package("dummy-package3", false)
.unwrap(),
UnableToParsePackageInfo
);
} }
#[test] #[test]
@ -534,7 +632,12 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("dummy-package4", false).unwrap(), UnableToParsePackageInfo); assert_eq!(
temp.package_manager
.install_package("dummy-package4", false)
.unwrap(),
UnableToParsePackageInfo
);
} }
#[test] #[test]
@ -544,10 +647,23 @@ mod tests {
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap(); std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
}); });
assert_eq!(temp.package_manager.install_package("dummy-package", false).unwrap(), Installed); assert_eq!(
temp.package_manager
.install_package("dummy-package", false)
.unwrap(),
Installed
);
assert!(temp.package_dir.path().join("dummy-package").exists()); assert!(temp.package_dir.path().join("dummy-package").exists());
assert!(temp.package_dir.path().join("dummy-package/README.md").exists()); assert!(temp
assert!(temp.package_dir.path().join("dummy-package/package.yml").exists()); .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(); let list = temp.package_manager.list_local_packages();
assert_eq!(list.len(), 1); assert_eq!(list.len(), 1);
@ -564,25 +680,51 @@ mod tests {
}); });
assert!(temp.package_dir.path().join("dummy-package").exists()); assert!(temp.package_dir.path().join("dummy-package").exists());
assert!(temp.package_dir.path().join("dummy-package/README.md").exists()); assert!(temp
assert!(temp.package_dir.path().join("dummy-package/package.yml").exists()); .package_dir
assert_eq!(temp.package_manager.remove_package("dummy-package").unwrap(), RemoveResult::Removed); .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").exists());
assert!(!temp.package_dir.path().join("dummy-package/README.md").exists()); assert!(!temp
assert!(!temp.package_dir.path().join("dummy-package/package.yml").exists()); .package_dir
.path()
.join("dummy-package/README.md")
.exists());
assert!(!temp
.package_dir
.path()
.join("dummy-package/package.yml")
.exists());
} }
#[test] #[test]
fn test_remove_package_not_found() { fn test_remove_package_not_found() {
let temp = create_temp_package_manager(|_, _| {}); let temp = create_temp_package_manager(|_, _| {});
assert_eq!(temp.package_manager.remove_package("not-existing").unwrap(), RemoveResult::NotFound); assert_eq!(
temp.package_manager.remove_package("not-existing").unwrap(),
RemoveResult::NotFound
);
} }
#[test] #[test]
fn test_parse_package_from_readme() { fn test_parse_package_from_readme() {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
fs::write(file.path(), r###" fs::write(
file.path(),
r###"
--- ---
package_name: "italian-accents" package_name: "italian-accents"
package_title: "Italian Accents" package_title: "Italian Accents"
@ -592,7 +734,9 @@ mod tests {
package_repo: "https://github.com/federico-terzi/espanso-hub-core" package_repo: "https://github.com/federico-terzi/espanso-hub-core"
is_core: true is_core: true
--- ---
"###).unwrap(); "###,
)
.unwrap();
let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap(); let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
@ -613,7 +757,9 @@ mod tests {
#[test] #[test]
fn test_parse_package_from_readme_with_bad_metadata() { fn test_parse_package_from_readme_with_bad_metadata() {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
fs::write(file.path(), r###" fs::write(
file.path(),
r###"
--- ---
package_name: italian-accents package_name: italian-accents
package_title: "Italian Accents" package_title: "Italian Accents"
@ -624,7 +770,9 @@ mod tests {
is_core: true is_core: true
--- ---
Readme text Readme text
"###).unwrap(); "###,
)
.unwrap();
let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap(); let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
@ -641,4 +789,4 @@ mod tests {
assert_eq!(package, target_package); assert_eq!(package, target_package);
} }
} }

View File

@ -17,10 +17,10 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
pub(crate) mod zip;
pub(crate) mod default; pub(crate) mod default;
pub(crate) mod zip;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
use std::error::Error; use std::error::Error;
use tempfile::TempDir; use tempfile::TempDir;
@ -30,8 +30,16 @@ pub trait PackageManager {
fn get_package(&self, name: &str) -> Option<Package>; fn get_package(&self, name: &str) -> Option<Package>;
fn install_package(&self, name: &str, allow_external: bool) -> Result<InstallResult, Box<dyn Error>>; fn install_package(
fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result<InstallResult, Box<dyn Error>>; &self,
name: &str,
allow_external: bool,
) -> Result<InstallResult, Box<dyn Error>>;
fn install_package_from_repo(
&self,
name: &str,
repo_url: &str,
) -> Result<InstallResult, Box<dyn Error>>;
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>; fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
@ -57,18 +65,21 @@ pub struct Package {
pub original_repo: String, pub original_repo: String,
} }
fn default_is_core() -> bool {false} fn default_is_core() -> bool {
fn default_original_repo() -> String {"".to_owned()} false
}
fn default_original_repo() -> String {
"".to_owned()
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PackageIndex { pub struct PackageIndex {
#[serde(rename = "lastUpdate")] #[serde(rename = "lastUpdate")]
pub last_update: u64, pub last_update: u64,
pub packages: Vec<Package> pub packages: Vec<Package>,
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum UpdateResult { pub enum UpdateResult {
NotOutdated, NotOutdated,
@ -83,11 +94,11 @@ pub enum InstallResult {
MissingPackageVersion, MissingPackageVersion,
AlreadyInstalled, AlreadyInstalled,
Installed, Installed,
BlockedExternalPackage(String) BlockedExternalPackage(String),
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum RemoveResult { pub enum RemoveResult {
NotFound, NotFound,
Removed Removed,
} }

View File

@ -1,14 +1,14 @@
use tempfile::TempDir;
use std::error::Error;
use std::io::{Cursor, copy};
use std::{fs, io};
use log::debug; use log::debug;
use std::error::Error;
use std::io::{copy, Cursor};
use std::{fs, io};
use tempfile::TempDir;
pub struct ZipPackageResolver; pub struct ZipPackageResolver;
impl ZipPackageResolver { impl ZipPackageResolver {
pub fn new() -> ZipPackageResolver { pub fn new() -> ZipPackageResolver {
return ZipPackageResolver{}; return ZipPackageResolver {};
} }
} }
@ -54,10 +54,19 @@ impl super::PackageResolver for ZipPackageResolver {
} }
if (&*file.name()).ends_with('/') { if (&*file.name()).ends_with('/') {
debug!("File {} extracted to \"{}\"", i, outpath.as_path().display()); debug!(
"File {} extracted to \"{}\"",
i,
outpath.as_path().display()
);
fs::create_dir_all(&outpath).unwrap(); fs::create_dir_all(&outpath).unwrap();
} else { } else {
debug!("File {} extracted to \"{}\" ({} bytes)", i, outpath.as_path().display(), file.size()); debug!(
"File {} extracted to \"{}\" ({} bytes)",
i,
outpath.as_path().display(),
file.size()
);
if let Some(p) = outpath.parent() { if let Some(p) = outpath.parent() {
if !p.exists() { if !p.exists() {
fs::create_dir_all(&p).unwrap(); fs::create_dir_all(&p).unwrap();
@ -74,13 +83,15 @@ impl super::PackageResolver for ZipPackageResolver {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use super::super::PackageResolver; use super::super::PackageResolver;
use super::*;
#[test] #[test]
fn test_clone_temp_repository() { fn test_clone_temp_repository() {
let resolver = ZipPackageResolver::new(); let resolver = ZipPackageResolver::new();
let cloned_dir = resolver.clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core").unwrap(); let cloned_dir = resolver
.clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core")
.unwrap();
assert!(cloned_dir.path().join("LICENSE").exists()); assert!(cloned_dir.path().join("LICENSE").exists());
} }
} }

46
src/process.rs Normal file
View File

@ -0,0 +1,46 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
use log::warn;
use widestring::WideCString;
#[cfg(target_os = "windows")]
pub fn spawn_process(cmd: &str, args: &Vec<String>) {
let quoted_args: Vec<String> = args.iter().map(|arg| format!("\"{}\"", arg)).collect();
let quoted_args = quoted_args.join(" ");
let final_cmd = format!("\"{}\" {}", cmd, quoted_args);
unsafe {
let cmd_wstr = WideCString::from_str(&final_cmd);
if let Ok(string) = cmd_wstr {
let res = crate::bridge::windows::start_process(string.as_ptr());
if res < 0 {
warn!("unable to start process: {}", final_cmd);
}
} else {
warn!("unable to convert process string into wide format")
}
}
}
#[cfg(not(target_os = "windows"))]
pub fn spawn_process(cmd: &str, args: &Vec<String>) {
use std::process::{Command, Stdio};
Command::new(cmd).args(args).spawn();
}

View File

@ -17,14 +17,14 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde::{Deserialize, Serialize}; use crate::config::Configs;
use std::sync::mpsc::Sender;
use crate::event::Event;
use crate::event::ActionType; use crate::event::ActionType;
use std::io::{BufReader, Read, Write}; use crate::event::{Event, SystemEvent};
use std::error::Error;
use log::error; use log::error;
use crate::config::ConfigSet; use serde::{Deserialize, Serialize};
use std::error::Error;
use std::io::{BufReader, Read, Write};
use std::sync::mpsc::Sender;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod windows; mod windows;
@ -40,6 +40,13 @@ pub trait IPCClient {
fn send_command(&self, command: IPCCommand) -> Result<(), String>; 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)] #[derive(Serialize, Deserialize, Debug)]
pub struct IPCCommand { pub struct IPCCommand {
pub id: String, pub id: String,
@ -51,19 +58,71 @@ pub struct IPCCommand {
impl IPCCommand { impl IPCCommand {
fn to_event(&self) -> Option<Event> { fn to_event(&self) -> Option<Event> {
match self.id.as_ref() { match self.id.as_ref() {
"exit" => { "exit" => Some(Event::Action(ActionType::Exit)),
Some(Event::Action(ActionType::Exit)) "wexit" => Some(Event::Action(ActionType::ExitWorker)),
}, "toggle" => Some(Event::Action(ActionType::Toggle)),
"toggle" => { "enable" => Some(Event::Action(ActionType::Enable)),
Some(Event::Action(ActionType::Toggle)) "disable" => Some(Event::Action(ActionType::Disable)),
}, "restartworker" => Some(Event::Action(ActionType::RestartWorker)),
"enable" => { "notify" => Some(Event::System(SystemEvent::NotifyRequest(
Some(Event::Action(ActionType::Enable)) self.payload.clone(),
}, ))),
"disable" => { _ => None,
Some(Event::Action(ActionType::Disable)) }
}, }
_ => None
pub fn from(event: Event) -> Option<IPCCommand> {
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,
}),
_ => 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(),
} }
} }
} }
@ -71,22 +130,23 @@ impl IPCCommand {
fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) { fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
let mut json_str= String::new(); let mut json_str = String::new();
let mut buf_reader = BufReader::new(stream); let mut buf_reader = BufReader::new(stream);
let res = buf_reader.read_to_string(&mut json_str); let res = buf_reader.read_to_string(&mut json_str);
if res.is_ok() { if res.is_ok() {
let command : Result<IPCCommand, serde_json::Error> = serde_json::from_str(&json_str); let command: Result<IPCCommand, serde_json::Error> =
serde_json::from_str(&json_str);
match command { match command {
Ok(command) => { Ok(command) => {
let event = command.to_event(); let event = command.to_event();
if let Some(event) = event { if let Some(event) = event {
event_channel.send(event).expect("Broken event channel"); event_channel.send(event).expect("Broken event channel");
} }
}, }
Err(e) => { Err(e) => {
error!("Error deserializing JSON command: {}", e); error!("Error deserializing JSON command: {}", e);
}, }
} }
} }
} }
@ -96,7 +156,10 @@ fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Resul
} }
} }
fn send_command<W: Write, E: Error>(command: IPCCommand, stream: Result<W, E>) -> Result<(), String>{ fn send_command<W: Write, E: Error>(
command: IPCCommand,
stream: Result<W, E>,
) -> Result<(), String> {
match stream { match stream {
Ok(mut stream) => { Ok(mut stream) => {
let json_str = serde_json::to_string(&command); let json_str = serde_json::to_string(&command);
@ -104,35 +167,46 @@ fn send_command<W: Write, E: Error>(command: IPCCommand, stream: Result<W, E>) -
stream.write_all(json_str.as_bytes()).unwrap_or_else(|e| { stream.write_all(json_str.as_bytes()).unwrap_or_else(|e| {
println!("Can't write to IPC socket: {}", e); println!("Can't write to IPC socket: {}", e);
}); });
return Ok(()) return Ok(());
} }
},
Err(e) => {
return Err(format!("Can't connect to daemon: {}", e))
} }
Err(e) => return Err(format!("Can't connect to daemon: {}", e)),
} }
Err("Can't send command".to_owned()) Err("Can't send command".to_owned())
} }
pub enum Service {
Daemon,
Worker,
}
// UNIX IMPLEMENTATION // UNIX IMPLEMENTATION
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub fn get_ipc_server(_: ConfigSet, event_channel: Sender<Event>) -> impl IPCServer { pub fn get_ipc_server(
unix::UnixIPCServer::new(event_channel) service: Service,
_: Configs,
event_channel: Sender<Event>,
) -> impl IPCServer {
unix::UnixIPCServer::new(service, event_channel)
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub fn get_ipc_client(_: ConfigSet) -> impl IPCClient { pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
unix::UnixIPCClient::new() unix::UnixIPCClient::new(service)
} }
// WINDOWS IMPLEMENTATION // WINDOWS IMPLEMENTATION
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_ipc_server(config_set: ConfigSet, event_channel: Sender<Event>) -> impl IPCServer { pub fn get_ipc_server(
windows::WindowsIPCServer::new(config_set, event_channel) service: Service,
config: Configs,
event_channel: Sender<Event>,
) -> impl IPCServer {
windows::WindowsIPCServer::new(service, config, event_channel)
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_ipc_client(config_set: ConfigSet) -> impl IPCClient { pub fn get_ipc_client(service: Service, config: Configs) -> impl IPCClient {
windows::WindowsIPCClient::new(config_set) windows::WindowsIPCClient::new(service, config)
} }

View File

@ -17,66 +17,88 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::os::unix::net::{UnixStream,UnixListener};
use log::{info, warn};
use std::sync::mpsc::Sender;
use super::IPCCommand; 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::context;
use crate::event::*; use crate::event::*;
use crate::protocol::{process_event, send_command}; use crate::protocol::{process_event, send_command};
const UNIX_SOCKET_NAME : &str = "espanso.sock"; const DAEMON_UNIX_SOCKET_NAME: &str = "espanso.sock";
const WORKER_UNIX_SOCKET_NAME: &str = "worker.sock";
pub struct UnixIPCServer { pub struct UnixIPCServer {
service: Service,
event_channel: Sender<Event>, event_channel: Sender<Event>,
} }
impl UnixIPCServer { impl UnixIPCServer {
pub fn new(event_channel: Sender<Event>) -> UnixIPCServer { pub fn new(service: Service, event_channel: Sender<Event>) -> UnixIPCServer {
UnixIPCServer {event_channel} 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 { impl super::IPCServer for UnixIPCServer {
fn start(&self) { fn start(&self) {
let event_channel = self.event_channel.clone(); let event_channel = self.event_channel.clone();
std::thread::Builder::new().name("ipc_server".to_string()).spawn(move || { let socket_name = get_unix_name(&self.service);
let espanso_dir = context::get_data_dir(); std::thread::Builder::new()
let unix_socket = espanso_dir.join(UNIX_SOCKET_NAME); .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| { std::fs::remove_file(unix_socket.clone()).unwrap_or_else(|e| {
warn!("Unable to delete Unix socket: {}", e); warn!("Unable to delete Unix socket: {}", e);
}); });
let listener = UnixListener::bind(unix_socket.clone()).expect("Can't bind to Unix Socket"); 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()); info!(
"Binded to IPC unix socket: {}",
unix_socket.as_path().display()
);
for stream in listener.incoming() { for stream in listener.incoming() {
process_event(&event_channel, stream); process_event(&event_channel, stream);
} }
}).expect("Unable to spawn IPC server thread"); })
.expect("Unable to spawn IPC server thread");
} }
} }
pub struct UnixIPCClient { pub struct UnixIPCClient {
service: Service,
} }
impl UnixIPCClient { impl UnixIPCClient {
pub fn new() -> UnixIPCClient { pub fn new(service: Service) -> UnixIPCClient {
UnixIPCClient{} UnixIPCClient { service }
} }
} }
impl super::IPCClient for UnixIPCClient { impl super::IPCClient for UnixIPCClient {
fn send_command(&self, command: IPCCommand) -> Result<(), String> { fn send_command(&self, command: IPCCommand) -> Result<(), String> {
let espanso_dir = context::get_data_dir(); let espanso_dir = context::get_data_dir();
let unix_socket = espanso_dir.join(UNIX_SOCKET_NAME); let socket_name = get_unix_name(&self.service);
let unix_socket = espanso_dir.join(socket_name);
// Open the stream // Open the stream
let stream = UnixStream::connect(unix_socket); let stream = UnixStream::connect(unix_socket);
send_command(command, stream) send_command(command, stream)
} }
} }

View File

@ -17,60 +17,82 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use log::{info};
use std::sync::mpsc::Sender;
use std::net::{TcpListener, TcpStream};
use super::IPCCommand; use super::IPCCommand;
use log::info;
use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::Sender;
use crate::config::Configs;
use crate::event::*; use crate::event::*;
use crate::protocol::{process_event, send_command}; use crate::protocol::{process_event, send_command, Service};
use crate::config::ConfigSet;
pub struct WindowsIPCServer { pub struct WindowsIPCServer {
config_set: ConfigSet, service: Service,
config: Configs,
event_channel: Sender<Event>, event_channel: Sender<Event>,
} }
fn to_port(config: &Configs, service: &Service) -> u16 {
let port = match service {
Service::Daemon => config.ipc_server_port,
Service::Worker => config.worker_ipc_server_port,
};
port as u16
}
impl WindowsIPCServer { impl WindowsIPCServer {
pub fn new(config_set: ConfigSet, event_channel: Sender<Event>) -> WindowsIPCServer { pub fn new(
WindowsIPCServer {config_set, event_channel} service: Service,
config: Configs,
event_channel: Sender<Event>,
) -> WindowsIPCServer {
WindowsIPCServer {
service,
config,
event_channel,
}
} }
} }
impl super::IPCServer for WindowsIPCServer { impl super::IPCServer for WindowsIPCServer {
fn start(&self) { fn start(&self) {
let event_channel = self.event_channel.clone(); let event_channel = self.event_channel.clone();
let server_port = self.config_set.default.ipc_server_port; let server_port = to_port(&self.config, &self.service);
std::thread::Builder::new().name("ipc_server".to_string()).spawn(move || { std::thread::Builder::new()
let listener = TcpListener::bind( .name("ipc_server".to_string())
format!("127.0.0.1:{}", server_port) .spawn(move || {
).expect("Error binding to IPC server port"); let listener = TcpListener::bind(format!("127.0.0.1:{}", server_port))
.expect("Error binding to IPC server port");
info!("Binded to IPC tcp socket: {}", listener.local_addr().unwrap().to_string()); info!(
"Binded to IPC tcp socket: {}",
listener.local_addr().unwrap().to_string()
);
for stream in listener.incoming() { for stream in listener.incoming() {
process_event(&event_channel, stream); process_event(&event_channel, stream);
} }
}).expect("Unable to spawn IPC server thread"); })
.expect("Unable to spawn IPC server thread");
} }
} }
pub struct WindowsIPCClient { pub struct WindowsIPCClient {
config_set: ConfigSet, service: Service,
config: Configs,
} }
impl WindowsIPCClient { impl WindowsIPCClient {
pub fn new(config_set: ConfigSet) -> WindowsIPCClient { pub fn new(service: Service, config: Configs) -> WindowsIPCClient {
WindowsIPCClient{config_set} WindowsIPCClient { service, config }
} }
} }
impl super::IPCClient for WindowsIPCClient { impl super::IPCClient for WindowsIPCClient {
fn send_command(&self, command: IPCCommand) -> Result<(), String> { fn send_command(&self, command: IPCCommand) -> Result<(), String> {
let stream = TcpStream::connect( let port = to_port(&self.config, &self.service);
("127.0.0.1", self.config_set.default.ipc_server_port as u16) let stream = TcpStream::connect(("127.0.0.1", port));
);
send_command(command, stream) send_command(command, stream)
} }
} }

View File

@ -17,18 +17,18 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use serde_yaml::{Value};
use std::collections::HashMap;
use regex::{Regex, Captures};
use log::{warn, error};
use super::*; use super::*;
use crate::matcher::{Match, MatchContentType};
use crate::config::Configs; use crate::config::Configs;
use crate::extension::Extension; use crate::extension::Extension;
use crate::matcher::{Match, MatchContentType};
use log::{error, warn};
use regex::{Captures, Regex};
use serde_yaml::Value;
use std::collections::{HashMap, HashSet};
lazy_static! { lazy_static! {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap(); static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
static ref UNKNOWN_VARIABLE : String = "".to_string(); static ref UNKNOWN_VARIABLE: String = "".to_string();
} }
pub struct DefaultRenderer { pub struct DefaultRenderer {
@ -47,12 +47,11 @@ impl DefaultRenderer {
} }
// Compile the regexes // Compile the regexes
let passive_match_regex = Regex::new(&config.passive_match_regex) let passive_match_regex = Regex::new(&config.passive_match_regex).unwrap_or_else(|e| {
.unwrap_or_else(|e| { panic!("Invalid passive match regex: {:?}", e);
panic!("Invalid passive match regex: {:?}", e); });
});
DefaultRenderer{ DefaultRenderer {
extension_map, extension_map,
passive_match_regex, passive_match_regex,
} }
@ -69,7 +68,6 @@ impl DefaultRenderer {
break; break;
} }
} }
} }
result result
@ -77,40 +75,67 @@ impl DefaultRenderer {
} }
impl super::Renderer for DefaultRenderer { impl super::Renderer for DefaultRenderer {
fn render_match(&self, m: &Match, trigger_offset: usize, config: &Configs, args: Vec<String>) -> RenderResult { fn render_match(
&self,
m: &Match,
trigger_offset: usize,
config: &Configs,
args: Vec<String>,
) -> RenderResult {
// Manage the different types of matches // Manage the different types of matches
match &m.content { match &m.content {
// Text Match // Text Match
MatchContentType::Text(content) => { MatchContentType::Text(content) => {
let target_string = if content._has_vars || !config.global_vars.is_empty(){ // Find all the variables that are required by the current match
let mut target_vars = 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 target_string = if target_vars.len() > 0 {
let mut output_map = HashMap::new(); let mut output_map = HashMap::new();
// Cycle through both the local and global variables // Cycle through both the local and global variables
for variable in config.global_vars.iter().chain(&content.vars) { for variable in config.global_vars.iter().chain(&content.vars) {
// Skip all non-required variables
if !target_vars.contains(&variable.name) {
continue;
}
// In case of variables of type match, we need to recursively call // In case of variables of type match, we need to recursively call
// the render function // the render function
if variable.var_type == "match" { if variable.var_type == "match" {
// Extract the match trigger from the variable params // Extract the match trigger from the variable params
let trigger = variable.params.get(&Value::from("trigger")); let trigger = variable.params.get(&Value::from("trigger"));
if trigger.is_none() { if trigger.is_none() {
warn!("Missing param 'trigger' in match variable: {}", variable.name); warn!(
"Missing param 'trigger' in match variable: {}",
variable.name
);
continue; continue;
} }
let trigger = trigger.unwrap(); let trigger = trigger.unwrap();
// Find the given match from the active configs // Find the given match from the active configs
let inner_match = DefaultRenderer::find_match(config, trigger.as_str().unwrap_or("")); let inner_match =
DefaultRenderer::find_match(config, trigger.as_str().unwrap_or(""));
if inner_match.is_none() { if inner_match.is_none() {
warn!("Could not find inner match with trigger: '{}'", trigger.as_str().unwrap_or("undefined")); warn!(
continue "Could not find inner match with trigger: '{}'",
trigger.as_str().unwrap_or("undefined")
);
continue;
} }
let (inner_match, trigger_offset) = inner_match.unwrap(); let (inner_match, trigger_offset) = inner_match.unwrap();
// Render the inner match // Render the inner match
// TODO: inner arguments // TODO: inner arguments
let result = self.render_match(&inner_match, trigger_offset, config, vec![]); let result =
self.render_match(&inner_match, trigger_offset, config, vec![]);
// Inner matches are only supported for text-expansions, warn the user otherwise // Inner matches are only supported for text-expansions, warn the user otherwise
match result { match result {
@ -121,18 +146,25 @@ impl super::Renderer for DefaultRenderer {
warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.") warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.")
}, },
} }
}else{ // Normal extension variables } else {
// Normal extension variables
let extension = self.extension_map.get(&variable.var_type); let extension = self.extension_map.get(&variable.var_type);
if let Some(extension) = extension { if let Some(extension) = extension {
let ext_out = extension.calculate(&variable.params, &args); let ext_out = extension.calculate(&variable.params, &args);
if let Some(output) = ext_out { if let Some(output) = ext_out {
output_map.insert(variable.name.clone(), output); output_map.insert(variable.name.clone(), output);
}else{ } else {
output_map.insert(variable.name.clone(), "".to_owned()); output_map.insert(variable.name.clone(), "".to_owned());
warn!("Could not generate output for variable: {}", variable.name); warn!(
"Could not generate output for variable: {}",
variable.name
);
} }
}else{ } else {
error!("No extension found for variable type: {}", variable.var_type); error!(
"No extension found for variable type: {}",
variable.var_type
);
} }
} }
} }
@ -145,14 +177,14 @@ impl super::Renderer for DefaultRenderer {
}); });
result.to_string() result.to_string()
}else{ // No variables, simple text substitution } else {
// No variables, simple text substitution
content.replace.clone() content.replace.clone()
}; };
// Unescape any brackets (needed to be able to insert double brackets in replacement // Unescape any brackets (needed to be able to insert double brackets in replacement
// text, without triggering the variable system). See issue #187 // text, without triggering the variable system). See issue #187
let target_string = target_string.replace("\\{", "{") let target_string = target_string.replace("\\{", "{").replace("\\}", "}");
.replace("\\}", "}");
// Render any argument that may be present // Render any argument that may be present
let target_string = utils::render_args(&target_string, &args); let target_string = utils::render_args(&target_string, &args);
@ -160,23 +192,30 @@ impl super::Renderer for DefaultRenderer {
// Handle case propagation // Handle case propagation
let target_string = if m.propagate_case { let target_string = if m.propagate_case {
let trigger = &m.triggers[trigger_offset]; let trigger = &m.triggers[trigger_offset];
let first_char = trigger.chars().nth(0);
let second_char = trigger.chars().nth(1); // 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 { let mode: i32 = if let Some(first_char) = first_char {
if first_char.is_uppercase() { if first_char.is_uppercase() {
if let Some(second_char) = second_char { if let Some(second_char) = second_char {
if second_char.is_uppercase() { if second_char.is_uppercase() {
2 // Full CAPITALIZATION 2 // Full CAPITALIZATION
}else{ } else {
1 // Only first letter capitalized: Capitalization 1 // Only first letter capitalized: Capitalization
} }
}else{ } else {
2 // Single char, defaults to full CAPITALIZATION 2 // Single char, defaults to full CAPITALIZATION
} }
}else{ } else {
0 // Lowercase, no action 0 // Lowercase, no action
} }
}else{ } else {
0 0
}; };
@ -186,78 +225,79 @@ impl super::Renderer for DefaultRenderer {
let mut v: Vec<char> = target_string.chars().collect(); let mut v: Vec<char> = target_string.chars().collect();
v[0] = v[0].to_uppercase().nth(0).unwrap(); v[0] = v[0].to_uppercase().nth(0).unwrap();
v.into_iter().collect() v.into_iter().collect()
}, }
2 => { // Full capitalization 2 => {
// Full capitalization
target_string.to_uppercase() target_string.to_uppercase()
}, }
_ => { // Noop _ => {
// Noop
target_string target_string
} }
} }
}else{ } else {
target_string target_string
}; };
RenderResult::Text(target_string) RenderResult::Text(target_string)
}, }
// Image Match // Image Match
MatchContentType::Image(content) => { MatchContentType::Image(content) => {
// Make sure the image exist beforehand // Make sure the image exist beforehand
if content.path.exists() { if content.path.exists() {
RenderResult::Image(content.path.clone()) RenderResult::Image(content.path.clone())
}else{ } else {
error!("Image not found in path: {:?}", content.path); error!("Image not found in path: {:?}", content.path);
RenderResult::Error RenderResult::Error
} }
}, }
} }
} }
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult { fn render_passive(&self, text: &str, config: &Configs) -> RenderResult {
// Render the matches // Render the matches
let result = self.passive_match_regex.replace_all(&text, |caps: &Captures| { let result = self
let match_name = if let Some(name) = caps.name("name") { .passive_match_regex
name.as_str() .replace_all(&text, |caps: &Captures| {
}else{ 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();
// Get the original matching string, useful to return the match untouched // Find the corresponding match
let original_match = caps.get(0).unwrap().as_str(); let m = DefaultRenderer::find_match(config, match_name);
// Find the corresponding match // If no match is found, leave the match without modifications
let m = DefaultRenderer::find_match(config, match_name); if m.is_none() {
return original_match.to_owned();
// 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<String> = 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()
} }
}
}); // Compute the args by separating them
let match_args = if let Some(args) = caps.name("args") {
args.as_str()
} else {
""
};
let args: Vec<String> = 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()) RenderResult::Text(result.into_owned())
} }
@ -270,11 +310,14 @@ mod tests {
use super::*; use super::*;
fn get_renderer(config: Configs) -> DefaultRenderer { fn get_renderer(config: Configs) -> DefaultRenderer {
DefaultRenderer::new(vec![Box::new(crate::extension::dummy::DummyExtension::new())], config) DefaultRenderer::new(
vec![Box::new(crate::extension::dummy::DummyExtension::new())],
config,
)
} }
fn get_config_for(s: &str) -> Configs { fn get_config_for(s: &str) -> Configs {
let config : Configs = serde_yaml::from_str(s).unwrap(); let config: Configs = serde_yaml::from_str(s).unwrap();
config config
} }
@ -282,10 +325,8 @@ mod tests {
match rendered { match rendered {
RenderResult::Text(rendered) => { RenderResult::Text(rendered) => {
assert_eq!(rendered, target); assert_eq!(rendered, target);
},
_ => {
assert!(false)
} }
_ => assert!(false),
} }
} }
@ -295,11 +336,13 @@ mod tests {
this text contains no matches this text contains no matches
"###; "###;
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: test - trigger: test
replace: result replace: result
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -312,11 +355,13 @@ mod tests {
fn test_render_passive_simple_match_no_args() { fn test_render_passive_simple_match_no_args() {
let text = "this is a :test"; let text = "this is a :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':test' - trigger: ':test'
replace: result replace: result
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -329,11 +374,13 @@ mod tests {
fn test_render_passive_multiple_match_no_args() { fn test_render_passive_multiple_match_no_args() {
let text = "this is a :test and then another :test"; let text = "this is a :test and then another :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':test' - trigger: ':test'
replace: result replace: result
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -348,15 +395,17 @@ mod tests {
:test :test
"###; "###;
let result= r###"this is a let result = r###"this is a
result result
"###; "###;
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':test' - trigger: ':test'
replace: result replace: result
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -369,7 +418,8 @@ mod tests {
fn test_render_passive_nested_matches_no_args() { fn test_render_passive_nested_matches_no_args() {
let text = ":greet"; let text = ":greet";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':greet' - trigger: ':greet'
replace: "hi {{name}}" replace: "hi {{name}}"
@ -381,7 +431,8 @@ mod tests {
- trigger: ':name' - trigger: ':name'
replace: john replace: john
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -394,11 +445,13 @@ mod tests {
fn test_render_passive_simple_match_with_args() { fn test_render_passive_simple_match_with_args() {
let text = ":greet/Jon/"; let text = ":greet/Jon/";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':greet' - trigger: ':greet'
replace: "Hi $0$" replace: "Hi $0$"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -411,11 +464,13 @@ mod tests {
fn test_render_passive_simple_match_with_multiple_args() { fn test_render_passive_simple_match_with_multiple_args() {
let text = ":greet/Jon/Snow/"; let text = ":greet/Jon/Snow/";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':greet' - trigger: ':greet'
replace: "Hi $0$, there is $1$ outside" replace: "Hi $0$, there is $1$ outside"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -428,11 +483,13 @@ mod tests {
fn test_render_passive_simple_match_with_escaped_args() { fn test_render_passive_simple_match_with_escaped_args() {
let text = ":greet/Jon/10\\/12/"; let text = ":greet/Jon/10\\/12/";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':greet' - trigger: ':greet'
replace: "Hi $0$, today is $1$" replace: "Hi $0$, today is $1$"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -445,11 +502,13 @@ mod tests {
fn test_render_passive_simple_match_with_args_not_closed() { fn test_render_passive_simple_match_with_args_not_closed() {
let text = ":greet/Jon/Snow"; let text = ":greet/Jon/Snow";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':greet' - trigger: ':greet'
replace: "Hi $0$" replace: "Hi $0$"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -462,7 +521,8 @@ mod tests {
fn test_render_passive_local_var() { fn test_render_passive_local_var() {
let text = "this is :test"; let text = "this is :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':test' - trigger: ':test'
replace: "my {{output}}" replace: "my {{output}}"
@ -471,7 +531,8 @@ mod tests {
type: dummy type: dummy
params: params:
echo: "result" echo: "result"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -484,7 +545,8 @@ mod tests {
fn test_render_passive_global_var() { fn test_render_passive_global_var() {
let text = "this is :test"; let text = "this is :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
global_vars: global_vars:
- name: output - name: output
type: dummy type: dummy
@ -494,7 +556,8 @@ mod tests {
- trigger: ':test' - trigger: ':test'
replace: "my {{output}}" replace: "my {{output}}"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -507,7 +570,8 @@ mod tests {
fn test_render_passive_global_var_is_overridden_by_local() { fn test_render_passive_global_var_is_overridden_by_local() {
let text = "this is :test"; let text = "this is :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
global_vars: global_vars:
- name: output - name: output
type: dummy type: dummy
@ -522,7 +586,8 @@ mod tests {
params: params:
echo: "local" echo: "local"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -535,11 +600,13 @@ mod tests {
fn test_render_match_with_unknown_variable_does_not_crash() { fn test_render_match_with_unknown_variable_does_not_crash() {
let text = "this is :test"; let text = "this is :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':test' - trigger: ':test'
replace: "my {{unknown}}" replace: "my {{unknown}}"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -552,11 +619,13 @@ mod tests {
fn test_render_escaped_double_brackets_should_not_consider_them_variable() { fn test_render_escaped_double_brackets_should_not_consider_them_variable() {
let text = "this is :test"; let text = "this is :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: ':test' - trigger: ':test'
replace: "my \\{\\{unknown\\}\\}" replace: "my \\{\\{unknown\\}\\}"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -569,11 +638,13 @@ mod tests {
fn test_render_passive_simple_match_multi_trigger_no_args() { fn test_render_passive_simple_match_multi_trigger_no_args() {
let text = "this is a :yolo and :test"; let text = "this is a :yolo and :test";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- triggers: [':test', ':yolo'] - triggers: [':test', ':yolo']
replace: result replace: result
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -586,11 +657,13 @@ mod tests {
fn test_render_passive_simple_match_multi_trigger_with_args() { fn test_render_passive_simple_match_multi_trigger_with_args() {
let text = ":yolo/Jon/"; let text = ":yolo/Jon/";
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- triggers: [':greet', ':yolo'] - triggers: [':greet', ':yolo']
replace: "Hi $0$" replace: "Hi $0$"
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
@ -601,18 +674,20 @@ mod tests {
#[test] #[test]
fn test_render_match_case_propagation_no_case() { fn test_render_match_case_propagation_no_case() {
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: 'test' - trigger: 'test'
replace: result replace: result
propagate_case: true propagate_case: true
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
let m = config.matches[0].clone(); let m = config.matches[0].clone();
let trigger_offset = m.triggers.iter().position(|x| x== "test").unwrap(); let trigger_offset = m.triggers.iter().position(|x| x == "test").unwrap();
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]); let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
@ -621,18 +696,20 @@ mod tests {
#[test] #[test]
fn test_render_match_case_propagation_first_capital() { fn test_render_match_case_propagation_first_capital() {
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: 'test' - trigger: 'test'
replace: result replace: result
propagate_case: true propagate_case: true
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
let m = config.matches[0].clone(); let m = config.matches[0].clone();
let trigger_offset = m.triggers.iter().position(|x| x== "Test").unwrap(); let trigger_offset = m.triggers.iter().position(|x| x == "Test").unwrap();
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]); let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
@ -641,21 +718,23 @@ mod tests {
#[test] #[test]
fn test_render_match_case_propagation_all_capital() { fn test_render_match_case_propagation_all_capital() {
let config = get_config_for(r###" let config = get_config_for(
r###"
matches: matches:
- trigger: 'test' - trigger: 'test'
replace: result replace: result
propagate_case: true propagate_case: true
"###); "###,
);
let renderer = get_renderer(config.clone()); let renderer = get_renderer(config.clone());
let m = config.matches[0].clone(); let m = config.matches[0].clone();
let trigger_offset = m.triggers.iter().position(|x| x== "TEST").unwrap(); let trigger_offset = m.triggers.iter().position(|x| x == "TEST").unwrap();
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]); let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
verify_render(rendered, "RESULT"); verify_render(rendered, "RESULT");
} }
} }

View File

@ -17,16 +17,22 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::path::PathBuf;
use crate::matcher::{Match};
use crate::config::Configs; use crate::config::Configs;
use crate::matcher::Match;
use std::path::PathBuf;
pub(crate) mod default; pub(crate) mod default;
pub(crate) mod utils; pub(crate) mod utils;
pub trait Renderer { pub trait Renderer {
// Render a match output // Render a match output
fn render_match(&self, m: &Match, trigger_offset: usize, config: &Configs, args: Vec<String>) -> RenderResult; fn render_match(
&self,
m: &Match,
trigger_offset: usize,
config: &Configs,
args: Vec<String>,
) -> RenderResult;
// Render a passive expansion text // Render a passive expansion text
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult; fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
@ -35,5 +41,5 @@ pub trait Renderer {
pub enum RenderResult { pub enum RenderResult {
Text(String), Text(String),
Image(PathBuf), Image(PathBuf),
Error Error,
} }

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use regex::{Regex, Captures}; use regex::{Captures, Regex};
lazy_static! { lazy_static! {
static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap(); static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap();
@ -25,12 +25,12 @@ lazy_static! {
pub fn render_args(text: &str, args: &Vec<String>) -> String { pub fn render_args(text: &str, args: &Vec<String>) -> String {
let result = ARG_REGEX.replace_all(text, |caps: &Captures| { let result = ARG_REGEX.replace_all(text, |caps: &Captures| {
let position_str = caps.name("pos").unwrap().as_str(); let position_str = caps.name("pos").unwrap().as_str();
let position = position_str.parse::<i32>().unwrap_or(-1); let position = position_str.parse::<i32>().unwrap_or(-1);
if position >= 0 && position < args.len() as i32 { if position >= 0 && position < args.len() as i32 {
args[position as usize].to_owned() args[position as usize].to_owned()
}else{ } else {
"".to_owned() "".to_owned()
} }
}); });
@ -43,24 +43,24 @@ pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec<String> {
// Make sure the text is not empty // Make sure the text is not empty
if text.is_empty() { if text.is_empty() {
return output return output;
} }
let mut last = String::from(""); let mut last = String::from("");
let mut previous : char = char::from(0); let mut previous: char = char::from(0);
text.chars().into_iter().for_each(|c| { text.chars().into_iter().for_each(|c| {
if c == delimiter { if c == delimiter {
if previous != escape { if previous != escape {
output.push(last.clone()); output.push(last.clone());
last = String::from(""); last = String::from("");
}else{ } else {
last.push(c); last.push(c);
} }
}else if c == escape { } else if c == escape {
if previous == escape { if previous == escape {
last.push(c); last.push(c);
} }
}else{ } else {
last.push(c); last.push(c);
} }
previous = c; previous = c;
@ -80,25 +80,28 @@ mod tests {
#[test] #[test]
fn test_render_args_no_args() { fn test_render_args_no_args() {
let args = vec!("hello".to_owned()); let args = vec!["hello".to_owned()];
assert_eq!(render_args("no args", &args), "no args") assert_eq!(render_args("no args", &args), "no args")
} }
#[test] #[test]
fn test_render_args_one_arg() { fn test_render_args_one_arg() {
let args = vec!("jon".to_owned()); let args = vec!["jon".to_owned()];
assert_eq!(render_args("hello $0$", &args), "hello jon") assert_eq!(render_args("hello $0$", &args), "hello jon")
} }
#[test] #[test]
fn test_render_args_one_multiple_args() { fn test_render_args_one_multiple_args() {
let args = vec!("jon".to_owned(), "snow".to_owned()); 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") assert_eq!(
render_args("hello $0$, the $1$ is white", &args),
"hello jon, the snow is white"
)
} }
#[test] #[test]
fn test_render_args_out_of_range() { fn test_render_args_out_of_range() {
let args = vec!("jon".to_owned()); let args = vec!["jon".to_owned()];
assert_eq!(render_args("hello $10$", &args), "hello ") assert_eq!(render_args("hello $10$", &args), "hello ")
} }
@ -124,7 +127,7 @@ mod tests {
#[test] #[test]
fn test_split_args_empty() { fn test_split_args_empty() {
let empty_vec : Vec<String> = vec![]; let empty_vec: Vec<String> = vec![];
assert_eq!(split_args("", '/', '\\'), empty_vec) assert_eq!(split_args("", '/', '\\'), empty_vec)
} }
} }

View File

@ -20,14 +20,14 @@
// This functions are used to register/unregister espanso from the system daemon manager. // This functions are used to register/unregister espanso from the system daemon manager.
use crate::config::ConfigSet; use crate::config::ConfigSet;
use crate::sysdaemon::VerifyResult::{EnabledAndValid, NotEnabled, EnabledButInvalidPath}; use crate::sysdaemon::VerifyResult::{EnabledAndValid, EnabledButInvalidPath, NotEnabled};
// INSTALLATION // INSTALLATION
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const MAC_PLIST_CONTENT : &str = include_str!("res/mac/com.federicoterzi.espanso.plist"); const MAC_PLIST_CONTENT: &str = include_str!("res/mac/com.federicoterzi.espanso.plist");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const MAC_PLIST_FILENAME : &str = "com.federicoterzi.espanso.plist"; const MAC_PLIST_FILENAME: &str = "com.federicoterzi.espanso.plist";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn register(_config_set: ConfigSet) { pub fn register(_config_set: ConfigSet) {
@ -45,13 +45,21 @@ pub fn register(_config_set: ConfigSet) {
let plist_file = agents_dir.join(MAC_PLIST_FILENAME); let plist_file = agents_dir.join(MAC_PLIST_FILENAME);
if !plist_file.exists() { if !plist_file.exists() {
println!("Creating LaunchAgents entry: {}", plist_file.to_str().unwrap_or_default()); 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"); 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()); println!(
"Entry will point to: {}",
espanso_path.to_str().unwrap_or_default()
);
let plist_content = String::from(MAC_PLIST_CONTENT) let plist_content = String::from(MAC_PLIST_CONTENT).replace(
.replace("{{{espanso_path}}}", espanso_path.to_str().unwrap_or_default()); "{{{espanso_path}}}",
espanso_path.to_str().unwrap_or_default(),
);
std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file"); std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file");
@ -61,8 +69,8 @@ pub fn register(_config_set: ConfigSet) {
println!("Reloading entry..."); println!("Reloading entry...");
let res = Command::new("launchctl") let res = Command::new("launchctl")
.args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()])
.output(); .output();
let res = Command::new("launchctl") let res = Command::new("launchctl")
.args(&["load", "-w", plist_file.to_str().unwrap_or_default()]) .args(&["load", "-w", plist_file.to_str().unwrap_or_default()])
@ -72,7 +80,7 @@ pub fn register(_config_set: ConfigSet) {
if status.success() { if status.success() {
println!("Entry loaded correctly!") println!("Entry loaded correctly!")
} }
}else{ } else {
println!("Error loading new entry"); println!("Error loading new entry");
} }
} }
@ -95,7 +103,7 @@ pub fn unregister(_config_set: ConfigSet) {
std::fs::remove_file(&plist_file).expect("Could not remove espanso entry"); std::fs::remove_file(&plist_file).expect("Could not remove espanso entry");
println!("Entry removed correctly!") println!("Entry removed correctly!")
}else{ } else {
println!("espanso is not installed"); println!("espanso is not installed");
} }
} }
@ -103,14 +111,14 @@ pub fn unregister(_config_set: ConfigSet) {
// LINUX // LINUX
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const LINUX_SERVICE_CONTENT : &str = include_str!("res/linux/systemd.service"); const LINUX_SERVICE_CONTENT: &str = include_str!("res/linux/systemd.service");
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const LINUX_SERVICE_FILENAME : &str = "espanso.service"; const LINUX_SERVICE_FILENAME: &str = "espanso.service";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn register(_: ConfigSet) { pub fn register(_: ConfigSet) {
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::process::{Command}; use std::process::Command;
// Check if espanso service is already registered // Check if espanso service is already registered
let res = Command::new("systemctl") let res = Command::new("systemctl")
@ -126,7 +134,7 @@ pub fn register(_: ConfigSet) {
eprintln!(" espanso unregister"); eprintln!(" espanso unregister");
std::process::exit(5); std::process::exit(5);
} }
}else{ } else {
if output == "disabled" { if output == "disabled" {
use dialoguer::Confirmation; use dialoguer::Confirmation;
if !Confirmation::new() if !Confirmation::new()
@ -154,15 +162,24 @@ pub fn register(_: ConfigSet) {
let service_file = user_dir.join(LINUX_SERVICE_FILENAME); let service_file = user_dir.join(LINUX_SERVICE_FILENAME);
if !service_file.exists() { if !service_file.exists() {
println!("Creating service entry: {}", service_file.to_str().unwrap_or_default()); 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"); 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()); println!(
"Entry will point to: {}",
espanso_path.to_str().unwrap_or_default()
);
let service_content = String::from(LINUX_SERVICE_CONTENT) let service_content = String::from(LINUX_SERVICE_CONTENT).replace(
.replace("{{{espanso_path}}}", espanso_path.to_str().unwrap_or_default()); "{{{espanso_path}}}",
espanso_path.to_str().unwrap_or_default(),
);
std::fs::write(service_file.clone(), service_content).expect("Unable to write service file"); std::fs::write(service_file.clone(), service_content)
.expect("Unable to write service file");
println!("Service file created correctly!") println!("Service file created correctly!")
} }
@ -177,7 +194,7 @@ pub fn register(_: ConfigSet) {
if status.success() { if status.success() {
println!("Service registered correctly!") println!("Service registered correctly!")
} }
}else{ } else {
println!("Error loading espanso service"); println!("Error loading espanso service");
} }
} }
@ -191,7 +208,7 @@ pub enum VerifyResult {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn verify() -> VerifyResult { pub fn verify() -> VerifyResult {
use regex::Regex; use regex::Regex;
use std::process::{Command}; use std::process::Command;
// Check if espanso service is already registered // Check if espanso service is already registered
let res = Command::new("systemctl") let res = Command::new("systemctl")
@ -201,7 +218,7 @@ pub fn verify() -> VerifyResult {
let output = String::from_utf8_lossy(res.stdout.as_slice()); let output = String::from_utf8_lossy(res.stdout.as_slice());
let output = output.trim(); let output = output.trim();
if !res.status.success() || output != "enabled" { if !res.status.success() || output != "enabled" {
return NotEnabled return NotEnabled;
} }
} }
@ -219,10 +236,11 @@ pub fn verify() -> VerifyResult {
if res.status.success() { if res.status.success() {
let caps = EXEC_PATH_REGEX.captures(output).unwrap(); let caps = EXEC_PATH_REGEX.captures(output).unwrap();
let path = caps.get(1).map_or("", |m| m.as_str()); 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"); let espanso_path =
std::env::current_exe().expect("Could not get espanso executable path");
if espanso_path.to_string_lossy() != path { if espanso_path.to_string_lossy() != path {
return EnabledButInvalidPath return EnabledButInvalidPath;
} }
} }
} }
@ -232,12 +250,13 @@ pub fn verify() -> VerifyResult {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn unregister(_: ConfigSet) { pub fn unregister(_: ConfigSet) {
use std::process::{Command}; use std::process::Command;
// Disable the service first // Disable the service first
Command::new("systemctl") Command::new("systemctl")
.args(&["--user", "disable", "espanso"]) .args(&["--user", "disable", "espanso"])
.status().expect("Unable to invoke systemctl"); .status()
.expect("Unable to invoke systemctl");
// Then delete the espanso.service entry // Then delete the espanso.service entry
let config_dir = dirs::config_dir().expect("Could not get configuration directory"); let config_dir = dirs::config_dir().expect("Could not get configuration directory");
@ -251,13 +270,16 @@ pub fn unregister(_: ConfigSet) {
Ok(_) => { Ok(_) => {
println!("Deleted entry at {}", service_file.to_string_lossy()); println!("Deleted entry at {}", service_file.to_string_lossy());
println!("Service unregistered successfully!"); println!("Service unregistered successfully!");
}, }
Err(e) => { Err(e) => {
println!("Error, could not delete service entry at {} with error {}", println!(
service_file.to_string_lossy(), e); "Error, could not delete service entry at {} with error {}",
}, service_file.to_string_lossy(),
e
);
}
} }
}else{ } else {
eprintln!("Error, could not find espanso service file"); eprintln!("Error, could not find espanso service file");
} }
} }
@ -272,4 +294,4 @@ pub fn register(_config_set: ConfigSet) {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn unregister(_config_set: ConfigSet) { pub fn unregister(_config_set: ConfigSet) {
println!("Windows does not support automatic system daemon integration.") println!("Windows does not support automatic system daemon integration.")
} }

View File

@ -19,7 +19,9 @@
use std::os::raw::c_char; use std::os::raw::c_char;
use crate::bridge::linux::{get_active_window_name, get_active_window_class, get_active_window_executable}; use crate::bridge::linux::{
get_active_window_class, get_active_window_executable, get_active_window_name,
};
use std::ffi::CStr; use std::ffi::CStr;
pub struct LinuxSystemManager {} pub struct LinuxSystemManager {}
@ -27,7 +29,7 @@ pub struct LinuxSystemManager {}
impl super::SystemManager for LinuxSystemManager { impl super::SystemManager for LinuxSystemManager {
fn get_current_window_title(&self) -> Option<String> { fn get_current_window_title(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [c_char; 100] = [0; 100]; let mut buffer: [c_char; 100] = [0; 100];
let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -39,13 +41,13 @@ impl super::SystemManager for LinuxSystemManager {
} }
} }
} }
None None
} }
fn get_current_window_class(&self) -> Option<String> { fn get_current_window_class(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [c_char; 100] = [0; 100]; let mut buffer: [c_char; 100] = [0; 100];
let res = get_active_window_class(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_window_class(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -63,7 +65,7 @@ impl super::SystemManager for LinuxSystemManager {
fn get_current_window_executable(&self) -> Option<String> { fn get_current_window_executable(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [c_char; 100] = [0; 100]; let mut buffer: [c_char; 100] = [0; 100];
let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -82,6 +84,6 @@ impl super::SystemManager for LinuxSystemManager {
impl LinuxSystemManager { impl LinuxSystemManager {
pub fn new() -> LinuxSystemManager { pub fn new() -> LinuxSystemManager {
LinuxSystemManager{} LinuxSystemManager {}
} }
} }

View File

@ -19,12 +19,12 @@
use std::os::raw::c_char; 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; use std::ffi::CStr;
use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier, get_secure_input_process, get_path_from_pid};
pub struct MacSystemManager { pub struct MacSystemManager {}
}
impl super::SystemManager for MacSystemManager { impl super::SystemManager for MacSystemManager {
fn get_current_window_title(&self) -> Option<String> { fn get_current_window_title(&self) -> Option<String> {
@ -33,7 +33,7 @@ impl super::SystemManager for MacSystemManager {
fn get_current_window_class(&self) -> Option<String> { fn get_current_window_class(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [c_char; 250] = [0; 250]; let mut buffer: [c_char; 250] = [0; 250];
let res = get_active_app_identifier(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_app_identifier(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -51,7 +51,7 @@ impl super::SystemManager for MacSystemManager {
fn get_current_window_executable(&self) -> Option<String> { fn get_current_window_executable(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [c_char; 250] = [0; 250]; let mut buffer: [c_char; 250] = [0; 250];
let res = get_active_app_bundle(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_app_bundle(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -70,9 +70,7 @@ impl super::SystemManager for MacSystemManager {
impl MacSystemManager { impl MacSystemManager {
pub fn new() -> MacSystemManager { pub fn new() -> MacSystemManager {
MacSystemManager{ MacSystemManager {}
}
} }
/// Check whether an application is currently holding the Secure Input. /// Check whether an application is currently holding the Secure Input.
@ -82,9 +80,9 @@ impl MacSystemManager {
let mut pid: i64 = -1; let mut pid: i64 = -1;
let res = get_secure_input_process(&mut pid as *mut i64); let res = get_secure_input_process(&mut pid as *mut i64);
if res > 0{ if res > 0 {
Some(pid) Some(pid)
}else{ } else {
None None
} }
} }
@ -105,7 +103,7 @@ impl MacSystemManager {
if let Some(pid) = pid { if let Some(pid) = pid {
// Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant. // 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. // the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently.
let mut buffer : [c_char; 4096] = [0; 4096]; let mut buffer: [c_char; 4096] = [0; 4096];
let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32); let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -117,23 +115,23 @@ impl MacSystemManager {
let caps = APP_REGEX.captures(&process); let caps = APP_REGEX.captures(&process);
let app_name = if let Some(caps) = caps { let app_name = if let Some(caps) = caps {
caps.get(1).map_or("", |m| m.as_str()).to_owned() caps.get(1).map_or("", |m| m.as_str()).to_owned()
}else{ } else {
process.to_owned() process.to_owned()
}; };
Some((app_name, process)) Some((app_name, process))
}else{ } else {
None None
} }
}else{ } else {
None None
} }
}else{ } else {
None None
} }
}else{ } else {
None None
} }
} }
} }
} }

View File

@ -48,4 +48,4 @@ pub fn get_manager() -> impl SystemManager {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn get_manager() -> impl SystemManager { pub fn get_manager() -> impl SystemManager {
macos::MacSystemManager::new() macos::MacSystemManager::new()
} }

View File

@ -17,23 +17,21 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use widestring::U16CString;
use crate::bridge::windows::*; use crate::bridge::windows::*;
use widestring::U16CString;
pub struct WindowsSystemManager { pub struct WindowsSystemManager {}
}
impl WindowsSystemManager { impl WindowsSystemManager {
pub fn new() -> WindowsSystemManager { pub fn new() -> WindowsSystemManager {
WindowsSystemManager{} WindowsSystemManager {}
} }
} }
impl super::SystemManager for WindowsSystemManager { impl super::SystemManager for WindowsSystemManager {
fn get_current_window_title(&self) -> Option<String> { fn get_current_window_title(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [u16; 100] = [0; 100]; let mut buffer: [u16; 100] = [0; 100];
let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -53,7 +51,7 @@ impl super::SystemManager for WindowsSystemManager {
fn get_current_window_executable(&self) -> Option<String> { fn get_current_window_executable(&self) -> Option<String> {
unsafe { unsafe {
let mut buffer : [u16; 250] = [0; 250]; let mut buffer: [u16; 250] = [0; 250];
let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32); let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32);
if res > 0 { if res > 0 {
@ -66,4 +64,4 @@ impl super::SystemManager for WindowsSystemManager {
None None
} }
} }

View File

@ -17,12 +17,12 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::process::Command;
use super::MenuItem; use super::MenuItem;
use log::{error, info}; use log::{error, info};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
const LINUX_ICON_CONTENT : &[u8] = include_bytes!("../res/linux/icon.png"); const LINUX_ICON_CONTENT: &[u8] = include_bytes!("../res/linux/icon.png");
pub struct LinuxUIManager { pub struct LinuxUIManager {
icon_path: PathBuf, icon_path: PathBuf,
@ -35,8 +35,14 @@ impl super::UIManager for LinuxUIManager {
fn notify_delay(&self, message: &str, duration: i32) { fn notify_delay(&self, message: &str, duration: i32) {
let res = Command::new("notify-send") let res = Command::new("notify-send")
.args(&["-i", self.icon_path.to_str().unwrap_or_default(), .args(&[
"-t", &duration.to_string(), "espanso", message]) "-i",
self.icon_path.to_str().unwrap_or_default(),
"-t",
&duration.to_string(),
"espanso",
message,
])
.output(); .output();
if let Err(e) = res { if let Err(e) = res {
@ -59,12 +65,13 @@ impl LinuxUIManager {
let data_dir = crate::context::get_data_dir(); let data_dir = crate::context::get_data_dir();
let icon_path = data_dir.join("icon.png"); let icon_path = data_dir.join("icon.png");
if !icon_path.exists() { if !icon_path.exists() {
info!("Creating espanso icon in '{}'", icon_path.to_str().unwrap_or_default()); 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"); std::fs::write(&icon_path, LINUX_ICON_CONTENT).expect("Unable to copy espanso icon");
} }
LinuxUIManager{ LinuxUIManager { icon_path }
icon_path
}
} }
} }

View File

@ -17,21 +17,21 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{fs, io}; use crate::bridge::macos::{show_context_menu, MacMenuItem};
use std::io::{Cursor}; use crate::context;
use crate::ui::{MenuItem, MenuItemType};
use log::{debug, info, warn};
use std::ffi::CString; use std::ffi::CString;
use log::{info, warn, debug}; use std::io::Cursor;
use std::os::raw::c_char;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use crate::ui::{MenuItem, MenuItemType}; use std::{fs, io};
use crate::bridge::macos::{MacMenuItem, show_context_menu};
use std::os::raw::c_char;
use crate::context;
const NOTIFY_HELPER_BINARY : &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip"); const NOTIFY_HELPER_BINARY: &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip");
pub struct MacUIManager { pub struct MacUIManager {
notify_helper_path: PathBuf notify_helper_path: PathBuf,
} }
impl super::UIManager for MacUIManager { impl super::UIManager for MacUIManager {
@ -60,14 +60,14 @@ impl super::UIManager for MacUIManager {
for item in menu.iter() { for item in menu.iter() {
let text = CString::new(item.item_name.clone()).unwrap_or_default(); let text = CString::new(item.item_name.clone()).unwrap_or_default();
let mut str_buff : [c_char; 100] = [0; 100]; let mut str_buff: [c_char; 100] = [0; 100];
unsafe { unsafe {
std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), item.item_name.len()); std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), item.item_name.len());
} }
let menu_type = match item.item_type { let menu_type = match item.item_type {
MenuItemType::Button => {1}, MenuItemType::Button => 1,
MenuItemType::Separator => {2}, MenuItemType::Separator => 2,
}; };
let raw_item = MacMenuItem { let raw_item = MacMenuItem {
@ -79,7 +79,9 @@ impl super::UIManager for MacUIManager {
raw_menu.push(raw_item); raw_menu.push(raw_item);
} }
unsafe { show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32); } unsafe {
show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32);
}
} }
fn cleanup(&self) { fn cleanup(&self) {
@ -91,21 +93,22 @@ impl MacUIManager {
pub fn new() -> MacUIManager { pub fn new() -> MacUIManager {
let notify_helper_path = MacUIManager::initialize_notify_helper(); let notify_helper_path = MacUIManager::initialize_notify_helper();
MacUIManager{ MacUIManager { notify_helper_path }
notify_helper_path
}
} }
fn initialize_notify_helper() -> PathBuf { fn initialize_notify_helper() -> PathBuf {
let espanso_dir = context::get_data_dir(); let espanso_dir = context::get_data_dir();
info!("Initializing EspansoNotifyHelper in {}", espanso_dir.as_path().display()); info!(
"Initializing EspansoNotifyHelper in {}",
espanso_dir.as_path().display()
);
let espanso_target = espanso_dir.join("EspansoNotifyHelper.app"); let espanso_target = espanso_dir.join("EspansoNotifyHelper.app");
if espanso_target.exists() { if espanso_target.exists() {
info!("EspansoNotifyHelper already initialized, skipping."); info!("EspansoNotifyHelper already initialized, skipping.");
}else{ } else {
// Extract zip file // Extract zip file
let reader = Cursor::new(NOTIFY_HELPER_BINARY); let reader = Cursor::new(NOTIFY_HELPER_BINARY);
@ -123,10 +126,19 @@ impl MacUIManager {
} }
if (&*file.name()).ends_with('/') { if (&*file.name()).ends_with('/') {
debug!("File {} extracted to \"{}\"", i, outpath.as_path().display()); debug!(
"File {} extracted to \"{}\"",
i,
outpath.as_path().display()
);
fs::create_dir_all(&outpath).unwrap(); fs::create_dir_all(&outpath).unwrap();
} else { } else {
debug!("File {} extracted to \"{}\" ({} bytes)", i, outpath.as_path().display(), file.size()); debug!(
"File {} extracted to \"{}\" ({} bytes)",
i,
outpath.as_path().display(),
file.size()
);
if let Some(p) = outpath.parent() { if let Some(p) = outpath.parent() {
if !p.exists() { if !p.exists() {
fs::create_dir_all(&p).unwrap(); fs::create_dir_all(&p).unwrap();
@ -146,4 +158,4 @@ impl MacUIManager {
espanso_target espanso_target
} }
} }

View File

@ -60,4 +60,4 @@ pub fn get_uimanager() -> impl UIManager {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_uimanager() -> impl UIManager { pub fn get_uimanager() -> impl UIManager {
windows::WindowsUIManager::new() windows::WindowsUIManager::new()
} }

View File

@ -17,16 +17,18 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::bridge::windows::{show_notification, close_notification, WindowsMenuItem, show_context_menu, cleanup_ui}; use crate::bridge::windows::{
use widestring::U16CString; cleanup_ui, close_notification, show_context_menu, show_notification, WindowsMenuItem,
use std::{thread, time}; };
use log::{debug};
use std::sync::Mutex;
use std::sync::Arc;
use crate::ui::{MenuItem, MenuItemType}; 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 { pub struct WindowsUIManager {
id: Arc<Mutex<i32>> id: Arc<Mutex<i32>>,
} }
impl super::UIManager for WindowsUIManager { impl super::UIManager for WindowsUIManager {
@ -45,29 +47,30 @@ impl super::UIManager for WindowsUIManager {
// Setup a timeout to close the notification // Setup a timeout to close the notification
let id = Arc::clone(&self.id); let id = Arc::clone(&self.id);
let _ = thread::Builder::new().name("notification_thread".to_string()).spawn(move || { let _ = thread::Builder::new()
for _ in 1..10 { .name("notification_thread".to_string())
let duration = time::Duration::from_millis(step as u64); .spawn(move || {
thread::sleep(duration); for _ in 1..10 {
let duration = time::Duration::from_millis(step as u64);
thread::sleep(duration);
let new_id = id.lock().unwrap(); let new_id = id.lock().unwrap();
if *new_id != current_id { if *new_id != current_id {
debug!("Cancelling notification close event with id {}", current_id); debug!("Cancelling notification close event with id {}", current_id);
return; return;
}
} }
}
unsafe { unsafe {
close_notification(); close_notification();
} }
}); });
// Create and show a window notification // Create and show a window notification
unsafe { unsafe {
let message = U16CString::from_str(message).unwrap(); let message = U16CString::from_str(message).unwrap();
show_notification(message.as_ptr()); show_notification(message.as_ptr());
} }
} }
fn show_menu(&self, menu: Vec<MenuItem>) { fn show_menu(&self, menu: Vec<MenuItem>) {
@ -75,14 +78,14 @@ impl super::UIManager for WindowsUIManager {
for item in menu.iter() { for item in menu.iter() {
let text = U16CString::from_str(item.item_name.clone()).unwrap_or_default(); let text = U16CString::from_str(item.item_name.clone()).unwrap_or_default();
let mut str_buff : [u16; 100] = [0; 100]; let mut str_buff: [u16; 100] = [0; 100];
unsafe { unsafe {
std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), text.len()); std::ptr::copy(text.as_ptr(), str_buff.as_mut_ptr(), text.len());
} }
let menu_type = match item.item_type { let menu_type = match item.item_type {
MenuItemType::Button => {1}, MenuItemType::Button => 1,
MenuItemType::Separator => {2}, MenuItemType::Separator => 2,
}; };
let raw_item = WindowsMenuItem { let raw_item = WindowsMenuItem {
@ -94,7 +97,9 @@ impl super::UIManager for WindowsUIManager {
raw_menu.push(raw_item); raw_menu.push(raw_item);
} }
unsafe { show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32); } unsafe {
show_context_menu(raw_menu.as_ptr(), raw_menu.len() as i32);
}
} }
fn cleanup(&self) { fn cleanup(&self) {
@ -108,10 +113,8 @@ impl WindowsUIManager {
pub fn new() -> WindowsUIManager { pub fn new() -> WindowsUIManager {
let id = Arc::new(Mutex::new(0)); let id = Arc::new(Mutex::new(0));
let manager = WindowsUIManager { let manager = WindowsUIManager { id };
id
};
manager manager
} }
} }

View File

@ -17,9 +17,9 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::path::Path;
use std::error::Error; use std::error::Error;
use std::fs::create_dir; use std::fs::create_dir;
use std::path::Path;
pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>> { pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>> {
for entry in std::fs::read_dir(source_dir)? { for entry in std::fs::read_dir(source_dir)? {
@ -30,8 +30,9 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>
let target_dir = dest_dir.join(name); let target_dir = dest_dir.join(name);
create_dir(&target_dir)?; create_dir(&target_dir)?;
copy_dir(&entry, &target_dir)?; copy_dir(&entry, &target_dir)?;
}else if entry.is_file() { } else if entry.is_file() {
let target_entry = dest_dir.join(entry.file_name().expect("Error obtaining the filename")); let target_entry =
dest_dir.join(entry.file_name().expect("Error obtaining the filename"));
std::fs::copy(entry, target_entry)?; std::fs::copy(entry, target_entry)?;
} }
} }
@ -42,8 +43,8 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use tempfile::TempDir;
use std::fs::create_dir; use std::fs::create_dir;
use tempfile::TempDir;
#[test] #[test]
fn test_copy_dir_into() { fn test_copy_dir_into() {
@ -88,7 +89,9 @@ mod tests {
assert!(dest_tmp_dir.path().join("source/file2.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").exists());
assert!(dest_tmp_dir.path().join("source/nested/nestedfile.txt").exists()); assert!(dest_tmp_dir
.path()
.join("source/nested/nestedfile.txt")
.exists());
} }
}
}