commit
0d60a40aae
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -36,3 +36,5 @@ DerivedData
|
||||||
*.snap
|
*.snap
|
||||||
|
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
.vscode/
|
51
Cargo.lock
generated
51
Cargo.lock
generated
|
@ -18,7 +18,7 @@ name = "ansi_term"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -45,7 +45,7 @@ version = "0.2.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
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)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -181,7 +181,7 @@ dependencies = [
|
||||||
"atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
"atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"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)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -212,7 +212,7 @@ dependencies = [
|
||||||
"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)",
|
||||||
"termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -334,7 +334,7 @@ dependencies = [
|
||||||
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -371,7 +371,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.6.3"
|
version = "0.7.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)",
|
||||||
|
@ -397,6 +397,7 @@ dependencies = [
|
||||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"zip 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"zip 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -428,7 +429,7 @@ dependencies = [
|
||||||
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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)",
|
"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)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -465,7 +466,7 @@ version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
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)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -808,7 +809,7 @@ name = "named_pipe"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -835,7 +836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -857,7 +858,7 @@ dependencies = [
|
||||||
"mio 0.6.19 (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)",
|
"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)",
|
"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)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -941,7 +942,7 @@ dependencies = [
|
||||||
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1028,7 +1029,7 @@ dependencies = [
|
||||||
"rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1113,7 +1114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
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)",
|
||||||
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1126,7 +1127,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)",
|
||||||
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1191,7 +1192,7 @@ name = "remove_dir_all"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1269,7 +1270,7 @@ version = "0.1.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1461,7 +1462,7 @@ dependencies = [
|
||||||
"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)",
|
||||||
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
|
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1470,7 +1471,7 @@ version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1504,7 +1505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
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)",
|
||||||
"redox_syscall 0.1.56 (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)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1731,7 +1732,7 @@ version = "2.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1762,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.8"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -1784,7 +1785,7 @@ name = "winapi-util"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1797,7 +1798,7 @@ name = "winreg"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2028,7 +2029,7 @@ dependencies = [
|
||||||
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
|
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
|
||||||
"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
|
"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
|
||||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||||
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
|
"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
|
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.6.3"
|
version = "0.7.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"
|
||||||
|
@ -9,6 +9,9 @@ homepage = "https://github.com/federico-terzi/espanso"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
build="build.rs"
|
build="build.rs"
|
||||||
|
|
||||||
|
[modulo]
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
widestring = "0.4.0"
|
widestring = "0.4.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -38,6 +41,7 @@ signal-hook = "0.1.15"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
named_pipe = "0.4.1"
|
named_pipe = "0.4.1"
|
||||||
|
winapi = { version = "0.3.9", features = ["wincon"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cmake = "0.1.31"
|
cmake = "0.1.31"
|
||||||
|
|
|
@ -76,6 +76,11 @@ void send_multi_vkey(int32_t vk, int32_t count);
|
||||||
*/
|
*/
|
||||||
void delete_string(int32_t count);
|
void delete_string(int32_t count);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed
|
||||||
|
*/
|
||||||
|
int32_t are_modifiers_pressed();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Trigger normal paste ( Pressing CMD+V )
|
* Trigger normal paste ( Pressing CMD+V )
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -235,6 +235,14 @@ void trigger_copy() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t are_modifiers_pressed() {
|
||||||
|
if ((NSEventModifierFlagControl | NSEventModifierFlagOption |
|
||||||
|
NSEventModifierFlagCommand | NSEventModifierFlagShift) & [NSEvent modifierFlags]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int32_t get_active_app_bundle(char * buffer, int32_t size) {
|
int32_t get_active_app_bundle(char * buffer, int32_t size) {
|
||||||
NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||||
NSString *bundlePath = [frontApp bundleURL].path;
|
NSString *bundlePath = [frontApp bundleURL].path;
|
||||||
|
|
|
@ -62,6 +62,7 @@ HWND nw = NULL;
|
||||||
HWND hwnd_st_u = NULL;
|
HWND hwnd_st_u = NULL;
|
||||||
HBITMAP g_espanso_bmp = NULL;
|
HBITMAP g_espanso_bmp = NULL;
|
||||||
HICON g_espanso_ico = NULL;
|
HICON g_espanso_ico = NULL;
|
||||||
|
HICON g_espanso_red_ico = NULL;
|
||||||
NOTIFYICONDATA nid = {};
|
NOTIFYICONDATA nid = {};
|
||||||
|
|
||||||
UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
|
UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
|
||||||
|
@ -309,13 +310,14 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path, int32_t _show_icon) {
|
int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t _show_icon) {
|
||||||
manager_instance = self;
|
manager_instance = self;
|
||||||
show_icon = _show_icon;
|
show_icon = _show_icon;
|
||||||
|
|
||||||
// Load the images
|
// Load the images
|
||||||
g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
|
g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
|
||||||
g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
|
g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
|
||||||
|
g_espanso_red_ico = (HICON)LoadImage(NULL, red_ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
|
||||||
|
|
||||||
// Make the notification capable of handling different screen definitions
|
// Make the notification capable of handling different screen definitions
|
||||||
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
|
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
|
||||||
|
@ -472,6 +474,19 @@ void eventloop() {
|
||||||
// Something went wrong, this should have been an infinite loop.
|
// Something went wrong, this should have been an infinite loop.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void update_tray_icon(int32_t enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
nid.hIcon = g_espanso_ico;
|
||||||
|
}else{
|
||||||
|
nid.hIcon = g_espanso_red_ico;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the icon
|
||||||
|
if (show_icon) {
|
||||||
|
Shell_NotifyIcon(NIM_MODIFY, &nid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Type the given string simulating keyboard presses.
|
* Type the given string simulating keyboard presses.
|
||||||
*/
|
*/
|
||||||
|
@ -730,7 +745,7 @@ int32_t start_daemon_process() {
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
FALSE,
|
FALSE,
|
||||||
DETACHED_PROCESS,
|
DETACHED_PROCESS | CREATE_NO_WINDOW,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
&si,
|
&si,
|
||||||
|
|
|
@ -33,7 +33,7 @@ extern void * manager_instance;
|
||||||
* Initialize the Windows parameters
|
* Initialize the Windows parameters
|
||||||
* return: 1 if OK, -1 otherwise.
|
* return: 1 if OK, -1 otherwise.
|
||||||
*/
|
*/
|
||||||
extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path, int32_t show_icon);
|
extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t show_icon);
|
||||||
|
|
||||||
#define LEFT_VARIANT 1
|
#define LEFT_VARIANT 1
|
||||||
#define RIGHT_VARIANT 2
|
#define RIGHT_VARIANT 2
|
||||||
|
@ -152,6 +152,11 @@ extern "C" int32_t show_notification(wchar_t * message);
|
||||||
*/
|
*/
|
||||||
extern "C" void close_notification();
|
extern "C" void close_notification();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update the tray icon status
|
||||||
|
*/
|
||||||
|
extern "C" void update_tray_icon(int32_t enabled);
|
||||||
|
|
||||||
// CLIPBOARD
|
// CLIPBOARD
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
48
packager.py
48
packager.py
|
@ -20,6 +20,7 @@ class PackageInfo:
|
||||||
description: str
|
description: str
|
||||||
publisher: str
|
publisher: str
|
||||||
url: str
|
url: str
|
||||||
|
modulo_version: str
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
|
@ -44,7 +45,8 @@ def build(skipcargo):
|
||||||
cargo_info["package"]["version"],
|
cargo_info["package"]["version"],
|
||||||
cargo_info["package"]["description"],
|
cargo_info["package"]["description"],
|
||||||
cargo_info["package"]["authors"][0],
|
cargo_info["package"]["authors"][0],
|
||||||
cargo_info["package"]["homepage"])
|
cargo_info["package"]["homepage"],
|
||||||
|
cargo_info["modulo"]["version"])
|
||||||
print(package_info)
|
print(package_info)
|
||||||
|
|
||||||
if not skipcargo:
|
if not skipcargo:
|
||||||
|
@ -58,6 +60,11 @@ def build(skipcargo):
|
||||||
elif TARGET_OS == "macos":
|
elif TARGET_OS == "macos":
|
||||||
build_mac(package_info)
|
build_mac(package_info)
|
||||||
|
|
||||||
|
def calculate_sha256(file):
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
b = f.read() # read entire file as bytes
|
||||||
|
readable_hash = hashlib.sha256(b).hexdigest()
|
||||||
|
return readable_hash
|
||||||
|
|
||||||
def build_windows(package_info):
|
def build_windows(package_info):
|
||||||
print("Starting packaging process for Windows...")
|
print("Starting packaging process for Windows...")
|
||||||
|
@ -78,6 +85,22 @@ def build_windows(package_info):
|
||||||
TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win")
|
TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win")
|
||||||
os.makedirs(TARGET_DIR, exist_ok=True)
|
os.makedirs(TARGET_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
modulo_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe".format(package_info.modulo_version)
|
||||||
|
modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe.sha256.txt".format(package_info.modulo_version)
|
||||||
|
print("Pulling modulo depencency from:", modulo_url)
|
||||||
|
modulo_target_file = os.path.join(TARGET_DIR, "modulo.exe")
|
||||||
|
urllib.request.urlretrieve(modulo_url, modulo_target_file)
|
||||||
|
print("Pulling SHA signature from:", modulo_sha_url)
|
||||||
|
modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256")
|
||||||
|
urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file)
|
||||||
|
print("Checking signatures...")
|
||||||
|
expected_sha = None
|
||||||
|
with open(modulo_sha_file, "r") as sha_f:
|
||||||
|
expected_sha = sha_f.read()
|
||||||
|
actual_sha = calculate_sha256(modulo_target_file)
|
||||||
|
if actual_sha != expected_sha:
|
||||||
|
raise Exception("Modulo SHA256 is not matching")
|
||||||
|
|
||||||
print("Gathering CRT DLLs...")
|
print("Gathering CRT DLLs...")
|
||||||
msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
|
msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
|
||||||
print("Found Redists: ", msvc_dirs)
|
print("Found Redists: ", msvc_dirs)
|
||||||
|
@ -104,12 +127,15 @@ def build_windows(package_info):
|
||||||
dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll")
|
dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll")
|
||||||
|
|
||||||
print("Found DLLs:")
|
print("Found DLLs:")
|
||||||
dll_include_list = []
|
include_list = []
|
||||||
for dll in dll_files:
|
for dll in dll_files:
|
||||||
print("Including: "+dll)
|
print("Including: "+dll)
|
||||||
dll_include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
|
include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
|
||||||
|
|
||||||
dll_include = "\r\n".join(dll_include_list)
|
print("Including modulo")
|
||||||
|
include_list.append("Source: \""+os.path.abspath(modulo_target_file)+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
|
||||||
|
|
||||||
|
include = "\r\n".join(include_list)
|
||||||
|
|
||||||
INSTALLER_NAME = f"espanso-win-installer"
|
INSTALLER_NAME = f"espanso-win-installer"
|
||||||
|
|
||||||
|
@ -130,7 +156,7 @@ def build_windows(package_info):
|
||||||
content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe"))
|
content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe"))
|
||||||
content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR))
|
content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR))
|
||||||
content = content.replace("{{{output_name}}}", INSTALLER_NAME)
|
content = content.replace("{{{output_name}}}", INSTALLER_NAME)
|
||||||
content = content.replace("{{{dll_include}}}", dll_include)
|
content = content.replace("{{{dll_include}}}", include)
|
||||||
|
|
||||||
with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script:
|
with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script:
|
||||||
output_script.write(content)
|
output_script.write(content)
|
||||||
|
@ -185,6 +211,16 @@ def build_mac(package_info):
|
||||||
with open(hash_file, "w") as hf:
|
with open(hash_file, "w") as hf:
|
||||||
hf.write(sha256_hash.hexdigest())
|
hf.write(sha256_hash.hexdigest())
|
||||||
|
|
||||||
|
modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-mac.sha256.txt".format(package_info.modulo_version)
|
||||||
|
print("Pulling SHA signature from:", modulo_sha_url)
|
||||||
|
modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256")
|
||||||
|
urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file)
|
||||||
|
modulo_sha = None
|
||||||
|
with open(modulo_sha_file, "r") as sha_f:
|
||||||
|
modulo_sha = sha_f.read()
|
||||||
|
if modulo_sha is None:
|
||||||
|
raise Exception("Cannot determine modulo SHA")
|
||||||
|
|
||||||
print("Processing Homebrew formula template")
|
print("Processing Homebrew formula template")
|
||||||
with open("packager/mac/espanso.rb", "r") as formula_template:
|
with open("packager/mac/espanso.rb", "r") as formula_template:
|
||||||
content = formula_template.read()
|
content = formula_template.read()
|
||||||
|
@ -193,6 +229,8 @@ def build_mac(package_info):
|
||||||
content = content.replace("{{{app_desc}}}", package_info.description)
|
content = content.replace("{{{app_desc}}}", package_info.description)
|
||||||
content = content.replace("{{{app_url}}}", package_info.url)
|
content = content.replace("{{{app_url}}}", package_info.url)
|
||||||
content = content.replace("{{{app_version}}}", package_info.version)
|
content = content.replace("{{{app_version}}}", package_info.version)
|
||||||
|
content = content.replace("{{{modulo_version}}}", package_info.modulo_version)
|
||||||
|
content = content.replace("{{{modulo_sha}}}", modulo_sha)
|
||||||
|
|
||||||
# Calculate hash
|
# Calculate hash
|
||||||
with open(archive_target, "rb") as f:
|
with open(archive_target, "rb") as f:
|
||||||
|
|
|
@ -4,11 +4,18 @@
|
||||||
class Espanso < Formula
|
class Espanso < Formula
|
||||||
desc "{{{app_desc}}}"
|
desc "{{{app_desc}}}"
|
||||||
homepage "{{{app_url}}}"
|
homepage "{{{app_url}}}"
|
||||||
url "https://github.com/federico-terzi/espanso/releases/latest/download/espanso-mac.tar.gz"
|
url "https://github.com/federico-terzi/espanso/releases/v{{{app_version}}}/download/espanso-mac.tar.gz"
|
||||||
sha256 "{{{release_hash}}}"
|
sha256 "{{{release_hash}}}"
|
||||||
version "{{{app_version}}}"
|
version "{{{app_version}}}"
|
||||||
|
|
||||||
|
resource "modulo" do
|
||||||
|
url "https://github.com/federico-terzi/modulo/releases/download/v{{{modulo_version}}}/modulo-mac"
|
||||||
|
sha256 "{{{modulo_sha}}}"
|
||||||
|
end
|
||||||
|
|
||||||
def install
|
def install
|
||||||
bin.install "espanso"
|
bin.install "espanso"
|
||||||
|
|
||||||
|
resource("modulo").stage { bin.install "modulo-mac" => "modulo" }
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 0.6.3
|
version: 0.7.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.
|
||||||
|
|
|
@ -63,4 +63,5 @@ extern "C" {
|
||||||
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();
|
||||||
|
pub fn are_modifiers_pressed() -> i32;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ extern "C" {
|
||||||
pub fn initialize(
|
pub fn initialize(
|
||||||
s: *const c_void,
|
s: *const c_void,
|
||||||
ico_path: *const u16,
|
ico_path: *const u16,
|
||||||
|
red_ico_path: *const u16,
|
||||||
bmp_path: *const u16,
|
bmp_path: *const u16,
|
||||||
show_icon: i32,
|
show_icon: i32,
|
||||||
) -> i32;
|
) -> i32;
|
||||||
|
@ -48,6 +49,7 @@ extern "C" {
|
||||||
pub fn register_icon_click_callback(cb: extern "C" 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 "C" 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();
|
||||||
|
pub fn update_tray_icon(enabled: i32);
|
||||||
|
|
||||||
// CLIPBOARD
|
// CLIPBOARD
|
||||||
pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
|
pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
|
||||||
|
|
|
@ -95,6 +95,9 @@ fn default_passive_arg_delimiter() -> char {
|
||||||
fn default_passive_arg_escape() -> char {
|
fn default_passive_arg_escape() -> char {
|
||||||
'\\'
|
'\\'
|
||||||
}
|
}
|
||||||
|
fn default_passive_delay() -> u64 {
|
||||||
|
100
|
||||||
|
}
|
||||||
fn default_passive_key() -> KeyModifier {
|
fn default_passive_key() -> KeyModifier {
|
||||||
KeyModifier::OFF
|
KeyModifier::OFF
|
||||||
}
|
}
|
||||||
|
@ -131,6 +134,9 @@ fn default_show_notifications() -> bool {
|
||||||
fn default_auto_restart() -> bool {
|
fn default_auto_restart() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
fn default_undo_backspace() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
fn default_show_icon() -> bool {
|
fn default_show_icon() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -146,6 +152,12 @@ fn default_matches() -> Vec<Match> {
|
||||||
fn default_global_vars() -> Vec<MatchVariable> {
|
fn default_global_vars() -> Vec<MatchVariable> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
fn default_modulo_path() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn default_mac_post_inject_delay() -> u64 {
|
||||||
|
100
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Configs {
|
pub struct Configs {
|
||||||
|
@ -206,12 +218,18 @@ pub struct Configs {
|
||||||
#[serde(default = "default_passive_key")]
|
#[serde(default = "default_passive_key")]
|
||||||
pub passive_key: KeyModifier,
|
pub passive_key: KeyModifier,
|
||||||
|
|
||||||
|
#[serde(default = "default_passive_delay")]
|
||||||
|
pub passive_delay: u64,
|
||||||
|
|
||||||
#[serde(default = "default_enable_passive")]
|
#[serde(default = "default_enable_passive")]
|
||||||
pub enable_passive: bool,
|
pub enable_passive: bool,
|
||||||
|
|
||||||
#[serde(default = "default_enable_active")]
|
#[serde(default = "default_enable_active")]
|
||||||
pub enable_active: bool,
|
pub enable_active: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_undo_backspace")]
|
||||||
|
pub undo_backspace: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub paste_shortcut: PasteShortcut,
|
pub paste_shortcut: PasteShortcut,
|
||||||
|
|
||||||
|
@ -227,6 +245,9 @@ pub struct Configs {
|
||||||
#[serde(default = "default_secure_input_watcher_interval")]
|
#[serde(default = "default_secure_input_watcher_interval")]
|
||||||
pub secure_input_watcher_interval: i32,
|
pub secure_input_watcher_interval: i32,
|
||||||
|
|
||||||
|
#[serde(default = "default_mac_post_inject_delay")]
|
||||||
|
pub mac_post_inject_delay: u64,
|
||||||
|
|
||||||
#[serde(default = "default_secure_input_notification")]
|
#[serde(default = "default_secure_input_notification")]
|
||||||
pub secure_input_notification: bool,
|
pub secure_input_notification: bool,
|
||||||
|
|
||||||
|
@ -259,6 +280,9 @@ pub struct Configs {
|
||||||
|
|
||||||
#[serde(default = "default_global_vars")]
|
#[serde(default = "default_global_vars")]
|
||||||
pub global_vars: Vec<MatchVariable>,
|
pub global_vars: Vec<MatchVariable>,
|
||||||
|
|
||||||
|
#[serde(default = "default_modulo_path")]
|
||||||
|
pub modulo_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Macro used to validate config fields
|
// Macro used to validate config fields
|
||||||
|
|
|
@ -48,6 +48,16 @@ pub fn new(
|
||||||
macos::MacContext::new(config, send_channel, is_injecting)
|
macos::MacContext::new(config, send_channel, is_injecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn update_icon(enabled: bool) {
|
||||||
|
// TODO: add update icon on macOS
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn get_icon_path() -> Option<PathBuf> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
// LINUX IMPLEMENTATION
|
// LINUX IMPLEMENTATION
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
@ -58,6 +68,16 @@ pub fn new(
|
||||||
linux::LinuxContext::new(config, send_channel, is_injecting)
|
linux::LinuxContext::new(config, send_channel, is_injecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn update_icon(enabled: bool) {
|
||||||
|
// No icon on Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn get_icon_path() -> Option<PathBuf> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
// WINDOWS IMPLEMENTATION
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
@ -68,6 +88,16 @@ pub fn new(
|
||||||
windows::WindowsContext::new(config, send_channel, is_injecting)
|
windows::WindowsContext::new(config, send_channel, is_injecting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn update_icon(enabled: bool) {
|
||||||
|
windows::update_icon(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn get_icon_path() -> Option<PathBuf> {
|
||||||
|
Some(windows::get_icon_path(&get_data_dir()))
|
||||||
|
}
|
||||||
|
|
||||||
// espanso directories
|
// espanso directories
|
||||||
|
|
||||||
static WARING_INIT: Once = Once::new();
|
static WARING_INIT: Once = Once::new();
|
||||||
|
|
|
@ -28,10 +28,12 @@ use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::Ordering::Acquire;
|
use std::sync::atomic::Ordering::Acquire;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use widestring::{U16CStr, U16CString};
|
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");
|
||||||
|
const RED_ICO_BINARY: &[u8] = include_bytes!("../res/win/espansored.ico");
|
||||||
|
|
||||||
pub struct WindowsContext {
|
pub struct WindowsContext {
|
||||||
send_channel: Sender<Event>,
|
send_channel: Sender<Event>,
|
||||||
|
@ -65,7 +67,7 @@ impl WindowsContext {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let espanso_ico_image = espanso_dir.join("espanso.ico");
|
let espanso_ico_image = get_icon_path(&espanso_dir);
|
||||||
if espanso_ico_image.exists() {
|
if espanso_ico_image.exists() {
|
||||||
info!("ICO already initialized, skipping.");
|
info!("ICO already initialized, skipping.");
|
||||||
} else {
|
} else {
|
||||||
|
@ -77,8 +79,22 @@ impl WindowsContext {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let espanso_red_ico_image = espanso_dir.join("espansored.ico");
|
||||||
|
if espanso_red_ico_image.exists() {
|
||||||
|
info!("red ICO already initialized, skipping.");
|
||||||
|
} else {
|
||||||
|
fs::write(&espanso_red_ico_image, RED_ICO_BINARY)
|
||||||
|
.expect("Unable to write windows ico file");
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Extracted 'red ico' icon to: {}",
|
||||||
|
espanso_red_ico_image.to_str().unwrap_or("error")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default();
|
let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default();
|
||||||
let ico_icon = espanso_ico_image.to_str().unwrap_or_default();
|
let ico_icon = espanso_ico_image.to_str().unwrap_or_default();
|
||||||
|
let red_ico_icon = espanso_red_ico_image.to_str().unwrap_or_default();
|
||||||
|
|
||||||
let send_channel = send_channel;
|
let send_channel = send_channel;
|
||||||
|
|
||||||
|
@ -96,6 +112,7 @@ impl WindowsContext {
|
||||||
register_context_menu_click_callback(context_menu_click_callback);
|
register_context_menu_click_callback(context_menu_click_callback);
|
||||||
|
|
||||||
let ico_file_c = U16CString::from_str(ico_icon).unwrap();
|
let ico_file_c = U16CString::from_str(ico_icon).unwrap();
|
||||||
|
let red_ico_file_c = U16CString::from_str(red_ico_icon).unwrap();
|
||||||
let bmp_file_c = U16CString::from_str(bmp_icon).unwrap();
|
let bmp_file_c = U16CString::from_str(bmp_icon).unwrap();
|
||||||
|
|
||||||
let show_icon = if config.show_icon { 1 } else { 0 };
|
let show_icon = if config.show_icon { 1 } else { 0 };
|
||||||
|
@ -104,6 +121,7 @@ impl WindowsContext {
|
||||||
let res = initialize(
|
let res = initialize(
|
||||||
context_ptr,
|
context_ptr,
|
||||||
ico_file_c.as_ptr(),
|
ico_file_c.as_ptr(),
|
||||||
|
red_ico_file_c.as_ptr(),
|
||||||
bmp_file_c.as_ptr(),
|
bmp_file_c.as_ptr(),
|
||||||
show_icon,
|
show_icon,
|
||||||
);
|
);
|
||||||
|
@ -124,8 +142,18 @@ impl super::Context for WindowsContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_icon_path(espanso_dir: &Path) -> PathBuf {
|
||||||
|
espanso_dir.join("espanso.ico")
|
||||||
|
}
|
||||||
|
|
||||||
// Native bridge code
|
// Native bridge code
|
||||||
|
|
||||||
|
pub fn update_icon(enabled: bool) {
|
||||||
|
unsafe {
|
||||||
|
crate::bridge::windows::update_tray_icon(if enabled { 1 } else { 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" fn keypress_callback(
|
extern "C" fn keypress_callback(
|
||||||
_self: *mut c_void,
|
_self: *mut c_void,
|
||||||
raw_buffer: *const u16,
|
raw_buffer: *const u16,
|
||||||
|
|
192
src/engine.rs
192
src/engine.rs
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
use crate::clipboard::ClipboardManager;
|
use crate::clipboard::ClipboardManager;
|
||||||
use crate::config::BackendType;
|
use crate::config::BackendType;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::{ConfigManager, Configs};
|
||||||
use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
|
use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
|
||||||
use crate::keyboard::KeyboardManager;
|
use crate::keyboard::KeyboardManager;
|
||||||
use crate::matcher::{Match, MatchReceiver};
|
use crate::matcher::{Match, MatchReceiver};
|
||||||
|
@ -50,6 +50,8 @@ pub struct Engine<
|
||||||
is_injecting: Arc<AtomicBool>,
|
is_injecting: Arc<AtomicBool>,
|
||||||
|
|
||||||
enabled: RefCell<bool>,
|
enabled: RefCell<bool>,
|
||||||
|
// Trigger string and injected text len pair
|
||||||
|
last_expansion_data: RefCell<Option<(String, i32)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<
|
||||||
|
@ -70,6 +72,7 @@ impl<
|
||||||
is_injecting: Arc<AtomicBool>,
|
is_injecting: Arc<AtomicBool>,
|
||||||
) -> Engine<'a, S, C, M, U, R> {
|
) -> Engine<'a, S, C, M, U, R> {
|
||||||
let enabled = RefCell::new(true);
|
let enabled = RefCell::new(true);
|
||||||
|
let last_expansion_data = RefCell::new(None);
|
||||||
|
|
||||||
Engine {
|
Engine {
|
||||||
keyboard_manager,
|
keyboard_manager,
|
||||||
|
@ -79,6 +82,7 @@ impl<
|
||||||
renderer,
|
renderer,
|
||||||
is_injecting,
|
is_injecting,
|
||||||
enabled,
|
enabled,
|
||||||
|
last_expansion_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,17 +151,61 @@ impl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inject_text(&self, config: &Configs, target_string: &str, force_clipboard: bool) {
|
||||||
|
let backend = if force_clipboard {
|
||||||
|
&BackendType::Clipboard
|
||||||
|
} else if config.backend == BackendType::Auto {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
||||||
|
if all_ascii {
|
||||||
|
debug!("All elements of the replacement are ascii, using Inject backend");
|
||||||
|
&BackendType::Inject
|
||||||
|
} else {
|
||||||
|
debug!("There are non-ascii characters, using Clipboard backend");
|
||||||
|
&BackendType::Clipboard
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&BackendType::Inject
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&config.backend
|
||||||
|
};
|
||||||
|
|
||||||
|
match backend {
|
||||||
|
BackendType::Inject => {
|
||||||
|
// To handle newlines, substitute each "\n" char with an Enter key press.
|
||||||
|
let splits = target_string.split('\n');
|
||||||
|
|
||||||
|
for (i, split) in splits.enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
self.keyboard_manager.send_enter(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keyboard_manager.send_string(&config, split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendType::Clipboard => {
|
||||||
|
self.clipboard_manager.set_clipboard(&target_string);
|
||||||
|
self.keyboard_manager.trigger_paste(&config);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Unsupported backend type evaluation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn inject_match(
|
fn inject_match(
|
||||||
&self,
|
&self,
|
||||||
m: &Match,
|
m: &Match,
|
||||||
trailing_separator: Option<char>,
|
trailing_separator: Option<char>,
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
skip_delete: bool,
|
skip_delete: bool,
|
||||||
) {
|
) -> Option<(String, i32)> {
|
||||||
let config = self.config_manager.active_config();
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
if !config.enable_active {
|
if !config.enable_active {
|
||||||
return;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block espanso from reinterpreting its own actions
|
// Block espanso from reinterpreting its own actions
|
||||||
|
@ -179,6 +227,8 @@ impl<
|
||||||
.renderer
|
.renderer
|
||||||
.render_match(m, trigger_offset, config, vec![]);
|
.render_match(m, trigger_offset, config, vec![]);
|
||||||
|
|
||||||
|
let mut expansion_data: Option<(String, i32)> = None;
|
||||||
|
|
||||||
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
|
||||||
|
@ -213,53 +263,18 @@ impl<
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let backend = if m.force_clipboard {
|
// If the preserve_clipboard option is enabled, save the current
|
||||||
&BackendType::Clipboard
|
// clipboard content to restore it later.
|
||||||
} else if config.backend == BackendType::Auto {
|
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
|
||||||
if all_ascii {
|
|
||||||
debug!(
|
|
||||||
"All elements of the replacement are ascii, using Inject backend"
|
|
||||||
);
|
|
||||||
&BackendType::Inject
|
|
||||||
} else {
|
|
||||||
debug!("There are non-ascii characters, using Clipboard backend");
|
|
||||||
&BackendType::Clipboard
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&BackendType::Inject
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&config.backend
|
|
||||||
};
|
|
||||||
|
|
||||||
match backend {
|
self.inject_text(&config, &target_string, m.force_clipboard);
|
||||||
BackendType::Inject => {
|
|
||||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
|
||||||
let splits = target_string.split('\n');
|
|
||||||
|
|
||||||
for (i, split) in splits.enumerate() {
|
// Disallow undo backspace if cursor positioning is used
|
||||||
if i > 0 {
|
if cursor_rewind.is_none() {
|
||||||
self.keyboard_manager.send_enter(&config);
|
expansion_data = Some((
|
||||||
}
|
m.triggers[trigger_offset].clone(),
|
||||||
|
target_string.chars().count() as i32,
|
||||||
self.keyboard_manager.send_string(&config, split);
|
));
|
||||||
}
|
|
||||||
}
|
|
||||||
BackendType::Clipboard => {
|
|
||||||
// If the preserve_clipboard option is enabled, save the current
|
|
||||||
// clipboard content to restore it later.
|
|
||||||
previous_clipboard_content =
|
|
||||||
self.return_content_if_preserve_clipboard_is_enabled();
|
|
||||||
|
|
||||||
self.clipboard_manager.set_clipboard(&target_string);
|
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("Unsupported backend type evaluation.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(moves) = cursor_rewind {
|
if let Some(moves) = cursor_rewind {
|
||||||
|
@ -292,8 +307,19 @@ impl<
|
||||||
.set_clipboard(&previous_clipboard_content);
|
.set_clipboard(&previous_clipboard_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On macOS, because the keyinjection is async, we need to wait a bit before
|
||||||
|
// giving back the control. Otherwise, the injected actions will be handled back
|
||||||
|
// by espanso itself.
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(
|
||||||
|
config.mac_post_inject_delay,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Re-allow espanso to interpret actions
|
// Re-allow espanso to interpret actions
|
||||||
self.is_injecting.store(false, Release);
|
self.is_injecting.store(false, Release);
|
||||||
|
|
||||||
|
expansion_data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +337,27 @@ impl<
|
||||||
> MatchReceiver for Engine<'a, S, C, M, U, R>
|
> 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) {
|
||||||
self.inject_match(m, trailing_separator, trigger_offset, false);
|
let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false);
|
||||||
|
let mut last_expansion_data = self.last_expansion_data.borrow_mut();
|
||||||
|
(*last_expansion_data) = expansion_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_undo(&self) {
|
||||||
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
|
if !config.undo_backspace {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_expansion_data = self.last_expansion_data.borrow();
|
||||||
|
if let Some(ref last_expansion_data) = *last_expansion_data {
|
||||||
|
let (trigger_string, injected_text_len) = last_expansion_data;
|
||||||
|
// Delete the previously injected text, minus one character as it has been consumed by the backspace
|
||||||
|
self.keyboard_manager
|
||||||
|
.delete_string(&config, *injected_text_len - 1);
|
||||||
|
// Restore previous text
|
||||||
|
self.inject_text(&config, trigger_string, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enable_update(&self, status: bool) {
|
fn on_enable_update(&self, status: bool) {
|
||||||
|
@ -331,6 +377,9 @@ impl<
|
||||||
if config.show_notifications {
|
if config.show_notifications {
|
||||||
self.ui_manager.notify(message);
|
self.ui_manager.notify(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the icon on supported OSes.
|
||||||
|
crate::context::update_icon(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_passive(&self) {
|
fn on_passive(&self) {
|
||||||
|
@ -346,16 +395,22 @@ impl<
|
||||||
// In order to avoid pasting previous clipboard contents, we need to check if
|
// In order to avoid pasting previous clipboard contents, we need to check if
|
||||||
// a new clipboard was effectively copied.
|
// a new clipboard was effectively copied.
|
||||||
// See issue: https://github.com/federico-terzi/espanso/issues/213
|
// See issue: https://github.com/federico-terzi/espanso/issues/213
|
||||||
let previous_clipboard = self.clipboard_manager.get_clipboard();
|
let previous_clipboard = self.clipboard_manager.get_clipboard().unwrap_or_default();
|
||||||
|
|
||||||
// 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(config.passive_delay));
|
||||||
|
|
||||||
|
// Clear the clipboard, for new-content detection later
|
||||||
|
self.clipboard_manager.set_clipboard("");
|
||||||
|
|
||||||
|
// Sleep for a while, giving time to effectively copy the text
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
||||||
|
|
||||||
// Trigger a copy shortcut to transfer the content of the selection to the clipboard
|
// 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(config.passive_delay));
|
||||||
|
|
||||||
// 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();
|
||||||
|
@ -365,30 +420,31 @@ impl<
|
||||||
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 {
|
info!("Passive mode activated");
|
||||||
// Because of issue #213, we need to make sure the user selected something.
|
|
||||||
if clipboard == previous_content {
|
|
||||||
info!("Avoiding passive expansion, as the user didn't select anything");
|
|
||||||
} else {
|
|
||||||
info!("Passive mode activated");
|
|
||||||
|
|
||||||
let rendered = self.renderer.render_passive(&clipboard, &config);
|
// Restore original clipboard in case it's used during render
|
||||||
|
self.clipboard_manager.set_clipboard(&previous_clipboard);
|
||||||
|
|
||||||
match rendered {
|
let rendered = self.renderer.render_passive(&clipboard, &config);
|
||||||
RenderResult::Text(payload) => {
|
|
||||||
// Paste back the result in the field
|
|
||||||
self.clipboard_manager.set_clipboard(&payload);
|
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
|
match rendered {
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
RenderResult::Text(payload) => {
|
||||||
}
|
// Paste back the result in the field
|
||||||
_ => warn!("Cannot expand passive match"),
|
self.clipboard_manager.set_clipboard(&payload);
|
||||||
}
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
||||||
|
self.keyboard_manager.trigger_paste(&config);
|
||||||
}
|
}
|
||||||
|
_ => warn!("Cannot expand passive match"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
||||||
|
|
||||||
|
// Restore original clipboard
|
||||||
|
self.clipboard_manager.set_clipboard(&previous_clipboard);
|
||||||
|
|
||||||
// Re-allow espanso to interpret actions
|
// Re-allow espanso to interpret actions
|
||||||
self.is_injecting.store(false, Release);
|
self.is_injecting.store(false, Release);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::clipboard::ClipboardManager;
|
use crate::clipboard::ClipboardManager;
|
||||||
|
use crate::extension::ExtensionResult;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct ClipboardExtension {
|
pub struct ClipboardExtension {
|
||||||
clipboard_manager: Box<dyn ClipboardManager>,
|
clipboard_manager: Box<dyn ClipboardManager>,
|
||||||
|
@ -35,7 +37,16 @@ impl super::Extension for ClipboardExtension {
|
||||||
String::from("clipboard")
|
String::from("clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate(&self, _: &Mapping, _: &Vec<String>) -> Option<String> {
|
fn calculate(
|
||||||
self.clipboard_manager.get_clipboard()
|
&self,
|
||||||
|
_: &Mapping,
|
||||||
|
_: &Vec<String>,
|
||||||
|
_: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
|
if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
|
||||||
|
Some(ExtensionResult::Single(clipboard))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* This file is part of espanso.
|
* This file is part of espanso.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019 Federico Terzi
|
* Copyright (C) 2019-2020 Federico Terzi
|
||||||
*
|
*
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,8 +17,10 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
use crate::extension::ExtensionResult;
|
||||||
|
use chrono::{DateTime, Duration, Local};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct DateExtension {}
|
pub struct DateExtension {}
|
||||||
|
|
||||||
|
@ -33,8 +35,21 @@ impl super::Extension for DateExtension {
|
||||||
String::from("date")
|
String::from("date")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {
|
fn calculate(
|
||||||
let now: DateTime<Local> = Local::now();
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
_: &Vec<String>,
|
||||||
|
_: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
|
let mut now: DateTime<Local> = Local::now();
|
||||||
|
|
||||||
|
// Compute the given offset
|
||||||
|
let offset = params.get(&Value::from("offset"));
|
||||||
|
if let Some(offset) = offset {
|
||||||
|
let seconds = offset.as_i64().unwrap_or_else(|| 0);
|
||||||
|
let offset = Duration::seconds(seconds);
|
||||||
|
now = now + offset;
|
||||||
|
}
|
||||||
|
|
||||||
let format = params.get(&Value::from("format"));
|
let format = params.get(&Value::from("format"));
|
||||||
|
|
||||||
|
@ -44,6 +59,6 @@ impl super::Extension for DateExtension {
|
||||||
now.to_rfc2822()
|
now.to_rfc2822()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(date)
|
Some(ExtensionResult::Single(date))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,26 +17,39 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::extension::ExtensionResult;
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct DummyExtension {}
|
pub struct DummyExtension {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl DummyExtension {
|
impl DummyExtension {
|
||||||
pub fn new() -> DummyExtension {
|
pub fn new(name: &str) -> DummyExtension {
|
||||||
DummyExtension {}
|
DummyExtension {
|
||||||
|
name: name.to_owned(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Extension for DummyExtension {
|
impl super::Extension for DummyExtension {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
String::from("dummy")
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
_: &Vec<String>,
|
||||||
|
_: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
let echo = params.get(&Value::from("echo"));
|
let echo = params.get(&Value::from("echo"));
|
||||||
|
|
||||||
if let Some(echo) = echo {
|
if let Some(echo) = echo {
|
||||||
Some(echo.as_str().unwrap_or_default().to_owned())
|
Some(ExtensionResult::Single(
|
||||||
|
echo.as_str().unwrap_or_default().to_owned(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
106
src/extension/form.rs
Normal file
106
src/extension/form.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* 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 crate::{config::Configs, extension::ExtensionResult, ui::modulo::ModuloManager};
|
||||||
|
use log::{error, warn};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct FormExtension {
|
||||||
|
manager: ModuloManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormExtension {
|
||||||
|
pub fn new(config: &Configs) -> FormExtension {
|
||||||
|
let manager = ModuloManager::new(config);
|
||||||
|
FormExtension { manager }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Extension for FormExtension {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"form".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
_: &Vec<String>,
|
||||||
|
_: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
|
let layout = params.get(&Value::from("layout"));
|
||||||
|
let layout = if let Some(value) = layout {
|
||||||
|
value.as_str().unwrap_or_default().to_string()
|
||||||
|
} else {
|
||||||
|
error!("invoking form extension without specifying a layout");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut form_config = Mapping::new();
|
||||||
|
form_config.insert(Value::from("title"), Value::from("espanso"));
|
||||||
|
form_config.insert(Value::from("layout"), Value::from(layout));
|
||||||
|
|
||||||
|
if let Some(fields) = params.get(&Value::from("fields")) {
|
||||||
|
form_config.insert(Value::from("fields"), fields.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(icon_path) = crate::context::get_icon_path() {
|
||||||
|
form_config.insert(Value::from("icon"), Value::from(icon_path.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialized_config: String =
|
||||||
|
serde_yaml::to_string(&form_config).expect("unable to serialize form config");
|
||||||
|
|
||||||
|
let output = self
|
||||||
|
.manager
|
||||||
|
.invoke(&["form", "-i", "-"], &serialized_config);
|
||||||
|
|
||||||
|
// On macOS, after the form closes we have to wait until the user releases the modifier keys
|
||||||
|
on_form_close();
|
||||||
|
|
||||||
|
if let Some(output) = output {
|
||||||
|
let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
|
||||||
|
match json {
|
||||||
|
Ok(json) => {
|
||||||
|
return Some(ExtensionResult::Multiple(json));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("modulo json parsing error: {}", error);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("modulo form didn't return any output");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
fn on_form_close() {
|
||||||
|
// NOOP on Windows and Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn on_form_close() {
|
||||||
|
let released = crate::keyboard::macos::wait_for_modifiers_release();
|
||||||
|
if !released {
|
||||||
|
warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)");
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,28 +17,50 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::clipboard::ClipboardManager;
|
use crate::{clipboard::ClipboardManager, config::Configs};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
mod date;
|
mod date;
|
||||||
pub mod dummy;
|
pub mod dummy;
|
||||||
|
mod form;
|
||||||
|
pub mod multiecho;
|
||||||
mod random;
|
mod random;
|
||||||
mod script;
|
mod script;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod utils;
|
||||||
|
pub mod vardummy;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ExtensionResult {
|
||||||
|
Single(String),
|
||||||
|
Multiple(HashMap<String, String>),
|
||||||
|
}
|
||||||
|
|
||||||
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>,
|
||||||
|
current_vars: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<dyn Extension>> {
|
pub fn get_extensions(
|
||||||
|
config: &Configs,
|
||||||
|
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()),
|
||||||
Box::new(script::ScriptExtension::new()),
|
Box::new(script::ScriptExtension::new()),
|
||||||
Box::new(random::RandomExtension::new()),
|
Box::new(random::RandomExtension::new()),
|
||||||
Box::new(dummy::DummyExtension::new()),
|
Box::new(multiecho::MultiEchoExtension::new()),
|
||||||
|
Box::new(dummy::DummyExtension::new("dummy")),
|
||||||
|
Box::new(dummy::DummyExtension::new("echo")),
|
||||||
Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
|
Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
|
||||||
|
Box::new(form::FormExtension::new(config)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
53
src/extension/multiecho.rs
Normal file
53
src/extension/multiecho.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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 crate::extension::ExtensionResult;
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct MultiEchoExtension {}
|
||||||
|
|
||||||
|
impl MultiEchoExtension {
|
||||||
|
pub fn new() -> MultiEchoExtension {
|
||||||
|
MultiEchoExtension {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Extension for MultiEchoExtension {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"multiecho".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
_: &Vec<String>,
|
||||||
|
_: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
|
let mut output: HashMap<String, String> = HashMap::new();
|
||||||
|
for (key, value) in params.iter() {
|
||||||
|
if let Some(key) = key.as_str() {
|
||||||
|
if let Some(value) = value.as_str() {
|
||||||
|
output.insert(key.to_owned(), value.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ExtensionResult::Multiple(output))
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,9 +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 crate::extension::ExtensionResult;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct RandomExtension {}
|
pub struct RandomExtension {}
|
||||||
|
|
||||||
|
@ -34,7 +36,12 @@ impl super::Extension for RandomExtension {
|
||||||
String::from("random")
|
String::from("random")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> {
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
args: &Vec<String>,
|
||||||
|
_: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
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");
|
||||||
|
@ -55,7 +62,7 @@ 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(ExtensionResult::Single(output));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
error!("Could not select a random choice.");
|
error!("Could not select a random choice.");
|
||||||
|
@ -81,13 +88,15 @@ mod tests {
|
||||||
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();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
let output = output.unwrap();
|
let output = output.unwrap();
|
||||||
|
|
||||||
assert!(choices.iter().any(|x| x == &output));
|
assert!(choices
|
||||||
|
.into_iter()
|
||||||
|
.any(|x| ExtensionResult::Single(x.to_owned()) == output));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -97,7 +106,7 @@ mod tests {
|
||||||
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();
|
||||||
let output = extension.calculate(¶ms, &vec!["test".to_owned()]);
|
let output = extension.calculate(¶ms, &vec!["test".to_owned()], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
|
@ -105,6 +114,8 @@ mod tests {
|
||||||
|
|
||||||
let rendered_choices = vec!["first test", "second test", "test third"];
|
let rendered_choices = vec!["first test", "second test", "test third"];
|
||||||
|
|
||||||
assert!(rendered_choices.iter().any(|x| x == &output));
|
assert!(rendered_choices
|
||||||
|
.into_iter()
|
||||||
|
.any(|x| ExtensionResult::Single(x.to_owned()) == output));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,10 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::extension::ExtensionResult;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
@ -35,7 +37,12 @@ impl super::Extension for ScriptExtension {
|
||||||
String::from("script")
|
String::from("script")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate(&self, params: &Mapping, user_args: &Vec<String>) -> Option<String> {
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
user_args: &Vec<String>,
|
||||||
|
vars: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
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");
|
||||||
|
@ -60,11 +67,28 @@ impl super::Extension for ScriptExtension {
|
||||||
|
|
||||||
// Replace %HOME% with current user home directory to
|
// Replace %HOME% with current user home directory to
|
||||||
// create cross-platform paths. See issue #265
|
// create cross-platform paths. See issue #265
|
||||||
|
// Also replace %CONFIG% and %PACKAGES% path. See issue #380
|
||||||
let home_dir = dirs::home_dir().unwrap_or_default();
|
let home_dir = dirs::home_dir().unwrap_or_default();
|
||||||
str_args.iter_mut().for_each(|arg| {
|
str_args.iter_mut().for_each(|arg| {
|
||||||
if arg.contains("%HOME%") {
|
if arg.contains("%HOME%") {
|
||||||
*arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string());
|
*arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
|
if arg.contains("%CONFIG%") {
|
||||||
|
*arg = arg.replace(
|
||||||
|
"%CONFIG%",
|
||||||
|
&crate::context::get_config_dir()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if arg.contains("%PACKAGES%") {
|
||||||
|
*arg = arg.replace(
|
||||||
|
"%PACKAGES%",
|
||||||
|
&crate::context::get_package_dir()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// On Windows, correct paths separators
|
// On Windows, correct paths separators
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
|
@ -77,9 +101,18 @@ impl super::Extension for ScriptExtension {
|
||||||
|
|
||||||
let mut command = Command::new(&str_args[0]);
|
let mut command = Command::new(&str_args[0]);
|
||||||
|
|
||||||
|
// Set the OS-specific flags
|
||||||
|
crate::utils::set_command_flags(&mut command);
|
||||||
|
|
||||||
// Inject the $CONFIG variable
|
// Inject the $CONFIG variable
|
||||||
command.env("CONFIG", crate::context::get_config_dir());
|
command.env("CONFIG", crate::context::get_config_dir());
|
||||||
|
|
||||||
|
// Inject all the env variables
|
||||||
|
let env_variables = super::utils::convert_to_env_variables(&vars);
|
||||||
|
for (key, value) in env_variables.iter() {
|
||||||
|
command.env(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
let output = if str_args.len() > 1 {
|
let output = if str_args.len() > 1 {
|
||||||
command.args(&str_args[1..]).output()
|
command.args(&str_args[1..]).output()
|
||||||
} else {
|
} else {
|
||||||
|
@ -112,7 +145,7 @@ impl super::Extension for ScriptExtension {
|
||||||
output_str = output_str.trim().to_owned()
|
output_str = output_str.trim().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(output_str);
|
return Some(ExtensionResult::Single(output_str));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not execute script '{:?}', error: {}", args, e);
|
error!("Could not execute script '{:?}', error: {}", args, e);
|
||||||
|
@ -141,10 +174,13 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -158,10 +194,13 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from(false));
|
params.insert(Value::from("trim"), Value::from(false));
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world\n");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world\n".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -174,10 +213,13 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["jon".to_owned()]);
|
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -191,9 +233,40 @@ mod tests {
|
||||||
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();
|
||||||
let output = extension.calculate(¶ms, &vec!["jon".to_owned()]);
|
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world jon");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world jon".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn test_script_var_injection() {
|
||||||
|
let mut params = Mapping::new();
|
||||||
|
params.insert(
|
||||||
|
Value::from("args"),
|
||||||
|
Value::from(vec!["bash", "-c", "echo $ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
||||||
|
let mut subvars = HashMap::new();
|
||||||
|
subvars.insert("name".to_owned(), "John".to_owned());
|
||||||
|
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
||||||
|
vars.insert(
|
||||||
|
"var1".to_owned(),
|
||||||
|
ExtensionResult::Single("hello".to_owned()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let extension = ScriptExtension::new();
|
||||||
|
let output = extension.calculate(¶ms, &vec![], &vars);
|
||||||
|
|
||||||
|
assert!(output.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello John".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +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 log::{error, warn};
|
use crate::extension::ExtensionResult;
|
||||||
|
use log::{error, info, warn};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::process::{Command, Output};
|
use std::process::{Command, Output};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -40,7 +42,9 @@ pub enum Shell {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shell {
|
impl Shell {
|
||||||
fn execute_cmd(&self, cmd: &str) -> std::io::Result<Output> {
|
fn execute_cmd(&self, cmd: &str, vars: &HashMap<String, String>) -> std::io::Result<Output> {
|
||||||
|
let mut is_wsl = false;
|
||||||
|
|
||||||
let mut command = match self {
|
let mut command = match self {
|
||||||
Shell::Cmd => {
|
Shell::Cmd => {
|
||||||
let mut command = Command::new("cmd");
|
let mut command = Command::new("cmd");
|
||||||
|
@ -53,11 +57,13 @@ impl Shell {
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
Shell::WSL => {
|
Shell::WSL => {
|
||||||
|
is_wsl = true;
|
||||||
let mut command = Command::new("bash");
|
let mut command = Command::new("bash");
|
||||||
command.args(&["-c", &cmd]);
|
command.args(&["-c", &cmd]);
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
Shell::WSL2 => {
|
Shell::WSL2 => {
|
||||||
|
is_wsl = true;
|
||||||
let mut command = Command::new("wsl");
|
let mut command = Command::new("wsl");
|
||||||
command.args(&["bash", "-c", &cmd]);
|
command.args(&["bash", "-c", &cmd]);
|
||||||
command
|
command
|
||||||
|
@ -74,9 +80,33 @@ impl Shell {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set the OS-specific flags
|
||||||
|
crate::utils::set_command_flags(&mut command);
|
||||||
|
|
||||||
// Inject the $CONFIG variable
|
// Inject the $CONFIG variable
|
||||||
command.env("CONFIG", crate::context::get_config_dir());
|
command.env("CONFIG", crate::context::get_config_dir());
|
||||||
|
|
||||||
|
// Inject all the previous variables
|
||||||
|
for (key, value) in vars.iter() {
|
||||||
|
command.env(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In WSL environment, we have to specify which ENV variables
|
||||||
|
// should be passed to linux.
|
||||||
|
// For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
|
||||||
|
if is_wsl {
|
||||||
|
let mut tokens: Vec<&str> = Vec::new();
|
||||||
|
tokens.push("CONFIG/p");
|
||||||
|
|
||||||
|
// Add all the previous variables
|
||||||
|
for (key, _) in vars.iter() {
|
||||||
|
tokens.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let wsl_env = tokens.join(":");
|
||||||
|
command.env("WSLENV", wsl_env);
|
||||||
|
}
|
||||||
|
|
||||||
command.output()
|
command.output()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,17 +150,23 @@ impl super::Extension for ShellExtension {
|
||||||
String::from("shell")
|
String::from("shell")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> {
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
args: &Vec<String>,
|
||||||
|
vars: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
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 original_cmd = cmd.unwrap().as_str().unwrap();
|
||||||
|
|
||||||
// Render positional parameters in args
|
// Render positional parameters in args
|
||||||
let cmd = POS_ARG_REGEX
|
let cmd = POS_ARG_REGEX
|
||||||
.replace_all(&cmd, |caps: &Captures| {
|
.replace_all(&original_cmd, |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 {
|
||||||
|
@ -156,7 +192,9 @@ impl super::Extension for ShellExtension {
|
||||||
Shell::default()
|
Shell::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = shell.execute_cmd(&cmd);
|
let env_variables = super::utils::convert_to_env_variables(&vars);
|
||||||
|
|
||||||
|
let output = shell.execute_cmd(&cmd, &env_variables);
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
|
@ -171,6 +209,22 @@ impl super::Extension for ShellExtension {
|
||||||
warn!("Shell command reported error: \n{}", error_str);
|
warn!("Shell command reported error: \n{}", error_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if debug flag set, provide additional context when an error occurs.
|
||||||
|
let debug_opt = params.get(&Value::from("debug"));
|
||||||
|
let with_debug = if let Some(value) = debug_opt {
|
||||||
|
let val = value.as_bool();
|
||||||
|
val.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if with_debug {
|
||||||
|
info!(
|
||||||
|
"debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'",
|
||||||
|
original_cmd, output.status, output_str, error_str
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// If specified, trim the output
|
// If specified, trim the output
|
||||||
let trim_opt = params.get(&Value::from("trim"));
|
let trim_opt = params.get(&Value::from("trim"));
|
||||||
let should_trim = if let Some(value) = trim_opt {
|
let should_trim = if let Some(value) = trim_opt {
|
||||||
|
@ -184,7 +238,7 @@ impl super::Extension for ShellExtension {
|
||||||
output_str = output_str.trim().to_owned()
|
output_str = output_str.trim().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(output_str)
|
Some(ExtensionResult::Single(output_str))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not execute cmd '{}', error: {}", cmd, e);
|
error!("Could not execute cmd '{}', error: {}", cmd, e);
|
||||||
|
@ -206,14 +260,20 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from(false));
|
params.insert(Value::from("trim"), Value::from(false));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
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(),
|
||||||
|
ExtensionResult::Single("hello world\r\n".to_owned())
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(output.unwrap(), "hello world\n");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world\n".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,10 +283,13 @@ mod tests {
|
||||||
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(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -238,10 +301,13 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -251,10 +317,13 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from("error"));
|
params.insert(Value::from("trim"), Value::from("error"));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -265,10 +334,13 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from(true));
|
params.insert(Value::from("trim"), Value::from(true));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), "hello world");
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("hello world".to_owned())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -278,11 +350,11 @@ mod tests {
|
||||||
params.insert(Value::from("cmd"), Value::from("echo $0"));
|
params.insert(Value::from("cmd"), Value::from("echo $0"));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["hello".to_owned()]);
|
let output = extension.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), "hello");
|
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -292,10 +364,53 @@ mod tests {
|
||||||
params.insert(Value::from("cmd"), Value::from("echo %0"));
|
params.insert(Value::from("cmd"), Value::from("echo %0"));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["hello".to_owned()]);
|
let output = extension.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new());
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), "hello");
|
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shell_vars_single_injection() {
|
||||||
|
let mut params = Mapping::new();
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_VAR1%"));
|
||||||
|
params.insert(Value::from("shell"), Value::from("cmd"));
|
||||||
|
} else {
|
||||||
|
params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_VAR1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension = ShellExtension::new();
|
||||||
|
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
||||||
|
vars.insert(
|
||||||
|
"var1".to_owned(),
|
||||||
|
ExtensionResult::Single("hello".to_owned()),
|
||||||
|
);
|
||||||
|
let output = extension.calculate(¶ms, &vec![], &vars);
|
||||||
|
|
||||||
|
assert!(output.is_some());
|
||||||
|
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shell_vars_multiple_injection() {
|
||||||
|
let mut params = Mapping::new();
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_FORM1_NAME%"));
|
||||||
|
params.insert(Value::from("shell"), Value::from("cmd"));
|
||||||
|
} else {
|
||||||
|
params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_FORM1_NAME"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension = ShellExtension::new();
|
||||||
|
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
||||||
|
let mut subvars = HashMap::new();
|
||||||
|
subvars.insert("name".to_owned(), "John".to_owned());
|
||||||
|
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
||||||
|
let output = extension.calculate(¶ms, &vec![], &vars);
|
||||||
|
|
||||||
|
assert!(output.is_some());
|
||||||
|
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
src/extension/utils.rs
Normal file
49
src/extension/utils.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use crate::extension::ExtensionResult;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub fn convert_to_env_variables(
|
||||||
|
original_vars: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> HashMap<String, String> {
|
||||||
|
let mut output = HashMap::new();
|
||||||
|
|
||||||
|
for (key, result) in original_vars.iter() {
|
||||||
|
match result {
|
||||||
|
ExtensionResult::Single(value) => {
|
||||||
|
let name = format!("ESPANSO_{}", key.to_uppercase());
|
||||||
|
output.insert(name, value.clone());
|
||||||
|
}
|
||||||
|
ExtensionResult::Multiple(values) => {
|
||||||
|
for (sub_key, sub_value) in values.iter() {
|
||||||
|
let name = format!("ESPANSO_{}_{}", key.to_uppercase(), sub_key.to_uppercase());
|
||||||
|
output.insert(name, sub_value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::extension::Extension;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convert_to_env_variables() {
|
||||||
|
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
||||||
|
let mut subvars = HashMap::new();
|
||||||
|
subvars.insert("name".to_owned(), "John".to_owned());
|
||||||
|
subvars.insert("lastname".to_owned(), "Snow".to_owned());
|
||||||
|
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
||||||
|
vars.insert(
|
||||||
|
"var1".to_owned(),
|
||||||
|
ExtensionResult::Single("test".to_owned()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = convert_to_env_variables(&vars);
|
||||||
|
assert_eq!(output.get("ESPANSO_FORM1_NAME").unwrap(), "John");
|
||||||
|
assert_eq!(output.get("ESPANSO_FORM1_LASTNAME").unwrap(), "Snow");
|
||||||
|
assert_eq!(output.get("ESPANSO_VAR1").unwrap(), "test");
|
||||||
|
}
|
||||||
|
}
|
52
src/extension/vardummy.rs
Normal file
52
src/extension/vardummy.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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 crate::extension::ExtensionResult;
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct VarDummyExtension {}
|
||||||
|
|
||||||
|
impl VarDummyExtension {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Extension for VarDummyExtension {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"vardummy".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
params: &Mapping,
|
||||||
|
_: &Vec<String>,
|
||||||
|
vars: &HashMap<String, ExtensionResult>,
|
||||||
|
) -> Option<ExtensionResult> {
|
||||||
|
let target = params.get(&Value::from("target"));
|
||||||
|
|
||||||
|
if let Some(target) = target {
|
||||||
|
let value = vars.get(target.as_str().unwrap_or_default());
|
||||||
|
Some(value.unwrap().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,3 +75,15 @@ impl super::KeyboardManager for MacKeyboardManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_modifiers_release() -> bool {
|
||||||
|
let start = std::time::SystemTime::now();
|
||||||
|
while start.elapsed().unwrap_or_default().as_millis() < 3000 {
|
||||||
|
let pressed = unsafe { crate::bridge::macos::are_modifiers_pressed() };
|
||||||
|
if pressed == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ mod windows;
|
||||||
mod linux;
|
mod linux;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod macos;
|
pub mod macos;
|
||||||
|
|
||||||
pub trait KeyboardManager {
|
pub trait KeyboardManager {
|
||||||
fn send_string(&self, active_config: &Configs, s: &str);
|
fn send_string(&self, active_config: &Configs, s: &str);
|
||||||
|
|
31
src/main.rs
31
src/main.rs
|
@ -17,6 +17,8 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#![cfg_attr(not(test), windows_subsystem = "windows")]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
@ -76,6 +78,8 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
const LOG_FILE: &str = "espanso.log";
|
const LOG_FILE: &str = "espanso.log";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
attach_console();
|
||||||
|
|
||||||
let install_subcommand = SubCommand::with_name("install")
|
let install_subcommand = SubCommand::with_name("install")
|
||||||
.about("Install a package. Equivalent to 'espanso package install'")
|
.about("Install a package. Equivalent to 'espanso package install'")
|
||||||
.arg(
|
.arg(
|
||||||
|
@ -344,6 +348,18 @@ fn main() {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn attach_console() {
|
||||||
|
// When using the windows subsystem we loose the terminal output.
|
||||||
|
// Therefore we try to attach to the current console if available.
|
||||||
|
unsafe { winapi::um::wincon::AttachConsole(0xFFFFFFFF) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn attach_console() {
|
||||||
|
// Not necessary on Linux and macOS
|
||||||
|
}
|
||||||
|
|
||||||
fn init_logger(config_set: &ConfigSet, reset: bool) {
|
fn init_logger(config_set: &ConfigSet, reset: bool) {
|
||||||
// Initialize log
|
// Initialize log
|
||||||
let log_level = match config_set.default.log_level {
|
let log_level = match config_set.default.log_level {
|
||||||
|
@ -566,8 +582,14 @@ fn watcher_background(sender: Sender<Event>) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
if path.extension().unwrap_or_default() == "yml" {
|
if path.extension().unwrap_or_default() == "yml"
|
||||||
// Only load yml files
|
&& !path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.starts_with(".")
|
||||||
|
{
|
||||||
|
// Only load non-hidden yml files
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -671,7 +693,10 @@ fn worker_background(
|
||||||
|
|
||||||
let keyboard_manager = keyboard::get_manager();
|
let keyboard_manager = keyboard::get_manager();
|
||||||
|
|
||||||
let extensions = extension::get_extensions(Box::new(clipboard::get_manager()));
|
let extensions = extension::get_extensions(
|
||||||
|
config_manager.default_config(),
|
||||||
|
Box::new(clipboard::get_manager()),
|
||||||
|
);
|
||||||
|
|
||||||
let renderer =
|
let renderer =
|
||||||
render::default::DefaultRenderer::new(extensions, config_manager.default_config().clone());
|
render::default::DefaultRenderer::new(extensions, config_manager.default_config().clone());
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* This file is part of espanso.
|
* This file is part of espans{ name: (), var_type: (), params: ()}
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019 Federico Terzi
|
* Copyright (C) 2019 Federico Terzi
|
||||||
*
|
*
|
||||||
|
@ -19,9 +19,10 @@
|
||||||
|
|
||||||
use crate::event::KeyEventReceiver;
|
use crate::event::KeyEventReceiver;
|
||||||
use crate::event::{KeyEvent, KeyModifier};
|
use crate::event::{KeyEvent, KeyModifier};
|
||||||
use regex::Regex;
|
use regex::{Captures, Regex};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ pub enum MatchContentType {
|
||||||
Image(ImageContent),
|
Image(ImageContent),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone, PartialEq)]
|
||||||
pub struct TextContent {
|
pub struct TextContent {
|
||||||
pub replace: String,
|
pub replace: String,
|
||||||
pub vars: Vec<MatchVariable>,
|
pub vars: Vec<MatchVariable>,
|
||||||
|
@ -74,7 +75,8 @@ impl<'de> serde::Deserialize<'de> for 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+)(\\.\\w+)?\\s*\\}\\}").unwrap();
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut triggers = if !other.triggers.is_empty() {
|
let mut triggers = if !other.triggers.is_empty() {
|
||||||
|
@ -143,6 +145,39 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
_has_vars: has_vars,
|
_has_vars: has_vars,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MatchContentType::Text(content)
|
||||||
|
} else if let Some(form) = &other.form {
|
||||||
|
// Form shorthand
|
||||||
|
// Replace all the form fields with actual variables
|
||||||
|
let new_replace = VAR_REGEX.replace_all(&form, |caps: &Captures| {
|
||||||
|
let var_name = caps.get(1).unwrap().as_str();
|
||||||
|
format!("{{{{form1.{}}}}}", var_name)
|
||||||
|
});
|
||||||
|
let new_replace = new_replace.to_string();
|
||||||
|
|
||||||
|
// Convert the form data to valid variables
|
||||||
|
let mut params = Mapping::new();
|
||||||
|
if let Some(fields) = &other.form_fields {
|
||||||
|
let mut mapping_fields = Mapping::new();
|
||||||
|
fields.iter().for_each(|(key, value)| {
|
||||||
|
mapping_fields.insert(Value::from(key.to_owned()), Value::from(value.clone()));
|
||||||
|
});
|
||||||
|
params.insert(Value::from("fields"), Value::from(mapping_fields));
|
||||||
|
}
|
||||||
|
params.insert(Value::from("layout"), Value::from(form.to_owned()));
|
||||||
|
|
||||||
|
let vars = vec![MatchVariable {
|
||||||
|
name: "form1".to_owned(),
|
||||||
|
var_type: "form".to_owned(),
|
||||||
|
params,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let content = TextContent {
|
||||||
|
replace: new_replace,
|
||||||
|
vars,
|
||||||
|
_has_vars: true,
|
||||||
|
};
|
||||||
|
|
||||||
MatchContentType::Text(content)
|
MatchContentType::Text(content)
|
||||||
} else if let Some(image_path) = &other.image_path {
|
} else if let Some(image_path) = &other.image_path {
|
||||||
// Image match
|
// Image match
|
||||||
|
@ -173,7 +208,7 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
|
|
||||||
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', 'image_path' or 'form'", other.trigger);
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -204,6 +239,12 @@ struct AutoMatch {
|
||||||
#[serde(default = "default_image_path")]
|
#[serde(default = "default_image_path")]
|
||||||
pub image_path: Option<String>,
|
pub image_path: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_form")]
|
||||||
|
pub form: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_form_fields")]
|
||||||
|
pub form_fields: Option<HashMap<String, Value>>,
|
||||||
|
|
||||||
#[serde(default = "default_vars")]
|
#[serde(default = "default_vars")]
|
||||||
pub vars: Vec<MatchVariable>,
|
pub vars: Vec<MatchVariable>,
|
||||||
|
|
||||||
|
@ -238,6 +279,12 @@ fn default_passive_only() -> bool {
|
||||||
fn default_replace() -> Option<String> {
|
fn default_replace() -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
fn default_form() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn default_form_fields() -> Option<HashMap<String, Value>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
fn default_image_path() -> Option<String> {
|
fn default_image_path() -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -248,7 +295,7 @@ fn default_force_clipboard() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct MatchVariable {
|
pub struct MatchVariable {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
@ -273,6 +320,7 @@ pub trait MatchReceiver {
|
||||||
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);
|
||||||
fn on_enable_update(&self, status: bool);
|
fn on_enable_update(&self, status: bool);
|
||||||
fn on_passive(&self);
|
fn on_passive(&self);
|
||||||
|
fn on_undo(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Matcher: KeyEventReceiver {
|
pub trait Matcher: KeyEventReceiver {
|
||||||
|
@ -543,4 +591,76 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec![":..", ":..", ":.."])
|
assert_eq!(_match.triggers, vec![":..", ":..", ":.."])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_form_translated_correctly() {
|
||||||
|
let match_str = r###"
|
||||||
|
trigger: ":test"
|
||||||
|
form: "Hey {{name}}, how are you? {{greet}}"
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
match _match.content {
|
||||||
|
MatchContentType::Text(content) => {
|
||||||
|
let mut mapping = Mapping::new();
|
||||||
|
mapping.insert(
|
||||||
|
Value::from("layout"),
|
||||||
|
Value::from("Hey {{name}}, how are you? {{greet}}"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
content,
|
||||||
|
TextContent {
|
||||||
|
replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(),
|
||||||
|
_has_vars: true,
|
||||||
|
vars: vec![MatchVariable {
|
||||||
|
name: "form1".to_owned(),
|
||||||
|
var_type: "form".to_owned(),
|
||||||
|
params: mapping,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("wrong content"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_form_with_fields_translated_correctly() {
|
||||||
|
let match_str = r###"
|
||||||
|
trigger: ":test"
|
||||||
|
form: "Hey {{name}}, how are you? {{greet}}"
|
||||||
|
form_fields:
|
||||||
|
name:
|
||||||
|
multiline: true
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
match _match.content {
|
||||||
|
MatchContentType::Text(content) => {
|
||||||
|
let mut name_mapping = Mapping::new();
|
||||||
|
name_mapping.insert(Value::from("multiline"), Value::Bool(true));
|
||||||
|
let mut submapping = Mapping::new();
|
||||||
|
submapping.insert(Value::from("name"), Value::from(name_mapping));
|
||||||
|
let mut mapping = Mapping::new();
|
||||||
|
mapping.insert(Value::from("fields"), Value::from(submapping));
|
||||||
|
mapping.insert(
|
||||||
|
Value::from("layout"),
|
||||||
|
Value::from("Hey {{name}}, how are you? {{greet}}"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
content,
|
||||||
|
TextContent {
|
||||||
|
replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(),
|
||||||
|
_has_vars: true,
|
||||||
|
vars: vec![MatchVariable {
|
||||||
|
name: "form1".to_owned(),
|
||||||
|
var_type: "form".to_owned(),
|
||||||
|
params: mapping,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("wrong content"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
||||||
passive_press_time: RefCell<SystemTime>,
|
passive_press_time: RefCell<SystemTime>,
|
||||||
is_enabled: RefCell<bool>,
|
is_enabled: RefCell<bool>,
|
||||||
was_previous_char_word_separator: RefCell<bool>,
|
was_previous_char_word_separator: RefCell<bool>,
|
||||||
|
was_previous_char_a_match: RefCell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -57,6 +58,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
||||||
passive_press_time,
|
passive_press_time,
|
||||||
is_enabled: RefCell::new(true),
|
is_enabled: RefCell::new(true),
|
||||||
was_previous_char_word_separator: RefCell::new(true),
|
was_previous_char_word_separator: RefCell::new(true),
|
||||||
|
was_previous_char_a_match: RefCell::new(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,12 +106,8 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
.word_separators
|
.word_separators
|
||||||
.contains(&c.chars().nth(0).unwrap_or_default());
|
.contains(&c.chars().nth(0).unwrap_or_default());
|
||||||
|
|
||||||
// Workaround needed on macos to consider espanso replacement key presses as separators.
|
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
||||||
if cfg!(target_os = "macos") {
|
(*was_previous_char_a_match) = false;
|
||||||
if c.len() > 1 {
|
|
||||||
is_current_word_separator = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
|
let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
|
||||||
|
|
||||||
|
@ -192,9 +190,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
if let Some(entry) = found_entry {
|
if let Some(entry) = found_entry {
|
||||||
let mtc = entry._match;
|
let mtc = entry._match;
|
||||||
|
|
||||||
if let Some(last) = current_set_queue.back_mut() {
|
current_set_queue.clear();
|
||||||
last.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
let trailing_separator = if !mtc.word {
|
let trailing_separator = if !mtc.word {
|
||||||
// If it's not a word match, it cannot have a trailing separator
|
// If it's not a word match, it cannot have a trailing separator
|
||||||
|
@ -216,12 +212,16 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
|
|
||||||
self.receiver
|
self.receiver
|
||||||
.on_match(mtc, trailing_separator, entry.trigger_offset);
|
.on_match(mtc, trailing_separator, entry.trigger_offset);
|
||||||
|
|
||||||
|
(*was_previous_char_a_match) = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_modifier(&self, m: KeyModifier) {
|
fn handle_modifier(&self, m: KeyModifier) {
|
||||||
let config = self.config_manager.default_config();
|
let config = self.config_manager.default_config();
|
||||||
|
|
||||||
|
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
||||||
|
|
||||||
// TODO: at the moment, activating the passive key triggers the toggle key
|
// 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
|
||||||
|
|
||||||
|
@ -253,8 +253,16 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
if m == BACKSPACE {
|
if m == BACKSPACE {
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
||||||
current_set_queue.pop_back();
|
current_set_queue.pop_back();
|
||||||
|
|
||||||
|
if (*was_previous_char_a_match) {
|
||||||
|
current_set_queue.clear();
|
||||||
|
self.receiver.on_undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable the "backspace undo" feature
|
||||||
|
(*was_previous_char_a_match) = false;
|
||||||
|
|
||||||
// Consider modifiers as separators to improve word matches reliability
|
// Consider modifiers as separators to improve word matches reliability
|
||||||
if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
|
if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
|
||||||
let mut was_previous_char_word_separator =
|
let mut was_previous_char_word_separator =
|
||||||
|
@ -269,6 +277,10 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
let mut was_previous_char_word_separator =
|
let mut was_previous_char_word_separator =
|
||||||
self.was_previous_char_word_separator.borrow_mut();
|
self.was_previous_char_word_separator.borrow_mut();
|
||||||
*was_previous_char_word_separator = true;
|
*was_previous_char_word_separator = true;
|
||||||
|
|
||||||
|
// Disable the "backspace undo" feature
|
||||||
|
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
||||||
|
(*was_previous_char_a_match) = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ use std::process::{Child, Command, Stdio};
|
||||||
pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
|
pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
Command::new(cmd)
|
Command::new(cmd)
|
||||||
.creation_flags(0x00000008) // Detached Process
|
.creation_flags(0x08000008) // Detached Process without window
|
||||||
.args(args)
|
.args(args)
|
||||||
.spawn()
|
.spawn()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,16 @@
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::Configs;
|
use crate::config::Configs;
|
||||||
use crate::extension::Extension;
|
use crate::extension::{Extension, ExtensionResult};
|
||||||
use crate::matcher::{Match, MatchContentType};
|
use crate::matcher::{Match, MatchContentType, MatchVariable};
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use serde_yaml::Value;
|
use serde_yaml::Value;
|
||||||
use std::collections::{HashMap, HashSet};
|
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(r"\{\{\s*((?P<name>\w+)(\.(?P<subname>(\w+)))?)\s*\}\}").unwrap();
|
||||||
static ref UNKNOWN_VARIABLE: String = "".to_string();
|
static ref UNKNOWN_VARIABLE: String = "".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,24 +87,55 @@ impl super::Renderer for DefaultRenderer {
|
||||||
match &m.content {
|
match &m.content {
|
||||||
// Text Match
|
// Text Match
|
||||||
MatchContentType::Text(content) => {
|
MatchContentType::Text(content) => {
|
||||||
// Find all the variables that are required by the current match
|
let target_string = if content._has_vars {
|
||||||
let mut target_vars = HashSet::new();
|
// Find all the variables that are required by the current match
|
||||||
|
let mut target_vars: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
for caps in VAR_REGEX.captures_iter(&content.replace) {
|
for caps in VAR_REGEX.captures_iter(&content.replace) {
|
||||||
let var_name = caps.name("name").unwrap().as_str();
|
let var_name = caps.name("name").unwrap().as_str();
|
||||||
target_vars.insert(var_name.to_owned());
|
target_vars.insert(var_name.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_string = if target_vars.len() > 0 {
|
let match_variables: HashSet<&String> =
|
||||||
let mut output_map = HashMap::new();
|
content.vars.iter().map(|var| &var.name).collect();
|
||||||
|
|
||||||
// Cycle through both the local and global variables
|
// Find the global variables that are not specified in the var list
|
||||||
for variable in config.global_vars.iter().chain(&content.vars) {
|
let mut missing_globals = Vec::new();
|
||||||
// Skip all non-required variables
|
let mut specified_globals: HashMap<String, &MatchVariable> = HashMap::new();
|
||||||
if !target_vars.contains(&variable.name) {
|
for global_var in config.global_vars.iter() {
|
||||||
continue;
|
if target_vars.contains(&global_var.name) {
|
||||||
|
if match_variables.contains(&global_var.name) {
|
||||||
|
specified_globals.insert(global_var.name.clone(), &global_var);
|
||||||
|
} else {
|
||||||
|
missing_globals.push(global_var);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the variable evaluation order
|
||||||
|
let mut variables: Vec<&MatchVariable> = Vec::new();
|
||||||
|
// First place the global that are not explicitly specified
|
||||||
|
variables.extend(missing_globals);
|
||||||
|
// Then the ones explicitly specified, in the given order
|
||||||
|
variables.extend(&content.vars);
|
||||||
|
|
||||||
|
// Replace variable type "global" with the actual reference
|
||||||
|
let variables: Vec<&MatchVariable> = variables
|
||||||
|
.into_iter()
|
||||||
|
.map(|variable| {
|
||||||
|
if variable.var_type == "global" {
|
||||||
|
if let Some(actual_variable) = specified_globals.get(&variable.name)
|
||||||
|
{
|
||||||
|
return actual_variable.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variable
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut output_map: HashMap<String, ExtensionResult> = HashMap::new();
|
||||||
|
|
||||||
|
for variable in variables.into_iter() {
|
||||||
// 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" {
|
||||||
|
@ -140,7 +172,7 @@ impl super::Renderer for DefaultRenderer {
|
||||||
// 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 {
|
||||||
RenderResult::Text(inner_content) => {
|
RenderResult::Text(inner_content) => {
|
||||||
output_map.insert(variable.name.clone(), inner_content);
|
output_map.insert(variable.name.clone(), ExtensionResult::Single(inner_content));
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.")
|
warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.")
|
||||||
|
@ -150,11 +182,15 @@ impl super::Renderer for DefaultRenderer {
|
||||||
// Normal extension variables
|
// 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, &output_map);
|
||||||
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(),
|
||||||
|
ExtensionResult::Single("".to_owned()),
|
||||||
|
);
|
||||||
warn!(
|
warn!(
|
||||||
"Could not generate output for variable: {}",
|
"Could not generate output for variable: {}",
|
||||||
variable.name
|
variable.name
|
||||||
|
@ -172,8 +208,26 @@ impl super::Renderer for DefaultRenderer {
|
||||||
// Replace the variables
|
// Replace the variables
|
||||||
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
||||||
let var_name = caps.name("name").unwrap().as_str();
|
let var_name = caps.name("name").unwrap().as_str();
|
||||||
let output = output_map.get(var_name);
|
let var_subname = caps.name("subname");
|
||||||
output.unwrap_or(&UNKNOWN_VARIABLE)
|
match output_map.get(var_name) {
|
||||||
|
Some(result) => match result {
|
||||||
|
ExtensionResult::Single(output) => output,
|
||||||
|
ExtensionResult::Multiple(results) => match var_subname {
|
||||||
|
Some(var_subname) => {
|
||||||
|
let var_subname = var_subname.as_str();
|
||||||
|
results.get(var_subname).unwrap_or(&UNKNOWN_VARIABLE)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(
|
||||||
|
"nested name missing from multi-value variable: {}",
|
||||||
|
var_name
|
||||||
|
);
|
||||||
|
&UNKNOWN_VARIABLE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => &UNKNOWN_VARIABLE,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
result.to_string()
|
result.to_string()
|
||||||
|
@ -311,7 +365,11 @@ mod tests {
|
||||||
|
|
||||||
fn get_renderer(config: Configs) -> DefaultRenderer {
|
fn get_renderer(config: Configs) -> DefaultRenderer {
|
||||||
DefaultRenderer::new(
|
DefaultRenderer::new(
|
||||||
vec![Box::new(crate::extension::dummy::DummyExtension::new())],
|
vec![
|
||||||
|
Box::new(crate::extension::dummy::DummyExtension::new("dummy")),
|
||||||
|
Box::new(crate::extension::vardummy::VarDummyExtension::new()),
|
||||||
|
Box::new(crate::extension::multiecho::MultiEchoExtension::new()),
|
||||||
|
],
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -737,4 +795,82 @@ mod tests {
|
||||||
|
|
||||||
verify_render(rendered, "RESULT");
|
verify_render(rendered, "RESULT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_render_variable_order() {
|
||||||
|
let config = get_config_for(
|
||||||
|
r###"
|
||||||
|
matches:
|
||||||
|
- trigger: 'test'
|
||||||
|
replace: "{{output}}"
|
||||||
|
vars:
|
||||||
|
- name: first
|
||||||
|
type: dummy
|
||||||
|
params:
|
||||||
|
echo: "hello"
|
||||||
|
- name: output
|
||||||
|
type: vardummy
|
||||||
|
params:
|
||||||
|
target: "first"
|
||||||
|
"###,
|
||||||
|
);
|
||||||
|
|
||||||
|
let renderer = get_renderer(config.clone());
|
||||||
|
let m = config.matches[0].clone();
|
||||||
|
let rendered = renderer.render_match(&m, 0, &config, vec![]);
|
||||||
|
verify_render(rendered, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_render_global_variable_order() {
|
||||||
|
let config = get_config_for(
|
||||||
|
r###"
|
||||||
|
global_vars:
|
||||||
|
- name: hello
|
||||||
|
type: dummy
|
||||||
|
params:
|
||||||
|
echo: "hello"
|
||||||
|
matches:
|
||||||
|
- trigger: 'test'
|
||||||
|
replace: "{{hello}} {{output}}"
|
||||||
|
vars:
|
||||||
|
- name: first
|
||||||
|
type: dummy
|
||||||
|
params:
|
||||||
|
echo: "world"
|
||||||
|
- name: output
|
||||||
|
type: vardummy
|
||||||
|
params:
|
||||||
|
target: "first"
|
||||||
|
- name: hello
|
||||||
|
type: global
|
||||||
|
"###,
|
||||||
|
);
|
||||||
|
|
||||||
|
let renderer = get_renderer(config.clone());
|
||||||
|
let m = config.matches[0].clone();
|
||||||
|
let rendered = renderer.render_match(&m, 0, &config, vec![]);
|
||||||
|
verify_render(rendered, "hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_render_multiple_results() {
|
||||||
|
let config = get_config_for(
|
||||||
|
r###"
|
||||||
|
matches:
|
||||||
|
- trigger: 'test'
|
||||||
|
replace: "hello {{var1.name}}"
|
||||||
|
vars:
|
||||||
|
- name: var1
|
||||||
|
type: multiecho
|
||||||
|
params:
|
||||||
|
name: "world"
|
||||||
|
"###,
|
||||||
|
);
|
||||||
|
|
||||||
|
let renderer = get_renderer(config.clone());
|
||||||
|
let m = config.matches[0].clone();
|
||||||
|
let rendered = renderer.render_match(&m, 0, &config, vec![]);
|
||||||
|
verify_render(rendered, "hello world");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
src/res/win/espansored.ico
Normal file
BIN
src/res/win/espansored.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -26,6 +26,8 @@ mod linux;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod macos;
|
mod macos;
|
||||||
|
|
||||||
|
pub mod modulo;
|
||||||
|
|
||||||
pub trait UIManager {
|
pub trait UIManager {
|
||||||
fn notify(&self, message: &str);
|
fn notify(&self, message: &str);
|
||||||
fn notify_delay(&self, message: &str, duration: i32);
|
fn notify_delay(&self, message: &str, duration: i32);
|
||||||
|
|
122
src/ui/modulo/mod.rs
Normal file
122
src/ui/modulo/mod.rs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
use crate::config::Configs;
|
||||||
|
use log::{error, info};
|
||||||
|
use std::io::{Error, Write};
|
||||||
|
use std::process::{Child, Command, Output};
|
||||||
|
|
||||||
|
pub struct ModuloManager {
|
||||||
|
modulo_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuloManager {
|
||||||
|
pub fn new(config: &Configs) -> Self {
|
||||||
|
let mut modulo_path: Option<String> = None;
|
||||||
|
// Check if the `MODULO_PATH` env variable is configured
|
||||||
|
if let Some(_modulo_path) = std::env::var_os("MODULO_PATH") {
|
||||||
|
modulo_path = Some(_modulo_path.to_string_lossy().to_string())
|
||||||
|
} else if let Some(ref _modulo_path) = config.modulo_path {
|
||||||
|
// Check the configs
|
||||||
|
modulo_path = Some(_modulo_path.to_owned());
|
||||||
|
} else {
|
||||||
|
// Check in the same directory of espanso
|
||||||
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
|
if let Some(parent) = exe_path.parent() {
|
||||||
|
let possible_path = parent.join("modulo");
|
||||||
|
let possible_path = possible_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if let Ok(output) = Command::new(&possible_path).arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
modulo_path = Some(possible_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise check if present in the PATH
|
||||||
|
if modulo_path.is_none() {
|
||||||
|
if let Ok(output) = Command::new("modulo").arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
modulo_path = Some("modulo".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref modulo_path) = modulo_path {
|
||||||
|
info!("Using modulo at {:?}", modulo_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { modulo_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.modulo_path.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_version(&self) -> Option<String> {
|
||||||
|
if let Some(ref modulo_path) = self.modulo_path {
|
||||||
|
if let Ok(output) = Command::new(modulo_path).arg("--version").output() {
|
||||||
|
let version = String::from_utf8_lossy(&output.stdout);
|
||||||
|
return Some(version.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invoke(&self, args: &[&str], body: &str) -> Option<String> {
|
||||||
|
if self.modulo_path.is_none() {
|
||||||
|
error!("Attempt to invoke modulo even though it's not configured");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref modulo_path) = self.modulo_path {
|
||||||
|
let mut command = Command::new(modulo_path);
|
||||||
|
command.args(args)
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped());
|
||||||
|
|
||||||
|
crate::utils::set_command_flags(&mut command);
|
||||||
|
|
||||||
|
let child = command.spawn();
|
||||||
|
|
||||||
|
match child {
|
||||||
|
Ok(mut child) => {
|
||||||
|
if let Some(stdin) = child.stdin.as_mut() {
|
||||||
|
match stdin.write_all(body.as_bytes()) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Get the output
|
||||||
|
match child.wait_with_output() {
|
||||||
|
Ok(child_output) => {
|
||||||
|
let output = String::from_utf8_lossy(&child_output.stdout);
|
||||||
|
|
||||||
|
// Check also if the program reports an error
|
||||||
|
let error = String::from_utf8_lossy(&child_output.stderr);
|
||||||
|
if !error.is_empty() {
|
||||||
|
error!("modulo reported an error: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(output.to_string());
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("error while getting output from modulo: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("error while sending body to modulo: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("unable to open stdin to modulo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("error reported when invoking modulo: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
14
src/utils.rs
14
src/utils.rs
|
@ -20,6 +20,7 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::create_dir;
|
use std::fs::create_dir;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
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)? {
|
||||||
|
@ -40,6 +41,19 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn set_command_flags(command: &mut Command) {
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
// Avoid showing the shell window
|
||||||
|
// See: https://github.com/federico-terzi/espanso/issues/249
|
||||||
|
command.creation_flags(0x08000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn set_command_flags(command: &mut Command) {
|
||||||
|
// NOOP on Linux and macOS
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user