Compare commits

...

772 Commits
v0.7.2 ... dev

Author SHA1 Message Date
Federico Terzi
b2356abe69
feat(ci): add automatic code-sign on Windows (#1352)
* feat(ci): first draft of signtool script

* feat(ci): add test signing step

* feat: another test commit that should fail

* feat: this one should succeed

* fix: create installer from resources executable

* fix: prevent reset of resources

* fix: remove test branch
2022-08-30 21:47:26 +02:00
Jaga Santagostino
8e919952e2
docs(misc): Fix wrong link to forms doc (#1345) 2022-08-25 21:24:50 +02:00
Federico Terzi
5256e3e79f
fix(misc): fix clippy warnings (#1342)
* fix(misc): fix clippy warnings

* fix(misc): fix clippy warnings

* fix(misc): fix clippy warnings

* fix(misc): fix clippy warnings

* fix(misc): fix clippy warnings
2022-08-25 21:23:55 +02:00
Federico Terzi
ee14983f7c chore(misc): remove GitHub Sponsors link 2022-08-20 10:38:26 +02:00
Andrea Giovine
ce01989f7c
fix(ci): improve version extraction (#1336)
* fix(ci): right use of grep

* fix(ci): remove unnecessary `head` command
2022-08-15 10:24:42 +02:00
Federico Terzi
d795d81fbf
feat(ci): add notarization step to CI (#1335)
* feat(ci): add notarization step on macOS release

* fix(ci): enable hardened runtime

* feat(ci): remove unused code
2022-08-15 10:22:15 +02:00
Federico Terzi
2ea452bf61
feat(ci): add macOS codesign step (#1334) 2022-08-13 21:53:53 +02:00
Federico Terzi
7fd1502bcf fix(misc): add missing feature flags in deb package build #1056 2022-08-13 10:59:48 +02:00
Federico Terzi
930bf807b5
feat: remove OpenSSL dependency on Linux #1056 (#1287) 2022-07-04 21:48:49 +02:00
Federico Terzi
1c660d9cfb chore: bump version 2022-07-04 21:26:44 +02:00
Federico Terzi
6b5d0a3d3b
feat(ci): optimize CI (#1283)
* feat(ci): attempt speeding up the CI

* fix(ci): avoid redundant runs on PRs

* fix: remove redundant comment

* fix: remove force option from cargo install

* feat(ci): first draft of modulo caching

* feat(modulo): add debug step

* fix(ci): attempt using absolute paths

* fix: use contexts instead of variables

* fix: use contexts

* fix: remove cache from test runs

* fix: change cache key

* fix: wrong indentation

* chore: add explainatory comment

* fix(ci): fix cache order
2022-07-03 20:15:00 +02:00
Federico Terzi
1b947ec188
fix(core): add multiple possible backup locations to fix. #1051 (#1282) 2022-07-03 14:03:45 +02:00
Federico Terzi
4d0cc7a6f1
feat(core): improve 'espanso stop' by handling non-graceful termination (#1281)
* feat(core): add workaround to forcefully stop Espanso if needed. #929

* feat(core): log system info to make debugging easier
2022-07-03 14:03:31 +02:00
Federico Terzi
abf31616c3
feat: implement alt-code emulation. #988 (#1277) 2022-07-02 20:58:51 +02:00
Federico Terzi
483e9c84ca chore(misc): sync up issue templates 2022-07-02 15:20:48 +02:00
Federico Terzi
ed2a2b25df Merge branch 'master' of https://github.com/federico-terzi/espanso into dev 2022-07-02 15:19:23 +02:00
Federico Terzi
a1101b907f Merge branch 'master' into dev 2022-07-02 15:18:44 +02:00
Federico Terzi
9f82b4e146 feat(package): add path information when manifest parsing fails. #1119 2022-06-12 12:19:24 +02:00
Federico Terzi
9e8a3f10da fix: wrong clippy name 2022-06-06 22:04:44 +02:00
Federico Terzi
bd4ae0f8aa fix: disable clippy warning 2022-06-06 21:55:39 +02:00
Federico Terzi
9059dcb4c1 fix: clippy warning 2022-06-06 21:22:00 +02:00
Federico Terzi
5e53acfc24 fix: update crossbeam crate version 2022-06-06 21:20:00 +02:00
Federico Terzi
12bb84f474 fix: missing license information. Fix #1114 2022-06-06 20:23:07 +02:00
Federico Terzi
f30395b8a6
feat: add alternative X11 injection backend based on libxdo (#1068)
* feat(inject): first steps towards xdotool inject fallback

* feat(inject): progress in the xdotool fallback implementation

* feat(config): add options for alternative xdotool backend

* feat(core): wire up alternative x11 backend
2022-04-12 22:08:06 +02:00
Federico Terzi
088080dd63 chore(misc): version bump 2022-04-12 21:40:07 +02:00
Federico Terzi
5df94b5031
Merge pull request #1053 from federico-terzi/dev
Version 2.1.5-beta
2022-03-30 20:49:32 +02:00
Federico Terzi
0b49784c6f fix(core): hide config_dir cli options to prevent non-intuitive behavior 2022-03-27 21:46:06 +02:00
Federico Terzi
e4309a4d17 fix(misc): vendor linuxdeploy and appimagetool to have reproducible AppImage builds 2022-03-27 21:21:11 +02:00
Federico Terzi
4bb540b095 chore(misc): version bump 2022-03-27 20:04:18 +02:00
Federico Terzi
e37aa98616
Merge pull request #1022 from federico-terzi/dev
Version 2.1.4-beta
2022-03-19 12:51:42 +01:00
Federico Terzi
e3887c0184 fix(misc): change cc tool to infer correct msvc location on Windows Server 2022 2022-03-06 21:23:44 +01:00
Federico Terzi
51527500e7 fix(modulo): change cc tool to infer correct msvc location on Windows Server 2022 2022-03-06 20:40:07 +01:00
Federico Terzi
880c7c0708 fix(misc): upgrade cc crate version to (hopefully) fix windows build on github ci 2022-03-05 22:05:15 +01:00
Federico Terzi
c77d9c560b fix(engine): fix clippy warning 2022-03-05 21:11:54 +01:00
Federico Terzi
cbca79ab0f feat(core): wire up options to delay injection after form/search gui 2022-03-05 20:28:19 +01:00
Federico Terzi
3abe84f8b0 feat(config): create options to delay injection after form/search gui 2022-03-05 20:27:49 +01:00
Federico Terzi
9760776904 fix(render): fix clippy warning 2022-02-15 23:07:35 +01:00
Federico Terzi
56b200609a feat(core): wire up new localized dates. #156 2022-02-15 21:58:00 +01:00
Federico Terzi
36a507488b feat(render): add support for localized dates. Fix #156 2022-02-15 21:57:40 +01:00
Federico Terzi
ec24100260 fix(detect): remove noisy log. Fix #961 2022-02-13 19:34:47 +01:00
Federico Terzi
dcea6fa178 fix(core): add missing wl-clipboard dependency to wayland deb package. Fix #956 2022-02-13 19:27:50 +01:00
Federico Terzi
4e9b49c3ad fix(modulo): change search bar window type to retain focus on Ubuntu. Fix #829 2022-02-13 19:20:29 +01:00
Federico Terzi
81245722b8 fix(misc): fix potential memory errors on macOS 2022-02-13 17:24:52 +01:00
Federico Terzi
a70d9b6770 fix(render): fix clippy warnings 2022-02-13 14:53:14 +01:00
Federico Terzi
ca8ca3001d fix(render): override PATH env variable on macOS to mitigate differences in shell extension executions. Fix #966 2022-02-13 13:23:08 +01:00
Federico Terzi
115e2f2138 fix(ci): upgrade Ubuntu Dockerfile rust version 2022-02-08 22:04:36 +01:00
Federico Terzi
c09e85ba85 feat(core): log when modulo exits with non-zero code. Fix #944 2022-02-08 21:31:54 +01:00
Federico Terzi
e304192dbd chore(misc): version bump 2022-02-08 21:09:37 +01:00
Federico Terzi
22987c6518
Merge pull request #949 from federico-terzi/dev
v2.1.3-alpha
2022-01-17 20:49:10 +01:00
Federico Terzi
cd9f26ce3e feat(ci): set up Homebrew release CI. #897 2022-01-12 21:46:06 +01:00
Federico Terzi
54bfed4c11 fix(ci): fix CI YAML indentation error 2022-01-12 21:42:24 +01:00
Federico Terzi
ff693dd998 feat(misc): add test action to publish homebrew cask 2022-01-12 21:40:47 +01:00
Federico Terzi
50a67baf03 feat(misc): add scripts to publish homebrew cask 2022-01-12 21:27:40 +01:00
Federico Terzi
b5ac6b85c5 fix(core): upgrade deb gtk version to 3 2022-01-10 22:46:12 +01:00
Federico Terzi
86632e38e6 fix(ci): fix wrong artifacts name 2022-01-10 21:42:45 +01:00
Federico Terzi
29f079729f fix(ci): fix debian package build error 2022-01-10 21:23:40 +01:00
Federico Terzi
3a6bd4f01e fix(ci): use fixed version of cargo-deb to avoid missing Rust 2021 crash 2022-01-10 21:04:47 +01:00
Federico Terzi
50f70b2ba5 fix(ci): fix wrong step name 2022-01-10 21:04:20 +01:00
Federico Terzi
671fd3c532 feat(misc): implement Deb package building pipeline. Fix #932 2022-01-10 20:53:56 +01:00
Federico Terzi
d7ee3b3833 fix(core): remove unused warning 2022-01-10 20:49:30 +01:00
Federico Terzi
85b8a90913 fix(core): remove paste_shortcut and backend override from Thunderbird patch. Fix #455 2022-01-10 19:19:00 +01:00
Federico Terzi
f07d297ce0 feat(core): wire up additional search terms and fix multiple triggers search. Fix #796 Fix #789 2022-01-04 21:19:09 +01:00
Federico Terzi
b31617a2f3 feat(config): add search terms option. #789 #796 2022-01-04 21:18:17 +01:00
Federico Terzi
90be91d1e9 feat(modulo): implement additional search items. #789 2022-01-04 21:17:09 +01:00
Federico Terzi
35ed59bd23 fix(config): disable 'search_trigger' by default. Fix #925 2022-01-04 20:33:28 +01:00
Federico Terzi
a143d951f8 fix(core): fix SecureInput troubleshoot link. Fix #892 2022-01-03 21:38:06 +01:00
Federico Terzi
fe5c9cf19d fix(engine): capitalize Espanso inside context menu 2022-01-03 21:37:39 +01:00
Federico Terzi
697f51e210 feat(inject): wire up inject_delay on macOS. Fix #849 2022-01-03 20:36:58 +01:00
Federico Terzi
7588900c06 feat(espanso): clarify 'espanso service' usage. Fix #920 2021-12-29 20:44:02 +01:00
Federico Terzi
68e2698a9c chore(misc): version bump 2021-12-28 20:38:33 +01:00
Federico Terzi
152402b74e
Merge pull request #917 from federico-terzi/dev
v2.1.2-alpha
2021-12-27 19:56:04 +01:00
Federico Terzi
a9f4cc1edf feat(config): disable toggle_key by default. Fix #916 2021-12-27 18:33:42 +01:00
Federico Terzi
8d0efc6436 chore(misc): remove legacy packager script 2021-12-25 23:20:36 +01:00
Federico Terzi
1f0761d140 fix(misc): remove hardcoded glib library inside AppImage. #900 2021-12-25 13:44:23 +01:00
Federico Terzi
9801b09ab6 feat(core): wire up choice extension. #850 2021-12-21 22:49:11 +01:00
Federico Terzi
873c70a248 feat(render): implement choice extension. #850 2021-12-21 22:48:10 +01:00
Federico Terzi
b2e6a8c8cc fix(modulo): fix form return key shortcut not working when ListBox component was selected. Fix #857 2021-12-21 21:32:31 +01:00
Federico Terzi
06f990fbd7 fix(misc): use appimage-extract-and-run option to (hopefully) enable appimagetool on Docker 2021-12-21 21:09:24 +01:00
Federico Terzi
92b85427ed fix(core): switch Wayland to the SHIFT+INSERT paste combination by default. Fix #899 2021-12-21 20:59:37 +01:00
Federico Terzi
08483e73b1 fix(misc): apply workaround to hopefully solve #900 2021-12-21 20:46:25 +01:00
Federico Terzi
d4d7609dd8 fix(clipboard): fix clippy warning 2021-12-13 23:53:09 +01:00
Federico Terzi
243c6604f8 fix(config): fix clippy warning 2021-12-13 23:52:57 +01:00
Federico Terzi
37893a3f74 fix(render): fix clippy warning 2021-12-13 23:08:00 +01:00
Federico Terzi
1e92cfef5c fix(misc): fix new clippy warnings 2021-12-11 18:19:56 +01:00
Federico Terzi
88b9c2ae03 fix(inject): fix clippy warning 2021-12-11 17:25:30 +01:00
Federico Terzi
bd2abeb8de fix(match): fix clippy warning 2021-12-11 17:25:18 +01:00
Federico Terzi
c23d99311a fix(ci): add explicit rust-script version to app-image build 2021-12-11 17:02:03 +01:00
Federico Terzi
38b4d437f3 fix(ci): install explicit rust-script version to mitigate CI failure: https://github.com/fornwall/rust-script/issues/42 2021-12-11 16:31:57 +01:00
Federico Terzi
f062a8ec35 fix(misc): explicitly add libglib dependency to (hopefully) mitigate #900 2021-12-11 16:26:11 +01:00
Federico Terzi
bf1d184ae9 feat(core): stop espanso when removing snap. #464 2021-12-11 11:36:16 +01:00
Federico Terzi
de1fefba51 fix(core): add patch for brave browser. Fix #876 2021-12-06 22:50:15 +01:00
Federico Terzi
74c5e5ae86 fix(clipboard): fix wrong xclip call. #885 2021-12-04 10:42:53 +01:00
Federico Terzi
50904c2d2b chore(misc): version bump 2021-12-04 10:42:19 +01:00
Federico Terzi
283b85818b
Merge pull request #886 from federico-terzi/dev
v2.1.1-alpha
2021-11-27 11:07:28 +01:00
Federico Terzi
73214cb59a fix(inject): improve X11 injector to handle dead keys. Fix #881 2021-11-22 22:54:44 +01:00
Federico Terzi
57b2f194e5 fix(inject): attempt setting explicit coregraphics dependency to fix compilation on macOS 11.6 2021-11-21 20:57:05 +01:00
Federico Terzi
20cfb9cb3b fix(misc): add xclip to snap packages to support alternative backend. #882 2021-11-21 19:42:08 +01:00
Federico Terzi
08853451c0 feat(core): wire up alternative x11 xclip clipboard backend and create patch for gedit. Fix #882 2021-11-21 19:40:53 +01:00
Federico Terzi
42cbb6e3de feat(config): create config option for alternative x11 xclip backend 2021-11-21 19:39:35 +01:00
Federico Terzi
41b72acdf1 feat(clipboard): add clipboard operation options and alternative x11 xclip backend. #882 2021-11-21 19:38:43 +01:00
Federico Terzi
c4f4f438d3 fix(core): prevent blocking when spawning the textview UI 2021-11-16 22:27:43 +01:00
Federico Terzi
8909ccdb4d feat(engine): add show logs entry in context menu 2021-11-16 22:27:14 +01:00
Federico Terzi
e2b6dcba38 feat(core): implement builtin to show logs 2021-11-15 22:31:57 +01:00
Federico Terzi
c69544c1e2 feat(engine): implement ShowLogs event 2021-11-15 22:31:26 +01:00
Federico Terzi
334e99b343 feat(core): implement builtins to show active config and app 2021-11-15 22:25:52 +01:00
Federico Terzi
9081ca76e7 feat(core): wire up textview UI events 2021-11-15 22:25:30 +01:00
Federico Terzi
84fd39a952 feat(engine): implement ShowText event 2021-11-15 22:24:30 +01:00
Federico Terzi
b2452ecca7 feat(modulo): add Esc handling in textview UI 2021-11-15 22:23:29 +01:00
Federico Terzi
02ec804604 feat(core): wire up textview UI 2021-11-15 21:50:58 +01:00
Federico Terzi
fff9f63f96 feat(modulo): implement textview UI 2021-11-15 21:50:35 +01:00
Federico Terzi
f7fdb06db8 Update issue templates 2021-11-15 20:56:38 +01:00
Federico Terzi
6168a28291 fix(modulo): fix wizard pages being cut with display scaling on Windows. Fix #871 2021-11-15 20:39:10 +01:00
Federico Terzi
411118b550 fix(core): fix string clipping operator that crashed with some unicode chars 2021-11-14 11:17:54 +01:00
Federico Terzi
bf1f3fc2e0 chore(misc): version bump 2021-11-13 16:01:26 +01:00
Federico Terzi
85f1598cf2
Merge pull request #866 from federico-terzi/dev
Release 2.1.0-alpha
2021-11-13 15:25:26 +01:00
Federico Terzi
0387ba8118 feat(render): add logic to enable variable injection escape 2021-11-12 22:13:37 +01:00
Federico Terzi
541c8d462c Merge branch 'dev' of github.com:federico-terzi/espanso into dev 2021-11-12 21:10:18 +01:00
Federico Terzi
38edd67bd0 feat(render): improve error log when variable is missing in forms 2021-11-12 21:10:04 +01:00
Federico Terzi
2745257ce9 fix(detect): add workaround to fix inconsistent modifier states on macOS. Fix #825 Fix #858 2021-11-12 20:49:56 +01:00
Federico Terzi
c7d6d69b72 feat(render): add tests for dict variables 2021-11-11 21:31:51 +01:00
Federico Terzi
317d3f2051 feat(core): wire up depends_on field for variables 2021-11-10 23:23:10 +01:00
Federico Terzi
aa26f27ed9 feat(config): implement depends_on field for variables 2021-11-10 23:22:53 +01:00
Federico Terzi
8acca4a366 feat(render): implement new variable resolution algorithm 2021-11-10 23:17:23 +01:00
Federico Terzi
57450bee32 feat(core): refactor form's choice and list values to accept multiline strings. Fix #855 2021-11-07 16:46:12 +01:00
Federico Terzi
d02c63dccf feat(render): add tip in logs when legacy form syntax throws error #856 2021-11-07 15:53:32 +01:00
Federico Terzi
a0412cdc7e feat(core): wire up inject vars option #856 2021-11-07 15:43:58 +01:00
Federico Terzi
9fb1d2a22a feat(config): add inject_vars option to matches. #856 2021-11-07 15:43:24 +01:00
Federico Terzi
34ba1e39e4 feat(config): refactor YAML match parsing to account for new form syntax #856 2021-11-07 15:29:45 +01:00
Federico Terzi
7244d34c7c feat(migrate): implement form syntax auto migration #856 2021-11-07 14:21:59 +01:00
Federico Terzi
fa26b1ffde feat(modulo): refactor form parser to accept new control syntax #856 2021-11-07 12:44:24 +01:00
Federico Terzi
29edcb900b chore(misc): version bump 2021-11-07 12:22:23 +01:00
Federico Terzi
40e8dace33 feat(render): add variable injection mechanism to renderer #856 2021-11-06 23:32:14 +01:00
Federico Terzi
9a2c27a202 chore(misc): version bump 2021-11-06 22:31:23 +01:00
Federico Terzi
3f5b0b04f2
Merge pull request #851 from federico-terzi/dev
v2.0.5-alpha release
2021-11-06 19:49:01 +01:00
Federico Terzi
98ef10716c fix(inject): fix wrong char type that prevented compilation on ARM. Fix #801 2021-11-06 10:10:04 +01:00
Federico Terzi
ac208ecf02 fix(modulo): check for 'wx-config-gtk3' command as well on Linux. Fix #840 2021-11-06 10:02:47 +01:00
Federico Terzi
80ed98c506 fix(clipboard): read wayland display from env-variable instead of hard-coding it. Fix #846 2021-11-06 09:25:45 +01:00
Federico Terzi
0983fa82d5 feat(inject): add log to improve uinput error debuggability. Related to #846 2021-11-06 09:19:28 +01:00
Federico Terzi
48262cd913 fix(core): block env-path register and service register on macOS if App Translocation is active. Fix #845 2021-11-05 21:22:35 +01:00
Federico Terzi
93ce220b62 fix(core): block the wizard if app has been translocated on macOS. Fix #844 2021-11-05 21:11:14 +01:00
Federico Terzi
fcc0a4ee50 fix(modulo): add wrapping to move bundle wizard page 2021-11-05 21:09:25 +01:00
Federico Terzi
2d84db409d feat(modulo): wire up move bundle page in wizard, related to #844 2021-11-05 20:29:37 +01:00
Federico Terzi
af6e91b516 fix(core): add delay in restart command to mitigate #839 2021-11-04 22:09:13 +01:00
Federico Terzi
5f1ec3719e fix(engine): fix clippy warning 2021-11-04 21:38:59 +01:00
Federico Terzi
7d5fd6aa8b fix(core): improve reporting of espanso start command. Relates to #839 2021-11-04 21:07:44 +01:00
Federico Terzi
ec7f1772dd fix(detect): unregister device from epoll when removed on Wayland. #836 2021-11-01 21:57:32 +01:00
Federico Terzi
846d0a2be3 fix(core): prevent certain commands from being run as root on macOS. Fix #648 2021-11-01 21:00:51 +01:00
Federico Terzi
3034dbe122 feat(modulo): submit forms by just pressing Enter when possible, and also add help label. Fix #709 2021-11-01 20:23:57 +01:00
Federico Terzi
b470aaad8a fix(core): improve handling of macOS env-path register command 2021-11-01 17:43:54 +01:00
Federico Terzi
30de95adef feat(core): create /usr/local/bin folder on macOS if it doesn't exist when adding espanso to the path. Fix #814 2021-11-01 17:36:45 +01:00
Federico Terzi
665790e8f5 fix(clipboard): fix missing return type of macOS. Fix #826 2021-11-01 11:41:33 +01:00
Federico Terzi
63b56c09ed feat(core): wire up rendering error notification 2021-10-31 22:22:01 +01:00
Federico Terzi
24087e8441 feat(engine): display and notify an error if rendering fails. Fix #774 2021-10-31 22:21:44 +01:00
Federico Terzi
93cb9680e6 feat(core): wire up status change notifications 2021-10-31 21:08:21 +01:00
Federico Terzi
a806e180e1 feat(engine): implement notification for enabled/disabled on linux. Fix #798 2021-10-31 21:07:58 +01:00
Federico Terzi
1bad4b2bfb fix(modulo): fix search bar shortcut handling for non-querty layouts. Fix #823 2021-10-31 20:21:04 +01:00
Federico Terzi
4ca35ea14d feat(core): implement 'espanso cmd' subcommand. Fix #832 2021-10-31 16:33:19 +01:00
Federico Terzi
70d97bf90d feat(engine): implement toggle event 2021-10-31 16:30:52 +01:00
Federico Terzi
4d3b1a5a59 fix(engine): avoid stripping away paragraph tags with markdown when multiple are present. Fix #811 2021-10-31 16:06:44 +01:00
Federico Terzi
8edf998e60 feat(core): implement 'match exec' subcommand. Fix #883, Fix #780 2021-10-31 15:44:25 +01:00
Federico Terzi
b38623376e feat(engine): add components to support ipc match exec 2021-10-31 15:43:50 +01:00
Federico Terzi
119d537fb7 feat(core): implement 'match list' command. Fix #786 2021-10-30 20:46:25 +02:00
Federico Terzi
382f708a02 chore(misc): version bump 2021-10-30 15:39:10 +02:00
Federico Terzi
4bd5f4c6c5
Merge pull request #821 from federico-terzi/dev
v2.0.4-alpha release
2021-10-26 22:11:44 +02:00
Federico Terzi
a00afd4874 feat(core): wire up windows layout caching interval setting. Fix #745 2021-10-25 21:31:38 +02:00
Federico Terzi
0faa838932 feat(config): add config option for windows keyboard layout cache duration #745 2021-10-25 21:31:11 +02:00
Federico Terzi
92645e0987 feat(detect): add option to lower the windows keyboard layout cache interval, #745 2021-10-25 21:30:16 +02:00
Federico Terzi
21bc75bc31 fix(engine): prevent other keys from interfering with toggle_key. Fix #815 2021-10-24 21:46:09 +02:00
Federico Terzi
8bab0f5e42 feat(core): add virtualbox patch on linux x11, related to #742 2021-10-22 21:55:24 +02:00
Federico Terzi
e6599c1036 fix(core): change backend in alacritty patch to fix #787 2021-10-22 21:32:31 +02:00
Federico Terzi
b20aa9c702 fix(ci): specify explicit cargo-make version 2021-10-22 21:18:28 +02:00
Federico Terzi
d9d7966cfc fix(detect): use c_char alias to fix build on ARM linux. Fix #801 2021-10-22 20:02:54 +02:00
Federico Terzi
4204bcf5ea fix(engine): fix bad handling of Iso Level3 Shift on Spanish layouts. Fix #802 2021-10-22 19:58:26 +02:00
Federico Terzi
cd89915525 fix(engine): add workaround to markdown parsing to avoid crashing espanso with malformed markdown. Fix #759 2021-10-21 21:52:22 +02:00
Federico Terzi
85bd14236a fix(modulo): remove rerun-if-changed directives that caused cargo to recompile modulo each time 2021-10-21 21:44:14 +02:00
Federico Terzi
869d8dbbaa fix(core): fix form extension not handling default field type. Fix #804 2021-10-21 21:39:47 +02:00
Federico Terzi
7588bdb1cf chore: version bump 2021-10-21 21:21:37 +02:00
Federico Terzi
b7e91d9b5d
Merge pull request #799 from federico-terzi/dev
v2.0.3-alpha release
2021-10-17 17:21:20 +02:00
Federico Terzi
72ef170a2c feat(misc): update snapcraft configuration for v2. Fix #795 2021-10-17 13:05:09 +02:00
Federico Terzi
39655269c3 fix(core): disable undo_backspace when using slow inject mode on linux 2021-10-16 20:48:18 +02:00
Federico Terzi
a584ee94ec fix(detect): filter out values from keyboard events when Alt key is pressed, related to #725 2021-10-16 14:42:22 +02:00
Federico Terzi
3b74b5f558 fix(engine): remove Alt from skipped modifiers on Windows, related to #725 2021-10-16 14:41:28 +02:00
Federico Terzi
789acc3d76 feat(core): pass necessary modifier state provider to engine 2021-10-16 13:23:50 +02:00
Federico Terzi
6f94ee3f38 fix(engine): filter out keyboard events while some modifiers are pressed. Fix #725 2021-10-16 13:23:27 +02:00
Federico Terzi
f9b256c1a3 fix(modulo): fix bug that prevented forms from being displayed on Linux 2021-10-16 12:22:28 +02:00
Federico Terzi
904f24a438 fix(ci): attempt to fix linux CI that failed on apt install step 2021-10-16 12:08:32 +02:00
Federico Terzi
70bff10fd5 fix(engine): prevent events from stacking up when the search bar is open. Fix #781 2021-10-16 11:19:26 +02:00
Federico Terzi
a257cf96e5 fix(ci): add explicit macOS minimum version to CI to (hopefully) fix #785 2021-10-14 19:37:47 +02:00
Federico Terzi
93b6c8e75a fix(render): remove outdated test 2021-10-13 22:38:35 +02:00
Federico Terzi
44dd54e2e1 fix(core): handle AllModifiersRelease event on macOS to prevent modifier state getting out-of-sync with shortcuts. Fix #791 2021-10-13 22:36:25 +02:00
Federico Terzi
0b23a5cc30 feat(engine): refactor funnel to allow skipping of events 2021-10-13 22:35:02 +02:00
Federico Terzi
379ab08cf1 feat(detect): handle modifiers release event on macOS 2021-10-13 22:34:36 +02:00
Federico Terzi
7987e01d72 fix(render): pprevent the shell extension from failing if stderr is not empty but process exits successfully. Fix #563 2021-10-13 22:03:30 +02:00
Federico Terzi
af70305ccc fix(core): fix edge case that prevented daemon from detecting a worker failure due to a signal. Fix #788 2021-10-13 21:17:17 +02:00
Federico Terzi
82c05d6615 fix(core): add workaround to prevent launcher process from appearing 'not responding' on macOS 2021-10-13 21:15:23 +02:00
Federico Terzi
806ac8112e feat(mac-utils): add methods to start and stop headless eventloops 2021-10-13 21:14:41 +02:00
Federico Terzi
12ba0b8755 fix(detect): add missing case that prevented some keys from being detected correctly on Windows. Fix #307 2021-10-12 22:17:19 +02:00
Federico Terzi
cba4c41006 fix(modulo): fix cut button in welcome screen when using 125% text scaling on windows. Fix #777 2021-10-12 21:26:50 +02:00
Federico Terzi
45b20fd067 fix(modulo): fix icon loading when path includes unicode chars. Fix #554 2021-10-12 21:19:05 +02:00
Federico Terzi
48bd591bd6 fix(detect): add flag to ToUnicodeEx call to prevent spurious random characters when pressing ALT+Arrow keys. Fix #552 2021-10-12 20:31:22 +02:00
Federico Terzi
6e4be12551 chore(core): bump version 2021-10-12 19:28:08 +02:00
Federico Terzi
89aded62e0 fix(core): add support for older macOS versions. Fix #785 2021-10-12 18:06:40 +02:00
Federico Terzi
3e88af0139 fix(core): rename v2 resources to avoid reusing old assets 2021-10-12 18:05:54 +02:00
Federico Terzi
f436a9757d
Merge pull request #784 from federico-terzi/dev
v2.0.2-alpha release
2021-10-11 21:01:40 +02:00
Federico Terzi
ea698e13b3 feat(core): don't show AutoStart wizard page on Linux 2021-10-11 19:23:03 +02:00
Federico Terzi
c24acc6e46 fix(core): change worker exit code that was conflicting with Rust's panics. Fix #783 2021-10-11 19:17:58 +02:00
Federico Terzi
ceade8d172 fix(core): prevent keyboard layout watcher from stopping when receiving invalid layout 2021-10-11 19:11:13 +02:00
Federico Terzi
2c25d60e87 fix(core): fix config watcher debouncing mechanism. Fix #769 2021-10-10 22:36:46 +02:00
Federico Terzi
d1ebcf62c8 fix(core): change Thunderbird parameters to account for edge cases described in #351 2021-10-10 19:27:44 +02:00
Federico Terzi
7b7489a8d8 feat(core): add patch for Thunderbird. Fix #130 Fix #376 2021-10-10 19:02:31 +02:00
Federico Terzi
2eabdf90bf feat(core): port 'espanso status' subcommand. Fix #775 2021-10-10 11:29:23 +02:00
Federico Terzi
2d8e30525d chore(core): version bump 2021-10-10 11:21:38 +02:00
Federico Terzi
84b029854a fix(migrate): update yaml-rust dependency to fix bad handling of multiline strings starting with a space. Fix #771 2021-10-09 21:25:56 +02:00
Federico Terzi
aa9465490b
feat(misc): release alpha v2.0.1
v2.0.1 alpha release
2021-10-09 19:16:55 +02:00
Federico Terzi
3ac6835c45 chore(core): bump version 2021-10-09 18:14:15 +02:00
Federico Terzi
0f01ab3e3a feat(core): wire up auto startup wizard page 2021-10-09 17:33:53 +02:00
Federico Terzi
0804ef5e27 feat(modulo): add auto startup wizard page 2021-10-09 17:33:39 +02:00
Federico Terzi
2680a5ef4f fix(core): fix warning on Unix 2021-10-09 16:40:54 +02:00
Federico Terzi
fa139a426b fix(core): fix missing parameter on Unix 2021-10-09 16:07:23 +02:00
Federico Terzi
3c455b6d4b feat(core): implement restart subcommand 2021-10-09 15:31:39 +02:00
Federico Terzi
5a75a04d5a feat(core): port 'edit' subcommand implementation from previous version 2021-10-09 15:02:44 +02:00
Federico Terzi
8f291f4717 fix(core): fix bug that caused the daemon to mistakenly restart the worker on Windows and macOS 2021-10-06 22:45:16 +02:00
Federico Terzi
c4070d0044 fix(modulo): update wrong link in wizard 2021-10-06 22:35:41 +02:00
Federico Terzi
7baa668b7d fix(config): fix include patterns not being included when starting with an underscore. Fix #762 2021-10-06 22:13:08 +02:00
Federico Terzi
6c3bd577b6
Merge pull request #764 from federico-terzi/dev-1.x
chore(misc): migrate v2 branch to development branch
2021-10-06 21:21:30 +02:00
Federico Terzi
3d7e711555 feat(ci): create artifacts also for dev commits, but publish releases only if master 2021-10-06 19:16:52 +02:00
Federico Terzi
2265261a3c chore(misc): remove outdated azure pipelines configuration 2021-10-06 19:05:20 +02:00
Federico Terzi
51b0131d39 docs(misc): update SECURITY.md file with notice about upcoming updates 2021-10-06 19:02:53 +02:00
Federico Terzi
210757c051 docs(misc): update compilation instructions 2021-10-06 18:58:50 +02:00
Federico Terzi
9ec361787c fix(mac-utils): fix warning 2021-10-06 19:34:53 +02:00
Federico Terzi
4f37a0abd1 fix(detect): fix warning 2021-10-06 19:25:20 +02:00
Federico Terzi
947edb0cbf fix(detect): fix warnings 2021-10-06 19:17:59 +02:00
Federico Terzi
d0e23ca2a8 fix(core): fix warnings 2021-10-06 19:17:44 +02:00
Federico Terzi
c7047275b0 fix(inject): fix warnings 2021-10-06 19:17:32 +02:00
Federico Terzi
55930364f8 fix(ci): install linux dependencies before clippy 2021-10-06 19:07:05 +02:00
Federico Terzi
bab9c1dc92 fix(detect): fix warnings on Wayland 2021-10-06 19:06:43 +02:00
Federico Terzi
cb372f9b9d fix(inject): fix warning 2021-10-06 19:06:28 +02:00
Federico Terzi
638863170c style(ui): fix formatting 2021-10-06 18:42:06 +02:00
Federico Terzi
a8a5ef16a2 style(render): fix formatting 2021-10-06 18:41:56 +02:00
Federico Terzi
c10dab9e80 style(path): fix formatting 2021-10-06 18:40:31 +02:00
Federico Terzi
3fd8bfccab style(package): fix formatting 2021-10-06 18:40:16 +02:00
Federico Terzi
308d1848be style(modulo): fix formatting 2021-10-06 18:39:55 +02:00
Federico Terzi
e227fae326 style(migrate): fix formatting 2021-10-06 18:39:42 +02:00
Federico Terzi
b0e836a7e7 style(mac-utils): fix formatting 2021-10-06 18:39:30 +02:00
Federico Terzi
9f368d8b87 style(kvs): fix formatting 2021-10-06 18:39:15 +02:00
Federico Terzi
f22037beda style(ipc): fix formatting 2021-10-06 18:39:01 +02:00
Federico Terzi
ad79f58dd4 style(inject): fix formatting 2021-10-06 18:38:42 +02:00
Federico Terzi
7529c9de38 style(info): fix formatting 2021-10-06 18:38:23 +02:00
Federico Terzi
8fe508bf93 style(engine): fix formatting 2021-10-06 18:38:02 +02:00
Federico Terzi
d11af8b1c2 style(detect): fix formatting 2021-10-06 18:37:32 +02:00
Federico Terzi
be68a1f8ff style(config): fix formatting 2021-10-06 18:37:15 +02:00
Federico Terzi
9202c1189c style(clipboard): fix formatting 2021-10-06 18:36:42 +02:00
Federico Terzi
9e3742f273 style(core): fix formatting 2021-10-06 18:36:23 +02:00
Federico Terzi
5d7b13e0bc feat(ci): add clippy and formatting checks 2021-10-05 23:20:07 +02:00
Federico Terzi
c318b1aaf6 fix(render): fix warnings 2021-10-05 23:18:39 +02:00
Federico Terzi
2cd974c80b fix(modulo): fix warnings 2021-10-05 23:18:26 +02:00
Federico Terzi
e3788ea868 fix(mac-utils): fix warnings 2021-10-05 23:18:11 +02:00
Federico Terzi
db0ea18a84 fix(kvs): fix warnings 2021-10-05 23:17:51 +02:00
Federico Terzi
3afd0a2605 fix(config): fix warnings 2021-10-05 23:17:40 +02:00
Federico Terzi
2fd6927030 fix(core): fix warnings 2021-10-05 23:17:22 +02:00
Federico Terzi
4b57dd04ca fix(render): fix warnings 2021-10-05 22:07:42 +02:00
Federico Terzi
71f0b575d3 fix(path): fix warnings 2021-10-05 22:07:29 +02:00
Federico Terzi
562c44127d fix(package): fix warnings 2021-10-05 22:07:18 +02:00
Federico Terzi
16350123d2 fix(modulo): fix warnings 2021-10-05 22:07:04 +02:00
Federico Terzi
8ecee79aa7 fix(migrate): fix warnings 2021-10-05 22:06:53 +02:00
Federico Terzi
11fd7ce167 fix(match): fix warnings 2021-10-05 22:06:39 +02:00
Federico Terzi
e112633509 fix(kvs): fix warnings 2021-10-05 22:06:26 +02:00
Federico Terzi
2ecf86d9fa fix(detect): fix warnings 2021-10-05 22:06:06 +02:00
Federico Terzi
42d4351f4b fix(config): fix warnings 2021-10-05 22:05:53 +02:00
Federico Terzi
d8412865f7 fix(clipboard): fix warnings 2021-10-05 22:05:37 +02:00
Federico Terzi
8f11bf6cc6 fix(core): fix warnings 2021-10-05 22:05:22 +02:00
Federico Terzi
2aa5c03246 feat(misc): update readme 2021-10-05 21:08:05 +02:00
Federico Terzi
291bec95f7 chore(misc): remove legacy ci folder 2021-10-05 20:54:45 +02:00
Federico Terzi
5b21c9490e feat(ci): point release ci to master branch 2021-10-05 20:52:29 +02:00
Federico Terzi
8e4f1920c7 fix(ci): use hash instead of branch name 2021-10-04 21:36:15 +02:00
Federico Terzi
cfa67d1b1b feat(ci): use current branch as base commit for tag 2021-10-04 21:25:41 +02:00
Federico Terzi
e94d97aae1 fix(modulo): explicitly specify built-in version of img libs on macOS 2021-10-04 21:21:26 +02:00
Federico Terzi
13da059a48 feat(ci): implement step to create new release 2021-10-04 20:42:38 +02:00
Federico Terzi
6f142681b5 fix(ci): change keychain name 2021-10-04 19:03:16 +02:00
Federico Terzi
8cac8e7479 fix(ci): fix invalid char in m1 pipeline 2021-10-04 18:13:56 +02:00
Federico Terzi
1de9ef9111 fix(ci): add missing char in base64 decoding 2021-10-04 16:20:18 +02:00
Federico Terzi
d6e225103b fix(ci): add missing target install in mac m1 pipeline 2021-10-04 15:31:05 +02:00
Federico Terzi
a70dcdd7d2 feat(ci): add macos arm ci pipeline 2021-10-04 14:56:58 +02:00
Federico Terzi
2979390ddf feat(ci): add mac intel release pipeline 2021-10-04 14:25:57 +02:00
Federico Terzi
912a1fd209 fix(ci): preserve rustup-related env-vars in ci build and fix a few errors 2021-10-03 18:29:10 +02:00
Federico Terzi
065e24b7f2 fix(ci): fix bad windows path and first steps in linux ci 2021-10-03 17:27:14 +02:00
Federico Terzi
c73a95d66a fix(ci): fix broken path in Windows pipeline 2021-10-03 16:44:30 +02:00
Federico Terzi
1f2532f32f fix(ci): fix wrong powershell command 2021-10-03 15:56:09 +02:00
Federico Terzi
dd6bb013ab fix(ci): add missing cargo make step 2021-10-03 15:17:54 +02:00
Federico Terzi
3b00bf53f3 fix(ci): fix error in extract version step 2021-10-03 15:16:15 +02:00
Federico Terzi
679e95f952 feat(ci): first draft of windows release ci 2021-10-03 15:13:32 +02:00
Federico Terzi
d487522849 fix(ci): remove redundant test suite from m1 pipeline 2021-10-03 14:28:46 +02:00
Federico Terzi
55d5010699 feat(ci): add missing test runs in ci pipeline 2021-10-03 14:03:18 +02:00
Federico Terzi
a426131b08 fix(ci): clean up ci pipeline 2021-10-03 13:14:26 +02:00
Federico Terzi
d83cf60f8e fix(ci): attempt with universal binary flag 2021-10-03 12:32:03 +02:00
Federico Terzi
aa1aaeca99 fix(ci): add debug code for M1 pipeline 2021-10-03 11:07:56 +02:00
Federico Terzi
6930687d01 fix(ci): add missing c flags in M1 pipeline 2021-10-03 10:13:09 +02:00
Federico Terzi
b7a80c956a fix(ci): explicitly use builtin jpeg and png libs in m1 pipeline 2021-10-02 21:54:54 +02:00
Federico Terzi
c90ae3c128 fix(ci): another attempt to fix m1 ci 2021-10-02 21:26:38 +02:00
Federico Terzi
e84abc21bc fix(ci): attempt to fix modulo building process on CI for M1 processors 2021-10-02 20:58:48 +02:00
Federico Terzi
82d1b2f4e1 fix(ci): attempt to fix m1 compilation problem 2021-10-02 19:45:30 +02:00
Federico Terzi
0afa3f4208 fix(ci): attempt to fix m1 compilation problem on GH Actions
This reverts commit e33fc6993c.
2021-10-02 19:36:21 +02:00
Federico Terzi
e33fc6993c Revert "fix(ci): attempt to fix m1 compilation problem on GH Actions"
This reverts commit 1caa5d2a57.
2021-10-02 18:44:00 +02:00
Federico Terzi
1caa5d2a57 fix(ci): attempt to fix m1 compilation problem on GH Actions 2021-10-02 18:14:41 +02:00
Federico Terzi
52632ec51c fix(ci): add tmate to m1 pipeline to ease debugging 2021-10-02 15:51:28 +02:00
Federico Terzi
2077eadd21 fix(ci): attempt to use another xcode sdk version on m1 2021-10-02 15:15:16 +02:00
Federico Terzi
c3dcae626b fix(ci): attempt to fix missing packages 2021-10-02 15:04:19 +02:00
Federico Terzi
6db6b496c7 fix(ci): add missing steps in CI pipeline 2021-10-02 14:35:44 +02:00
Federico Terzi
680d0cf1de fix(ci): add missing step in macOS arm check 2021-10-02 14:22:12 +02:00
Federico Terzi
8ab7d0be07 feat(ci): add basic CI pipeline 2021-10-02 14:20:21 +02:00
Federico Terzi
14c70ff62d fix(engine): improve debug information when rendering error occurs 2021-09-30 22:20:20 +02:00
Federico Terzi
c85020fdfa fix(render): add quotes to missing var error 2021-09-30 22:20:00 +02:00
Federico Terzi
ad4762f012 feat(core): add VSCode patch on Windows 2021-09-25 15:56:28 +02:00
Federico Terzi
c1caa7137f fix(core): fix v2 icon being ignored on Linux 2021-09-09 22:05:50 +02:00
Federico Terzi
9d64658d5c feat(core): implement env-path register on Linux 2021-09-09 22:03:32 +02:00
Federico Terzi
36984a9426 feat(core): start espanso launcher when starting app-image directly 2021-09-09 21:29:04 +02:00
Federico Terzi
9628361db0 fix(core): disable undo_backspace on Wayland due to stability issues 2021-09-09 20:06:12 +02:00
Federico Terzi
27f53c386c fix(core): fix problematic handling of target directory in migration 2021-09-09 19:45:50 +02:00
Federico Terzi
b5422b939d feat(core): wire up evdev_modifier_delay option 2021-09-06 23:12:49 +02:00
Federico Terzi
fc96d80791 feat(config): add evdev_modifier_delay option 2021-09-06 23:11:47 +02:00
Federico Terzi
d20eaf1aba feat(inject): add evdev_modifier_delay option 2021-09-06 23:10:56 +02:00
Federico Terzi
d86c6f7c46 feat(package): implement gitlab package provider 2021-09-06 22:37:14 +02:00
Federico Terzi
db77e9617a feat(core): implement external flag in installation command 2021-09-06 22:23:17 +02:00
Federico Terzi
36ff784684 feat(core): wire up espanso hub package index caching flags 2021-09-06 22:14:26 +02:00
Federico Terzi
bfb5a8ac4c feat(package): implement espanso hub package index caching 2021-09-06 22:13:54 +02:00
Federico Terzi
23a73f7ea2 feat(package): implement first version of espanso-hub provider 2021-09-05 22:59:47 +02:00
Federico Terzi
393f431bc3 feat(core): implement list, uninstall and update package commands 2021-09-03 23:23:35 +02:00
Federico Terzi
64350de3a9 feat(package): complete archiver implementation 2021-09-03 23:13:37 +02:00
Federico Terzi
2c955198ff feat(package): add archiver tests 2021-09-03 22:01:48 +02:00
Federico Terzi
c9e8f3e480 fix(core): fix race condition when multiple files are changed 2021-09-01 22:43:25 +02:00
Federico Terzi
55c8ba6b75 feat(core): implement first draft of package install command 2021-09-01 22:12:05 +02:00
Federico Terzi
5714ebe131 feat(package): progress in package archiver implementation 2021-09-01 22:11:28 +02:00
Federico Terzi
89e487747a feat(package): progress in Github and Gitlab package providers 2021-08-31 22:57:22 +02:00
Federico Terzi
d74d6e37bc feat(package): progress in the git provider 2021-08-30 22:05:16 +02:00
Federico Terzi
2c1e7144c7 feat(core): add espanso-package dependency 2021-08-29 19:16:57 +02:00
Federico Terzi
e95fb441bd feat(package): early work in package resolvers 2021-08-29 19:16:38 +02:00
Federico Terzi
03f09e4de8 fix(core): add compilation flag to fix compile error when modulo is disabled 2021-08-29 18:04:37 +02:00
Federico Terzi
86e4101cb3 fix(misc): remove previous espanso versions when using windows installer 2021-08-25 22:15:20 +02:00
Federico Terzi
d95ec773ad fix(misc): fix arch naming on windows installer 2021-08-25 22:14:55 +02:00
Federico Terzi
27f665d0d4 feat(modulo): improve Wizard wording 2021-08-24 23:19:30 +02:00
Federico Terzi
51e0589bf9 fix(core): add missing flag in Wizard GUI 2021-08-24 23:13:57 +02:00
Federico Terzi
e92078e3ca feat(core): wire up WrongEditionPage GUI 2021-08-24 23:04:55 +02:00
Federico Terzi
ad029b626e feat(modulo): add wrong edition page in Wizard GUI 2021-08-24 23:04:25 +02:00
Federico Terzi
0d70cd8405 feat(config): add support for search-related fields in legacy config 2021-08-24 20:09:47 +02:00
Federico Terzi
0cc52ccf63 feat(modulo): sort search items 2021-08-24 20:05:00 +02:00
Federico Terzi
9b52a06a8b feat(core): wire up configurable search bar hint 2021-08-24 19:54:26 +02:00
Federico Terzi
6726436674 feat(engine): refactor match detected event to support configurable hint 2021-08-24 19:54:02 +02:00
Federico Terzi
96ce9090f8 feat(modulo): make search bar hint configurable 2021-08-24 19:53:16 +02:00
Federico Terzi
eb7474763b fix(detect): fix bug that caused espanso to consume 100% on some occasions. Fix #737 Fix #125 2021-08-24 19:20:08 +02:00
Federico Terzi
6c68a9d51d fix(modulo): fix bad searchbar handling of utf8 chars 2021-08-22 21:53:21 +02:00
Federico Terzi
babd1f715e feat(core): wire up win32_exclude_orphan_events option 2021-08-22 21:47:04 +02:00
Federico Terzi
7eddfd1f12 feat(config): add win32_exclude_orphan_events option 2021-08-22 21:46:26 +02:00
Federico Terzi
53eef3ce7b fix(detect): exclude software-generated events by default on Windows to avoid reading back espanso's events 2021-08-22 21:45:49 +02:00
Federico Terzi
2129494ae3 feat(core): wire up suppression middleware (enable=false in config) 2021-08-22 20:15:01 +02:00
Federico Terzi
c624ca676d feat(engine): implement suppress middleware 2021-08-22 20:14:21 +02:00
Federico Terzi
09109e0d57 fix(engine): add missing injection events in delay middleware 2021-08-21 09:42:28 +02:00
Federico Terzi
ab96b966dd fix(core): add missing field in patch configuration 2021-08-21 09:30:15 +02:00
Federico Terzi
82ef974a6b fix(config): add missing fields to pretty dump 2021-08-21 09:29:50 +02:00
Federico Terzi
eb727abeec feat(config): add enable option 2021-08-21 09:21:59 +02:00
Federico Terzi
ce802bc72e fix(migrate): adjust wrong migrate fields 2021-08-21 08:36:36 +02:00
Federico Terzi
e8db703014 feat(core): wire up show_icon and show_notification options 2021-08-20 19:36:52 +02:00
Federico Terzi
fa149471f2 feat(config): add gui-related fields 2021-08-20 18:45:22 +02:00
Federico Terzi
19aab8987e feat(modulo): implement built-in filter and help text 2021-08-18 15:52:48 +02:00
Federico Terzi
0f361ba6d0 feat(core): add built-in filter in search bar 2021-08-18 15:52:19 +02:00
Federico Terzi
d4cede1862 feat(core): add exit and restart built-ins 2021-08-16 20:39:15 +02:00
Federico Terzi
7439cc1081 feat(core): wire up undo feature 2021-08-15 11:10:31 +02:00
Federico Terzi
fb45f92b69 feat(engine): implement undo feature 2021-08-15 11:10:13 +02:00
Federico Terzi
eab305d45f feat(config): add undo_backspace option 2021-08-15 11:01:36 +02:00
Federico Terzi
97130e972c feat(detect): filter out espanso-generated events on macOS 2021-08-14 16:07:14 +02:00
Federico Terzi
0a38fafae9 feat(inject): mark espanso-generated event with magic location on macOS 2021-08-14 16:06:54 +02:00
Federico Terzi
bf1d17ad7d feat(core): cache app info provider results 2021-08-14 11:43:23 +02:00
Federico Terzi
1713b078be refactor(core): move engine into separate module 2021-08-14 11:08:20 +02:00
Federico Terzi
974e405b23 feat(engine): move engine into separate module 2021-08-14 11:07:43 +02:00
Federico Terzi
de8879e03c feat(misc): add make task to build universal mac bundle 2021-08-14 10:08:25 +02:00
Federico Terzi
c8887fdee0 fix(misc): make build script fail if cargo fails 2021-08-13 22:22:17 +02:00
Federico Terzi
ba4f9400ea feat(core): implement hotkey detection for builtin matches and wire up search 2021-08-13 21:19:46 +02:00
Federico Terzi
fa2709d43b feat(config): implement search_shortcut field 2021-08-13 21:17:01 +02:00
Federico Terzi
5c06699a80 feat(core): add search bar context menu entry 2021-08-13 20:39:00 +02:00
Federico Terzi
80a197376c feat(core): implement search bar 2021-08-13 20:35:22 +02:00
Federico Terzi
d569d96dc6 feat(config): add search trigger field 2021-08-13 20:34:15 +02:00
Federico Terzi
db1a3e1247 feat(config): wire up match label field 2021-08-13 20:21:15 +02:00
Federico Terzi
e3dc943a73 feat(info): add fallback title detection on macOS 2021-08-12 20:06:24 +02:00
Federico Terzi
35d931e49c fix(core): fix layout watcher channel crashing on macOS and Windows 2021-08-12 19:50:08 +02:00
Federico Terzi
88d589e851 feat(core): improve engine flag handling 2021-08-10 22:22:45 +02:00
Federico Terzi
c91e8f56bc feat(core): implement keyboard layout watcher and refactor worker start flag 2021-08-10 22:18:39 +02:00
Federico Terzi
fb93754f90 fix(inject): fix possible memory leak 2021-08-10 21:11:04 +02:00
Federico Terzi
0f6c32350e fix(detect): fix possible memory leak 2021-08-10 21:10:56 +02:00
Federico Terzi
a54d850cf3 fix(inject): fix possible memory leak 2021-08-10 20:29:27 +02:00
Federico Terzi
20d9324f38 fix(detect): fix possible memory leak 2021-08-10 20:29:14 +02:00
Federico Terzi
b9ae27a1e9 feat(core): pass RLVMO config to detect and inject modules 2021-08-10 20:16:16 +02:00
Federico Terzi
8691e68e6b feat(config): add default and display implementation to RLMVO struct 2021-08-10 20:15:32 +02:00
Federico Terzi
52b73ba031 fix(core): add missing patch field 2021-08-09 22:53:53 +02:00
Federico Terzi
47eb2b0b69 feat(config): add options to configure keyboard layout on Wayland 2021-08-09 22:53:32 +02:00
Federico Terzi
c68d59797e fix(detect): fix possible undefined behavior 2021-08-09 22:24:13 +02:00
Federico Terzi
bb4e88c445 fix(detect): fix possible undefined behavior 2021-08-09 22:24:13 +02:00
Federico Terzi
1aea1261f0 feat(detect): add active layout detection on Gnome (wayland) 2021-08-09 21:57:01 +02:00
Federico Terzi
e9d4dde750 feat(detect): add active keyboard layout detection on X11 2021-08-07 10:22:41 +02:00
Federico Terzi
733d7e2ff4 feat(ui): implement Heartbeat on Linux 2021-08-06 19:56:36 +02:00
Federico Terzi
02b486dc7a feat(ui): implement Heartbeat on macOS 2021-08-06 19:41:59 +02:00
Federico Terzi
f494d46fee feat(ui): implement heartbeat on Windows 2021-08-06 19:26:22 +02:00
Federico Terzi
36400afab4 feat(core): add heartbeat handling 2021-08-06 19:25:58 +02:00
Federico Terzi
103699ec36 feat(modulo): change welcome title 2021-08-06 18:42:07 +02:00
Federico Terzi
378152e565 fix(core): Fix XTerm patch on Linux 2021-08-03 19:15:19 +02:00
Federico Terzi
65e1a9aaf3 feat(core): add patches for common linux terminals 2021-08-02 21:59:48 +02:00
Federico Terzi
497c845b88 fix(config): fix wrong mapping in legacy config 2021-08-02 21:32:57 +02:00
Federico Terzi
f9f7db623d feat(core): add LibreOffice writer patch 2021-08-01 23:17:01 +02:00
Federico Terzi
40ad3c8551 fix(info): use alternative method to gather active window on X11 2021-08-01 23:09:09 +02:00
Federico Terzi
c85e063e71 feat(docs): add required packages to compilation docs on Ubuntu 2021-08-01 21:51:13 +02:00
Federico Terzi
04bc9b904d feat(core): add SecureInput context menu entries 2021-08-01 19:06:18 +02:00
Federico Terzi
79be8d2988 feat(core): implement first draft of secure input workaround script 2021-08-01 15:44:09 +02:00
Federico Terzi
278a3fe008 feat(core): improve secure input detection interval handling 2021-08-01 11:31:05 +02:00
Federico Terzi
d23de2e3c9 feat(core): implement builtin to dump app info 2021-07-31 22:56:29 +02:00
Federico Terzi
7f3c70c8df feat(core): improve config dump when patched 2021-07-31 22:28:18 +02:00
Federico Terzi
e151364014 feat(config): improve label field fallback 2021-07-31 22:27:52 +02:00
Federico Terzi
dcc2cd2708 feat(core): add builtin function to dump current config 2021-07-31 22:06:39 +02:00
Federico Terzi
3cdc964777 feat(config): add config pretty dump 2021-07-31 22:05:30 +02:00
Federico Terzi
b6fcc1723c fix(core): add SHIFT as conflicting modifier to avoid bad pasting when trigger ends with shifted char 2021-07-31 22:05:01 +02:00
Federico Terzi
48d05a3f32 feat(core): first draft of builtin matches 2021-07-31 21:19:50 +02:00
Federico Terzi
90db84b92f feat(core): wire up apply_patch option 2021-07-31 17:17:47 +02:00
Federico Terzi
4504977384 feat(config): add option to skip patching 2021-07-31 17:17:24 +02:00
Federico Terzi
0112603ff7 feat(core): complete work on patch system structure 2021-07-31 16:58:54 +02:00
Federico Terzi
bc19b412ae feat(core): first steps in patch system implementation 2021-07-31 11:54:53 +02:00
Federico Terzi
24910859ac feat(config): refactor config_store to use Arc instead of plain references 2021-07-31 11:54:13 +02:00
Federico Terzi
96309709b2 fix(core): fix service register on linux when executed from app-image 2021-07-30 20:40:20 +02:00
Federico Terzi
32775d414c fix(path): use /home/freddy/.cache as runtime directory to avoid loosing kvs store data on reboot 2021-07-30 20:06:15 +02:00
Federico Terzi
93a7caa101 fix(detect): fix incorrect modifier sync on Wayland 2021-07-30 19:49:03 +02:00
Federico Terzi
bee79eb4a4 fix(detect): fix wrong default feature and improve context information 2021-07-30 19:37:18 +02:00
Federico Terzi
33b9012802 feat(detect): implement modifier state synchronization on Wayland 2021-07-30 19:31:33 +02:00
Federico Terzi
08e86c7e35 fix(core): remove spurious print 2021-07-28 22:41:52 +02:00
Federico Terzi
3aea65de72 feat(core): implement KeyStateProvider for wayland injector 2021-07-28 22:37:46 +02:00
Federico Terzi
31b93ebdb0 feat(inject): add wait mechanism on wayland injector 2021-07-28 22:37:21 +02:00
Federico Terzi
6a558f74c7 feat(core): show welcome UI only once after the engine has successfuly initialized 2021-07-27 23:12:22 +02:00
Federico Terzi
c82690ba61 feat(modulo): improve troubleshooting UI 2021-07-27 22:41:29 +02:00
Federico Terzi
925a411460 feat(core): improve troubleshooter behavior on critical errors 2021-07-27 22:23:48 +02:00
Federico Terzi
6dc3f1093d feat(core): add start and stop aliases 2021-07-26 20:40:45 +02:00
Federico Terzi
7bdeff8bb7 feat(core: improve auto troubleshooting logic 2021-07-24 15:47:08 +02:00
Federico Terzi
adc13c707d feat(core): improve notification handling 2021-07-24 10:08:37 +02:00
Federico Terzi
e9d90929a6 feat(core): show already running warning if multiple instances are present 2021-07-23 22:15:18 +02:00
Federico Terzi
56502fd0e5 feat(modulo): implement already_running flag on welcome gui 2021-07-23 22:14:44 +02:00
Federico Terzi
2d559b7fb0 feat(core): implement run-count config in worker 2021-07-21 20:23:37 +02:00
Federico Terzi
acf4286cd4 fix(path): fix legacy runtime dir detection 2021-07-20 20:09:26 +02:00
Federico Terzi
0280738dd3 fix(modulo): fix compilation problem on macOS 2021-07-19 23:51:28 +02:00
Federico Terzi
f42c4ef56e feat(core): implement basic troubleshooting logic 2021-07-19 23:35:20 +02:00
Federico Terzi
dc6b11cfc8 feat(config): improve error messages 2021-07-19 20:50:14 +02:00
Federico Terzi
11edf080de feat(modulo): implement troubleshooting window 2021-07-19 20:48:55 +02:00
Federico Terzi
aec2425b0b feat(config): refactor config handler to also return warnings and errors 2021-07-18 12:10:56 +02:00
Federico Terzi
c0d08bf1bd fix(core): fix starting template that misbehaved on Windows 2021-07-17 16:38:05 +02:00
Federico Terzi
17e59c8edf feat(core): improve Windows installer 2021-07-17 15:59:08 +02:00
Federico Terzi
08efac57d7 feat(core): show initial notification when espanso starts 2021-07-17 15:58:45 +02:00
Federico Terzi
a8799c3d9a feat(ui): refactor windows notification system away from WinToast 2021-07-17 15:58:19 +02:00
Federico Terzi
f8b7300d31 feat(core): implement Windows installer packaging 2021-07-17 10:24:25 +02:00
Federico Terzi
fc70b4fc03 feat(core): factor out windows build steps to be reused 2021-07-16 22:50:13 +02:00
Federico Terzi
a4b9b30cdf feat(core): create packager for Windows portable mode and move scripts to separate files 2021-07-16 22:14:05 +02:00
Federico Terzi
fd2f385858 feat(core): implement linux service methods 2021-07-14 22:35:45 +02:00
Federico Terzi
40a307c38d fix(core): fix makefile configuration for newer versions of Rust 2021-07-14 20:49:59 +02:00
Federico Terzi
72d7c19e9f feat(core): add support for linux capabilities 2021-07-14 20:43:31 +02:00
Federico Terzi
c381da94f9 feat(core): add todo service cli stub 2021-07-14 19:35:49 +02:00
Federico Terzi
64886ff436 feat(core): implement service methods on Windows 2021-07-12 21:42:51 +02:00
Federico Terzi
59a405a21d feat(core): implement service cli handler on macOS 2021-07-11 22:26:40 +02:00
Federico Terzi
d9f275895b fix(detect): fix wrong handling of repeated key presses on Wayland 2021-07-10 11:19:24 +02:00
Federico Terzi
99d040e708 feat(core): add run-binary make command 2021-07-10 10:45:39 +02:00
Federico Terzi
8f44b6631c feat(core): enable modulo as a default feature 2021-07-10 10:28:32 +02:00
Federico Terzi
4dac91e10c style(core): remove outdated comments 2021-07-10 10:28:10 +02:00
Federico Terzi
190b718614 docs(core): introduce early draft of compilation docs 2021-07-10 10:22:56 +02:00
Federico Terzi
587b120f1c feat(core): add AppImage build script 2021-07-10 00:18:04 +02:00
Federico Terzi
5f98041413 feat(core): add basic AppImage resources 2021-07-09 21:23:30 +02:00
Federico Terzi
79f7a546d4 fix(modulo): fix compilation on Linux 2021-07-09 21:23:12 +02:00
Federico Terzi
1895488f6b feat(core): populate config directory with default content if not present 2021-07-07 22:05:39 +02:00
Federico Terzi
0c1a8dd080 feat(core): replace old accessibility images with up-to-date ones 2021-06-29 20:31:29 +02:00
Federico Terzi
b897ac4421 fix(mac-utils): fix app not going on the foreground 2021-06-29 20:31:08 +02:00
Federico Terzi
4ee1215fbc feat(core): improve icon dock handling on macOS 2021-06-29 20:04:19 +02:00
Federico Terzi
ddf8a35aeb feat(mac-utils): add methods to show/hide macOS dock icon 2021-06-29 20:03:45 +02:00
Federico Terzi
40ea11ef94 feat(modulo): improve welcome screen on macOS 2021-06-29 20:03:23 +02:00
Federico Terzi
612c8ba8e4 feat(core): add explanatory image on Welcome screen for macOS 2021-06-29 20:02:11 +02:00
Federico Terzi
5b547fec9f feat(core): launch daemon from launcher process 2021-06-27 20:20:05 +02:00
Federico Terzi
dd4d93bc94 fix(core): reduce timeout for broadcast message when adding espanso to PATH on Windows 2021-06-27 20:19:31 +02:00
Federico Terzi
7e09fe769b feat(modulo): return result from Wizard 2021-06-27 20:18:49 +02:00
Federico Terzi
ce3a1c456c feat(core): wire up Welcome screen 2021-06-27 18:04:21 +02:00
Federico Terzi
a6ca1ee4c2 feat(modulo): add welcome screen 2021-06-27 18:02:29 +02:00
Federico Terzi
cec11f39cb feat(core): extend env-path module to Windows 2021-06-27 15:46:09 +02:00
Federico Terzi
5abf2419e1 feat(path): include portable mode flag 2021-06-27 15:45:40 +02:00
Federico Terzi
887dfa2d5c fix(core): fix wrong symlink detection on macOS 2021-06-27 09:12:30 +02:00
Federico Terzi
4734eabb0d fix(core): fix wrong handler override on macOS bundle 2021-06-26 22:40:38 +02:00
Federico Terzi
a8522720a2 feat(core): implement macOS bundle launching logic 2021-06-26 22:31:20 +02:00
Federico Terzi
9f905fbd36 feat(mac-utils): implement accessibility methods 2021-06-26 21:32:55 +02:00
Federico Terzi
355196ea90 feat(core): wire up Wizard accessibility page 2021-06-26 21:32:21 +02:00
Federico Terzi
7f7b3cb358 feat(modulo): wire up Wizard accessibility page 2021-06-26 21:31:50 +02:00
Federico Terzi
1edb533c32 feat(core): wire up Add to PATH wizard page 2021-06-26 19:49:41 +02:00
Federico Terzi
49550f0e60 feat(modulo): wire up Add to PATH wizard page 2021-06-26 19:49:12 +02:00
Federico Terzi
c5ef5337bf feat(kvs): improve KVS trait signature 2021-06-26 19:48:54 +02:00
Federico Terzi
8f99c6ce16 feat(core): implement env-path cli module on macOS 2021-06-26 18:55:05 +02:00
Federico Terzi
8fb3826298 feat(core): implement runtime Preferences module 2021-06-25 18:57:17 +02:00
Federico Terzi
cfe44fd861 feat(kvs): implement basic persistent key-value store 2021-06-25 18:07:21 +02:00
Federico Terzi
49714b5b53 feat(wizard): progress in the Wizard planning 2021-06-25 17:10:02 +02:00
Federico Terzi
f948dff26f feat(modulo): improve Wizard label 2021-06-25 17:09:37 +02:00
Federico Terzi
e992fe4f0c feat(core): implement wizard migration handler 2021-06-15 23:29:04 +02:00
Federico Terzi
363a5cf2ef feat(modulo): wire up migration operation 2021-06-15 23:27:17 +02:00
Federico Terzi
b47279db63 feat(core): improve migration error logging 2021-06-15 23:15:43 +02:00
Federico Terzi
f984d22f0c feat(core): improve migration exit code handling 2021-06-15 22:30:53 +02:00
Federico Terzi
5a4f0dee31 fix(modulo): fix wrong wizard image size on macOS 2021-06-13 17:31:47 +02:00
Federico Terzi
e544e59580 feat(modulo): improve wizard welcome screen 2021-06-13 17:17:51 +02:00
Federico Terzi
2cba576084 feat(core): add logo no-background 2021-06-13 17:16:57 +02:00
Federico Terzi
5e4840fd34 feat(modulo): improve dwizard default button handling 2021-06-13 14:47:40 +02:00
Federico Terzi
ac3f6735f6 feat(modulo): improve wizard main buttons 2021-06-13 14:39:24 +02:00
Federico Terzi
2cfb1be487 feat(core): progress in the launcher implementation 2021-06-13 14:18:27 +02:00
Federico Terzi
da67a46e2b feat(modulo): progress in the Wizard implementation 2021-06-13 14:17:28 +02:00
Federico Terzi
9bfe515c87 feat(modulo): progress in the wizard GUI layout 2021-06-12 21:57:55 +02:00
Federico Terzi
85b656ebf8 feat(modulo): update build.rs to include Wizard on macOS 2021-06-12 21:32:44 +02:00
Federico Terzi
2a9c64d685 feat(modulo): add early work on Wizard GUI 2021-06-12 21:24:15 +02:00
Federico Terzi
5e3491e238 feat(core): add launcher cli module 2021-06-12 21:23:56 +02:00
Federico Terzi
05920fbebc feat(core): wire up backspace_limit option 2021-06-10 21:27:37 +02:00
Federico Terzi
ddbfa06881 feat(config): add backspace_limit option 2021-06-10 21:27:05 +02:00
Federico Terzi
581bd199bb feat(core): wire up word_separator option 2021-06-10 21:15:13 +02:00
Federico Terzi
3df0df6fc7 feat(config): add word_separator option 2021-06-10 21:14:12 +02:00
Federico Terzi
17ffe488fe fix(core): fix secure input watcher on non-macos systems 2021-06-09 22:05:47 +02:00
Federico Terzi
f266c3c23c fix(mac-utils): fix compilation on non-macOS systems 2021-06-09 22:05:28 +02:00
Federico Terzi
09095a1e8d feat(core): implement secure-input watcher 2021-06-09 21:49:27 +02:00
Federico Terzi
e7df57ef87 feat(mac-utils): add secure-input-related utils 2021-06-09 21:48:39 +02:00
Federico Terzi
645cd78573 feat(core): wire up parameters in clipboard injector 2021-06-08 20:48:33 +02:00
Federico Terzi
136b4791df feat(core): wire up delay options to injectors 2021-06-07 21:16:21 +02:00
Federico Terzi
9efc7cfa0a feat(config): add inject_delay and key_delay options 2021-06-07 21:15:39 +02:00
Federico Terzi
0829816bac fix(inject): make injection options clonable 2021-06-07 21:10:42 +02:00
Federico Terzi
efb2888e17 chore(ui): add missing licence header 2021-06-06 21:52:54 +02:00
Federico Terzi
e1e805ce65 feat(core): wire up force_clipboard/force_mode params 2021-06-06 21:51:34 +02:00
Federico Terzi
030763cab5 feat(config): add force_mode parameter 2021-06-06 21:51:14 +02:00
Federico Terzi
394aed1b1d feat(core): change tray icons on Windows 2021-06-06 21:24:34 +02:00
Federico Terzi
a4c02a906b fix(config): increase shortcut event delay to mitigate issue on macOS 2021-06-05 22:45:47 +02:00
Federico Terzi
5b9060b6b9 feat(core): replace icons on macOS 2021-06-05 22:30:56 +02:00
Federico Terzi
5f8d30498f fix(core): remove previous bundle when compiling to avoid code signing errors 2021-06-05 22:26:08 +02:00
Federico Terzi
4ddd061a0f feat(core): first steps in creating a macOS bundle 2021-06-05 19:10:31 +02:00
Federico Terzi
7e322f93dd chore(core): change version to 2.0.0 2021-06-05 15:34:16 +02:00
Federico Terzi
9f1cf0c90c feat(core): add support for legacy dummy extension type 2021-06-05 15:32:22 +02:00
Federico Terzi
ccd00cb706 feat(render): add alias parameter to echo extension to support legacy dummy type 2021-06-05 15:31:55 +02:00
Federico Terzi
149d9ec8a0 feat(core): wire up some parameters in the clipboard injector 2021-06-05 12:19:22 +02:00
Federico Terzi
f9ff881136 feat(config): add clipboard-related parameters 2021-06-05 12:18:52 +02:00
Federico Terzi
0d226619a9 feat(core): implement preserve_clipboard 2021-06-05 11:37:16 +02:00
Federico Terzi
dadc71728c feat(config): add preserve clipboard config fields 2021-06-05 11:36:55 +02:00
Federico Terzi
28d8194b4a feat(core): implement daemon process monitor in worker 2021-06-03 21:47:24 +02:00
Federico Terzi
34f08964d7 fix(core): fix wrong loop in file watcher 2021-06-03 21:43:56 +02:00
Federico Terzi
c79d248812 feat(core): wire up auto_restart option 2021-06-03 21:01:04 +02:00
Federico Terzi
da19664f5b feat(config): add auto_restart option 2021-06-03 21:00:38 +02:00
Federico Terzi
8fb95ccf22 feat(ipc): implement new IPC semantics on Unix 2021-06-02 22:07:19 +02:00
Federico Terzi
833e39c328 feat(core): implement config auto-reload 2021-06-02 21:52:08 +02:00
Federico Terzi
33eeee99a9 feat(ipc): refactor IPC on Windows 2021-06-02 21:12:56 +02:00
Federico Terzi
87581c5b35 fix(core): release legacy lock file after check 2021-06-02 12:49:59 +02:00
Federico Terzi
00b4f97b14 feat(core): change IPC channel names to avoid conflicts with legacy version 2021-06-02 12:34:46 +02:00
Federico Terzi
5b9743bf68 feat(core): prevent migration from happening if a legacy version is running 2021-06-02 12:34:19 +02:00
Federico Terzi
4dd3df2326 feat(core): prevent espanso from starting if legacy instance is running 2021-06-02 12:28:35 +02:00
Federico Terzi
2cf6cafdb6 feat(core): introduce migrate cli command 2021-06-02 11:54:00 +02:00
Federico Terzi
f98ab3dffd fix(path): change wrong package resolution order 2021-06-02 11:48:58 +02:00
Federico Terzi
2489f6c404 feat(migrate): add warning if passive_mode directives are detected 2021-05-29 18:45:34 +02:00
Federico Terzi
d724c5468a feat(migrate): add warning if a nested parent directive is detected 2021-05-29 18:40:30 +02:00
Federico Terzi
7d115d588b feat(migrate): improve multiline string handling 2021-05-29 16:28:40 +02:00
Federico Terzi
21c988c2b4 feat(migrate): add test cases and rendering implementation 2021-05-29 13:08:32 +02:00
Federico Terzi
4388fc0ba6 feat(migrate): add package handling 2021-05-29 10:28:09 +02:00
Federico Terzi
ec73edba24 feat(migrate): progress in the migration implementation 2021-05-29 10:14:42 +02:00
Federico Terzi
b5786b6530 fix(mitigate): fix non-deterministic test 2021-05-27 23:16:53 +02:00
Federico Terzi
6ee301c6e1 feat(migrate): setting up automatic test pipeline and progress in the implementation 2021-05-27 22:06:39 +02:00
Federico Terzi
ae2da9b0fe feat(misc): add cargo Makefile 2021-05-27 22:05:45 +02:00
Federico Terzi
e3cdaa91fb feat(migrate): progress in the migration process 2021-05-25 22:07:26 +02:00
Federico Terzi
58e048900c feat(migrate): update cargo lock 2021-05-24 20:58:40 +02:00
Federico Terzi
9c6e37bc44 feat(migrate): progress in the migrate implementation 2021-05-24 20:58:18 +02:00
Federico Terzi
04c998274e feat(migrate): create base test-case 2021-05-23 22:08:30 +02:00
Federico Terzi
85d2660768 fix(ui): fix test runs on Windows when modulo is disabled 2021-05-23 18:11:07 +02:00
Federico Terzi
023ac615ba style(core): refactor worker module structure 2021-05-23 17:36:06 +02:00
Federico Terzi
2ad8dfa8a5 feat(core): add Enable/Disable option in context menu 2021-05-23 16:07:17 +02:00
Federico Terzi
5abea19016 feat(core): add support for Disable/Enable 2021-05-23 15:47:15 +02:00
Federico Terzi
d193cb749b feat(config): add toggle_key option 2021-05-23 15:45:58 +02:00
Federico Terzi
76b4a4a302 feat(core): move form icon injection logic 2021-05-22 16:49:08 +02:00
Federico Terzi
985706c6fe fix(core): remove unused code 2021-05-22 12:21:20 +02:00
Federico Terzi
2b5e77e1da feat(core): integrate new built-in modulo with existing interface 2021-05-22 12:20:49 +02:00
Federico Terzi
a7c51fe803 feat(core): overwrite modulo icon 2021-05-22 12:05:28 +02:00
Federico Terzi
0e5e308e9f feat(modulo): port modulo build on Linux 2021-05-22 11:55:21 +02:00
Federico Terzi
ed60ae46cf feat(modulo): implement building process on macOS 2021-05-22 11:45:17 +02:00
Federico Terzi
074eae309c feat(core): add support for modulo in espanso core 2021-05-21 22:03:54 +02:00
Federico Terzi
23895841e3 feat(modulo): initial port of modulo within the espanso repo 2021-05-21 22:03:15 +02:00
Federico Terzi
04720212e5 feat(ui): add feature to avoid double linking gdiplus 2021-05-21 22:00:02 +02:00
Federico Terzi
ccb3e11d93 feat(clipboard): add feature to avoid double linking gdiplus 2021-05-21 21:59:29 +02:00
Federico Terzi
c996bbb0f5 fix(ui): implement exit method on Linux 2021-05-20 20:18:54 +02:00
Federico Terzi
fc69b7a79f fix(info): fix segmentation fault on macOS 2021-05-20 19:16:17 +02:00
Federico Terzi
8c2e02030a feat(ui): implement exit method on macOS 2021-05-18 21:50:14 +02:00
Federico Terzi
4ab040da3c feat(core): add restart option in context menu and improve exit handling 2021-05-18 21:23:14 +02:00
Federico Terzi
39758e2b9a feat(core): improve exit handling 2021-05-18 21:04:23 +02:00
Federico Terzi
cba94607fa feat(core): first half of context menu handling 2021-05-17 22:09:29 +02:00
Federico Terzi
716c50cee1 feat(ui): refactor context menu structure 2021-05-17 22:06:37 +02:00
Federico Terzi
798cbfee45 feat(core): improve exit code handling and investigate improvement of shell handling on Windows 2021-05-17 21:02:21 +02:00
Federico Terzi
6eb3fdfcf3 fix(core): fix bug that caused multiple processes to overwrite logs 2021-05-16 21:39:24 +02:00
Federico Terzi
befcbd984e feat(core): add check to kill orphan worker processes when daemon is launched 2021-05-16 18:59:50 +02:00
Federico Terzi
db5b7c1c38 feat(core): improve exit handling and connect first IPC worker POC 2021-05-16 18:42:56 +02:00
Federico Terzi
91046b3c18 feat(ui): add method to exit eventloop 2021-05-16 18:42:22 +02:00
Federico Terzi
22ba3a5e03 feat(core): prevent multiple daemon and worker instances 2021-05-16 16:55:40 +02:00
Federico Terzi
e08bf2f69a feat(core): add logging for panics 2021-05-16 16:26:03 +02:00
Federico Terzi
8b73a41439 feat(core): add code to launch worker from daemon 2021-05-16 16:20:15 +02:00
Federico Terzi
0831d19841 feat(core): improve logging handling and create daemon skeleton 2021-05-16 15:45:05 +02:00
Federico Terzi
6cf5b51487 feat(path): add option to accept custom package dir 2021-05-16 14:37:12 +02:00
Federico Terzi
e049ba51fb fix(core): improve handling of conflicting pressed modifiers 2021-05-11 22:11:02 +02:00
Federico Terzi
2eef404d6c fix(core): fix bug that skipped propagate_case check 2021-05-11 21:14:53 +02:00
Federico Terzi
46b9382fef feat(core): improve clipboard_injector parameter handling 2021-05-08 14:57:45 +02:00
Federico Terzi
139fa7e511 feat(config): add pre_paste_delay configuration option 2021-05-08 14:57:18 +02:00
Federico Terzi
a40125715f feat(core): improve clipboard backend handling on macOS 2021-05-08 14:03:14 +02:00
Federico Terzi
4068f5ac66 fix(inject): export options fields and change macos default delay 2021-05-08 14:02:30 +02:00
Federico Terzi
ca6d04cd0e feat(core): add macOS icon extraction 2021-05-08 12:33:31 +02:00
Federico Terzi
5a66594532 feat(core): wire up match selection GUI 2021-05-08 10:56:05 +02:00
Federico Terzi
e361bdb9c2 feat(config): add methods to generate match descriptions 2021-05-08 10:55:44 +02:00
Federico Terzi
6d5507cc5f refactor(core): refactor modulo form invocation structure 2021-05-07 22:11:11 +02:00
Federico Terzi
248b62a6a2 feat(core): wire up regex matcher 2021-05-06 21:22:03 +02:00
Federico Terzi
745c329580 feat(config): add regex option 2021-05-06 21:21:43 +02:00
Federico Terzi
bfe6b13ae7 feat(match): limit regex buffer size 2021-05-06 21:20:46 +02:00
Federico Terzi
10d37d1fe6 fix(core): fix bug that prevented a matcher state invalidation on mouse clicks 2021-05-05 22:02:12 +02:00
Federico Terzi
eeec734d32 fix(config): remove useless warning 2021-05-05 21:30:52 +02:00
Federico Terzi
e3eecf0555 feat(core): implement active backend detection 2021-05-05 21:19:49 +02:00
Federico Terzi
fddb2711e4 feat(config): introduce clipboard threshold option 2021-05-05 21:19:32 +02:00
Federico Terzi
47587681bd feat(core): introduce command line argument and env variables to customize config path. #267 2021-05-05 21:07:08 +02:00
Federico Terzi
f252c6a119 feat(path): include argument to force config and runtime directories 2021-05-05 21:04:53 +02:00
Federico Terzi
ddab54616d feat(core): implement icon extraction on Linux 2021-05-04 21:05:26 +02:00
Federico Terzi
ddd62b225f feat(core): implement image matches 2021-05-01 19:41:22 +02:00
Federico Terzi
7a8e39fdad feat(config): introduce image matches options 2021-05-01 19:41:04 +02:00
Federico Terzi
7b9e01c1db style(ui): remove useless TODO comment 2021-04-30 17:07:37 +02:00
Federico Terzi
905f67e668 style(config): remove useless TODO comment 2021-04-30 17:07:20 +02:00
Federico Terzi
42ff9b5928 style(clipboard): remove lib TODO comments 2021-04-30 17:06:53 +02:00
Federico Terzi
f07ff326b5 style(core): expand some TODO comments 2021-04-30 17:06:26 +02:00
Federico Terzi
2f53752e97 feat(core): implement rich text matches 2021-04-29 22:37:44 +02:00
Federico Terzi
038798a2d6 feat(config): parse rich text matches 2021-04-29 22:35:05 +02:00
Federico Terzi
302a45aa88 feat(core): add source_id parameter to events and introduce past events discarding mechanism 2021-04-25 21:03:13 +02:00
Federico Terzi
7db39f4c74 feat(core): implement delay-for-modifier-release mechanism 2021-04-25 15:54:08 +02:00
Federico Terzi
3dfde8e830 feat(config): wire up form match syntax 2021-04-24 18:00:46 +02:00
Federico Terzi
a2522af57a feat(render): create FormExtension 2021-04-24 17:59:50 +02:00
Federico Terzi
d7ebd2a4dd feat(core): wire up modulo forms 2021-04-24 17:58:58 +02:00
Federico Terzi
de236a89d2 feat(core): implement threading structure in worker thread and add resources 2021-04-23 22:00:14 +02:00
Federico Terzi
83a58c9912 feat(config): make config store trait Send 2021-04-23 21:52:02 +02:00
Federico Terzi
03d2aac56e feat(core): wire up mouse event 2021-04-23 20:41:41 +02:00
Federico Terzi
aaa55ca063 refactor(core): change event package names 2021-04-23 19:55:11 +02:00
Federico Terzi
871da0aaa1 refactor(core): change event package names 2021-04-23 19:53:38 +02:00
Federico Terzi
a5fff067f7 feat(core): wire up extensions 2021-04-21 20:49:46 +02:00
Federico Terzi
4af0cde3ed refactor(render): transform boxed extensions to references 2021-04-21 20:49:25 +02:00
Federico Terzi
1e65d2ead6 Merge branch 'dev-1.x-core' of github.com:federico-terzi/espanso into dev-1.x-core 2021-04-21 21:35:39 +02:00
Federico Terzi
f35eecae88 fix(render): fix failing test on Windows 2021-04-21 19:26:55 +02:00
Federico Terzi
05c26f9eb0 fix(clipboard): add missing linking configuration for Windows 2021-04-21 19:25:09 +02:00
Federico Terzi
cedb073f06 feat(core): implement propagate_case option 2021-04-19 22:04:38 +02:00
Federico Terzi
5a729f5810 feat(config): introduce the uppercase_style option for matches (related to #625) 2021-04-19 22:03:29 +02:00
Federico Terzi
1fad2039d6 refactor(match): remove preserve_case_markers option as it's not needed 2021-04-19 22:02:23 +02:00
Federico Terzi
82096ad9fa fix(core): improve continuous word matches detection 2021-04-18 12:33:53 +02:00
Federico Terzi
04f1449046 feat(core): improve support for word matches 2021-04-18 12:22:08 +02:00
Federico Terzi
97b789e946 feat(match): provide word separators in the output of Rolling Match result 2021-04-18 12:21:26 +02:00
Federico Terzi
dd2cc9de17 fix(match): fix bug that prevented the matcher from detecting word matches at start 2021-04-17 20:51:47 +02:00
Federico Terzi
5c8333e307 fix(core): fix wrong handling of multiline string in event injector 2021-04-17 20:23:07 +02:00
Federico Terzi
976e653fc8 refactor(core): restructure event pipeline to account for match cause compensation 2021-04-17 14:59:42 +02:00
Federico Terzi
799474a0fc fix(core): change trace log format 2021-04-17 13:31:45 +02:00
Federico Terzi
3a51efda2c feat(core): introduce basic injection backend switch logic 2021-04-17 13:27:08 +02:00
Federico Terzi
e532a377b1 feat(core): add names to middlewares 2021-04-16 21:01:58 +02:00
Federico Terzi
da6b68b09e feat(core): add cursor hint field to rendered event 2021-04-16 21:01:14 +02:00
Federico Terzi
d7e841bf03 feat(core): add cursor hint handling in RenderMiddleware 2021-04-16 21:00:27 +02:00
Federico Terzi
9b1c57d217 fix(config): improve id deduplication process in legacy loader 2021-04-16 20:59:06 +02:00
Federico Terzi
f4f841f58a fix(core): fix misbehaving backspace handling 2021-04-11 19:34:55 +02:00
Federico Terzi
518f0f8376 feat(core): wire up injectors 2021-04-11 19:19:52 +02:00
Federico Terzi
4af4a434a3 feat(core): progress on the core pipeline 2021-04-10 12:05:32 +02:00
Federico Terzi
14fdfe4149 feat(config): add method to list all configs 2021-04-10 12:04:53 +02:00
Federico Terzi
3ecc4e8546 feat(render): expose public struct fields 2021-04-10 12:04:18 +02:00
Federico Terzi
f847d0cd81 feat(config): add id to config and decouple variable params from serde_yaml 2021-04-09 21:29:23 +02:00
Federico Terzi
e643609d57 feat(core): progress in the core implementation 2021-04-04 22:05:03 +02:00
Federico Terzi
459e414a09 feat(config): refactor id field and introduce deduplication on legacy loader 2021-04-04 22:01:59 +02:00
Federico Terzi
d1776c9fea feat(config): convert id generator to local storage 2021-04-04 22:00:26 +02:00
Federico Terzi
3a0c397410 feat(match): expose struct option fields 2021-04-04 21:59:55 +02:00
Federico Terzi
c0de39fdd0 feat(core): improve basic core structure 2021-04-03 13:55:21 +02:00
Federico Terzi
a4958ff352 feat(core): early structure of the engine 2021-04-01 22:13:56 +02:00
Federico Terzi
5c912777c7 fix(core): use local time instead of UTC in logs. #522 2021-03-29 21:20:33 +02:00
Federico Terzi
38fb74cdac feat(core): implement log command 2021-03-29 21:10:30 +02:00
Federico Terzi
aa6853293a fix(detect): fix compilation warnings 2021-03-28 18:36:56 +02:00
Federico Terzi
d957fd35ce fix(clipboard): fix compilation warnings 2021-03-28 18:36:36 +02:00
Federico Terzi
1dad647310 fix(info): fix compilation warnings 2021-03-28 18:36:12 +02:00
Federico Terzi
e29dbfc9d5 fix(inject): fix compilation warnings 2021-03-28 18:35:50 +02:00
Federico Terzi
1f0fe74ac1 feat(core): implement basic cli handler structure and path handler 2021-03-28 18:22:50 +02:00
Federico Terzi
b375518730 feat(core): wire up path resolution package 2021-03-27 14:35:05 +01:00
Federico Terzi
c4ba7411c1 feat(config): improve legacy loader signature 2021-03-27 14:34:49 +01:00
Federico Terzi
279752ba59 feat(path): implement path resolution logic 2021-03-27 14:34:10 +01:00
Federico Terzi
65fd76c5b9 feat(config): add backend option 2021-03-24 20:51:15 +01:00
Federico Terzi
8257e9f400 Merge branch 'dev-1.x' of github.com:federico-terzi/espanso into dev-1.x 2021-03-23 22:09:00 +01:00
Federico Terzi
14196be5c5 fix(config): improve signature of config loader 2021-03-23 22:03:25 +01:00
Federico Terzi
9098d5ac2a feat(config): implement legacy loader 2021-03-23 21:57:03 +01:00
Federico Terzi
1e015eb891 feat(config): port legacy loader to new project 2021-03-21 15:28:07 +01:00
Federico Terzi
bfb38c19c7 feat(info): implement app info manager on macOS 2021-03-21 10:57:08 +01:00
Federico Terzi
15d78dc13f feat(info): implement app info provider on Windows 2021-03-20 22:04:47 +01:00
Federico Terzi
9ce453e58b feat(info): implement app info provider on linux 2021-03-20 21:47:07 +01:00
Federico Terzi
3217865d4b feat(render): implement random extension 2021-03-20 19:16:53 +01:00
Federico Terzi
0ca9f7689b feat(render): implement script extension 2021-03-20 17:13:50 +01:00
Federico Terzi
09ff2178e2 feat(render): implement shell extension 2021-03-20 15:06:52 +01:00
Federico Terzi
ec78fb9ff2 feat(render): implement clipboard extension 2021-03-20 10:33:38 +01:00
Federico Terzi
de5ced12a3 fix(clipboard): fix linking error on test builds 2021-03-20 10:16:50 +01:00
Federico Terzi
ea1479366e feat(render): add echo extension 2021-03-19 21:43:22 +01:00
Federico Terzi
b2422bac55 feat(render): introduce casing style to capitalize each word first letter. #625 2021-03-19 21:26:08 +01:00
Federico Terzi
d11c924965 feat(render): implement date extension 2021-03-19 21:09:56 +01:00
Federico Terzi
7806a079dc feat(render): implement default renderer 2021-03-19 21:09:43 +01:00
Federico Terzi
8df15bba3f feat(clipboard): add macOS clipboard implementation 2021-03-17 11:36:02 +01:00
Federico Terzi
d9496899ed feat(clipboard): add windows clipboard implementation 2021-03-17 10:27:00 +01:00
Federico Terzi
f7e58f9711 fix(inject): remove unnecessary print 2021-03-17 09:14:30 +01:00
Federico Terzi
c8a3ff1d8f fix(clipboard): fix compilation error on X11 2021-03-16 20:53:55 +01:00
Federico Terzi
4038dd0cf3 feat(clipboard): implement wayland clipboard manager 2021-03-16 19:56:55 +01:00
Federico Terzi
aa64f11950 feat(clipboard): implement clipboard on X11 systems 2021-03-16 16:09:59 +01:00
Federico Terzi
32b1de8ddc feat(clipboard): define base library interface 2021-03-16 12:30:18 +01:00
Federico Terzi
353f3f10de fix: broken tests on Windows and macOS 2021-03-15 21:05:21 +01:00
Federico Terzi
2647f099ad style: formatting 2021-03-15 19:08:08 +01:00
Federico Terzi
307599b761 Finish implementation of hotkey detection on Wayland 2021-03-15 19:05:49 +01:00
Federico Terzi
b18cf1c153 First half of hotkeys detection on Wayland 2021-03-15 12:26:22 +01:00
Federico Terzi
fbeca8b6e9 Implement hotkeys handling on X11 2021-03-14 21:53:17 +01:00
Federico Terzi
474eae69d5 Add hotkey detection implementation to Windows 2021-03-14 18:24:37 +01:00
Federico Terzi
89805a0248 First draft of hotkey support on macOS 2021-03-14 15:50:54 +01:00
Federico Terzi
0ae9b60175 Fix absolute path error on macOS tests 2021-03-14 10:30:53 +01:00
Federico Terzi
2a2fbbd792 Format code 2021-03-13 13:45:52 +01:00
Federico Terzi
beab299aa0 Improve Rolling matcher implementation and add Regex matcher 2021-03-13 13:45:37 +01:00
Federico Terzi
fcfca92bc5 Early draft of RegexMatcher 2021-03-11 22:03:28 +01:00
Federico Terzi
b2f28bb739 Finalize first draft of RollingMatcher 2021-03-11 21:16:18 +01:00
Federico Terzi
caf72c9aef Fix absolute path error on windows 2021-03-11 17:41:34 +01:00
Federico Terzi
784e074795 Improve the RollingMatcher implementation 2021-03-10 21:12:23 +01:00
Federico Terzi
1103278ccd Early draft of RollingMatcher 2021-03-09 21:43:07 +01:00
Federico Terzi
e8881d0faf Add license header and formatting 2021-03-09 16:06:50 +01:00
Federico Terzi
4143caff3d Improve config loading process and tests 2021-03-08 21:46:27 +01:00
Federico Terzi
7262727823 Add config store base implementation and tests 2021-03-08 16:36:16 +01:00
Federico Terzi
0ca740914f Improve the config parsing logic and test cases 2021-03-07 15:53:02 +01:00
Federico Terzi
7b9e43ab06 Improve testing coverage of config module 2021-03-06 15:26:34 +01:00
Federico Terzi
3974d90bc9 Refactor config structure and improve importer logic 2021-03-05 21:31:54 +01:00
Federico Terzi
2cb8da91a5 Continue the work on the new config module 2021-03-04 22:02:44 +01:00
Federico Terzi
2283cedbd3 Making progress in the config parsing 2021-02-26 22:33:33 +01:00
Federico Terzi
e26a04de67 Initial draft of config parsing 2021-02-24 21:57:23 +01:00
Federico Terzi
79a1b85769 Refactor linux methods 2021-02-18 19:51:45 +01:00
Federico Terzi
75ca7ec071 Refactor macOS implementation 2021-02-17 20:40:27 +01:00
Federico Terzi
a57092517e Add windows IPC implementation and general refactor 2021-02-17 19:09:28 +01:00
Federico Terzi
ee611c3a03 First unix ipc implementation 2021-02-15 21:25:38 +01:00
Federico Terzi
cfadebc733 Improve wayland settings 2021-02-15 15:34:24 +01:00
Federico Terzi
3737eed034 Introduce wayland feature 2021-02-14 22:01:42 +01:00
Federico Terzi
a9d24d400d First draft of evdev inject backend 2021-02-14 21:02:50 +01:00
Federico Terzi
ff6bfa20cb First draft of espanso inject x11 implementation 2021-02-13 15:55:28 +01:00
Federico Terzi
e0bf94013d First implementation of espanso-inject on macOS 2021-02-12 16:58:05 +01:00
Federico Terzi
e3e1ad720f Format code 2021-02-10 21:33:14 +01:00
Federico Terzi
c2f497ef59 Initial implementation of espanso-inject on Windows 2021-02-10 21:31:46 +01:00
Federico Terzi
afb64df17c Initial macOS detect implementation 2021-02-09 17:12:16 +01:00
Federico Terzi
18515319a8 Format code 2021-02-08 21:23:28 +01:00
Federico Terzi
7d357149ff First draft of macOS ui layer 2021-02-08 21:13:33 +01:00
Federico Terzi
bee2001e28 Improve detect error handling 2021-02-08 17:16:35 +01:00
Federico Terzi
2f04f174b7 Improve evdev detect implementation 2021-02-08 16:55:20 +01:00
Federico Terzi
1d6b152c15 Initial draft of wayland event source 2021-02-04 22:12:30 +01:00
Federico Terzi
1a21a81ace Add implementation of linux ui eventloop 2021-01-31 21:29:18 +01:00
Federico Terzi
a450ee18fa First implementation of x11 source 2021-01-31 18:09:03 +01:00
Federico Terzi
e77e013ae7 Format code 2021-01-31 14:27:45 +01:00
Federico Terzi
b04ca73641 Wireup context menu item clicks 2021-01-31 14:24:33 +01:00
Federico Terzi
65f9811db3 First draft of context menu handling on Windows 2021-01-31 13:00:27 +01:00
Federico Terzi
567c35eb0e Add native windows notifications to espanso-ui package 2021-01-30 21:20:09 +01:00
Federico Terzi
4c213db5fb Fix clippy warnings 2021-01-30 18:52:42 +01:00
Federico Terzi
686ceb88da First draft of Windows ui-layer 2021-01-30 18:41:47 +01:00
Federico Terzi
b5e2d42ec4 Add tests in espanso-detect win32 2021-01-29 22:24:24 +01:00
Federico Terzi
b16d1a04ae Fresh start 2021-01-29 21:55:47 +01:00
Federico Terzi
386a351df7
Merge pull request #535 from federico-terzi/dev
Version 0.7.3
2020-12-03 20:47:59 +01:00
Federico Terzi
39040bbaef Update serde_yaml dependency. Fix #533 2020-12-03 20:14:23 +01:00
Federico Terzi
7a688ab291 Fix broken test on Linux 2020-12-03 20:02:45 +01:00
Federico Terzi
727d738615 Update serde version. Fix #533 2020-12-03 19:43:12 +01:00
Federico Terzi
6a89b9a489 Update readme 2020-12-03 19:37:22 +01:00
Federico Terzi
0ead01d5d6 Format source code 2020-11-14 21:59:14 +01:00
Federico Terzi
cc72f10398 Refactor extensions to allow them to stop the expansion process. Fix #475 2020-11-14 21:58:54 +01:00
Federico Terzi
c5c2a4ab90 Improve buffer handling when interfacing with native layer. Fix #431 2020-11-14 21:27:47 +01:00
Federico Terzi
c4409241b6 Disable argument rendering for matches unless args are present. Fix #465 2020-11-14 21:19:59 +01:00
Federico Terzi
4c799f736f Add support for escaped brakets in form fields. Fix #503 2020-11-14 21:11:46 +01:00
Federico Terzi
1431145afa Add option to disable passive argument injection in shell extension. Fix #513 2020-11-14 20:52:02 +01:00
Federico Terzi
be0dd8a2cb Add option to wait for modifier release before injection. Fix #470 2020-10-23 21:54:27 +02:00
Federico Terzi
63c28081a6 Fix bugs related to macOS status icon. #480 #474 2020-10-23 21:40:05 +02:00
Federico Terzi
dcdae3ad2d Format code 2020-10-23 21:28:37 +02:00
Federico Terzi
37bd79039f Add macOS support for rich text 2020-10-23 21:28:05 +02:00
Federico Terzi
c43774a59f Add linux implementation for Rich text 2020-10-12 20:49:07 +02:00
Federico Terzi
185d06e8e9 Implement Rich text on Windows 2020-10-08 23:17:01 +02:00
Federico Terzi
50f1fe9f19 Version bump 0.7.3 2020-10-08 21:19:28 +02:00
Federico Terzi
92bc451c91
Merge pull request #476 from mattn/mingw
Fix build on mingw64 gcc compiler
2020-10-01 20:34:10 +02:00
Yasuhiro Matsumoto
48c7535d28
Fix build on mingw64 gcc compiler 2020-10-01 23:28:12 +09:00
Federico Terzi
901058a62e
Fix incorrect modulo app stub template 2020-09-24 17:54:41 +02:00
Federico Terzi
d2a119eb29 Fix incorrect modulo app stub template 2020-09-24 17:53:15 +02:00
678 changed files with 95975 additions and 18608 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target/*

1
.github/FUNDING.yml vendored
View File

@ -1,4 +1,3 @@
# These are supported funding model platforms # These are supported funding model platforms
github: ['federico-terzi']
custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FHNLR5DRS267E&source=url'] custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FHNLR5DRS267E&source=url']

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: Bug report
about: Let us know about a bug in Espanso
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is
**To Reproduce**
Steps to reproduce the behavior:
1.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots or screen recordings to help explain your problem.
**Logs**
If possible, run `espanso log` in a terminal after the bug has occurred, then post the output here so that we can better diagnose the problem
**Setup information**
- OS: What OS are you using?
- Version: which version of Espanso are you running? (you can find out by running `espanso --version` inside a terminal)

View File

@ -0,0 +1,12 @@
---
name: Feature request
about: Suggest an idea to make Espanso better
title: ''
labels: ''
assignees: ''
---
Feature requests have been moved to discussions, please open it there :)
https://github.com/espanso/espanso/discussions/categories/feature-requests-and-ideas

View File

@ -3,12 +3,13 @@ FROM ubuntu:18.04
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y libssl-dev \ && apt-get install -y libssl-dev \
libxdo-dev libxtst-dev libx11-dev \ libxdo-dev libxtst-dev libx11-dev \
wget git cmake build-essential pkg-config libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev \
wget git file build-essential pkg-config
ENV RUSTUP_HOME=/usr/local/rustup \ ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \ CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH \ PATH=/usr/local/cargo/bin:$PATH \
RUST_VERSION=1.41.0 RUST_VERSION=1.57.0
RUN set -eux; \ RUN set -eux; \
dpkgArch="$(dpkg --print-architecture)"; \ dpkgArch="$(dpkg --print-architecture)"; \
@ -30,9 +31,8 @@ RUN set -eux; \
cargo --version; \ cargo --version; \
rustc --version; rustc --version;
RUN mkdir espanso RUN mkdir espanso && cargo install rust-script --version "0.7.0" && cargo install --force cargo-make --version 0.34.0
COPY . espanso COPY . espanso
RUN cd espanso \ RUN cd espanso
&& cargo install cargo-deb

18
.github/scripts/ubuntu/build_appimage.sh vendored Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "Testing espanso..."
cd espanso
cargo make test-binary --profile release
echo "Building espanso and creating AppImage"
cargo make create-app-image --profile release
cd ..
cp espanso/target/linux/AppImage/out/Espanso-*.AppImage Espanso-X11.AppImage
sha256sum Espanso-X11.AppImage > Espanso-X11.AppImage.sha256.txt
ls -la
echo "Copying to mounted volume"
cp Espanso-X11* /shared

24
.github/scripts/ubuntu/build_deb.sh vendored Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -e
echo "Installing cargo-deb"
cargo install cargo-deb --version 1.34.0
cd espanso
echo "Building X11 deb package"
cargo deb -p espanso -- --no-default-features --features "modulo vendored-tls"
echo "Building Wayland deb package"
cargo deb -p espanso --variant wayland -- --no-default-features --features "modulo wayland vendored-tls"
cd ..
cp espanso/target/debian/espanso_*.deb espanso-debian-x11-amd64.deb
sha256sum espanso-debian-x11-amd64.deb > espanso-debian-x11-amd64-sha256.txt
cp espanso/target/debian/espanso-wayland*.deb espanso-debian-wayland-amd64.deb
sha256sum espanso-debian-wayland-amd64.deb > espanso-debian-wayland-amd64-sha256.txt
ls -la
echo "Copying to mounted volume"
cp espanso-debian-* /shared

159
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,159 @@
# Huge thanks to Alacritty, as their configuration served as a starting point for this one!
# See: https://github.com/alacritty/alacritty
name: CI
on:
push:
branches:
- master
- dev
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
WX_WIDGETS_BUILD_OUT_DIR: "${{github.workspace}}/wx-widgets-build"
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- name: Check formatting
run: |
rustup component add rustfmt
cargo fmt --all -- --check
- name: Install Linux dependencies
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install cargo-make --version 0.34.0
# wxWidgets builds (internal to espanso-modulo) are by far the largest bottleneck
# in the current pipeline, so we cache the results for a faster compilation process
- name: "Cache wxWidgets builds"
if: ${{ runner.os != 'Linux' }}
uses: actions/cache@v3
with:
path: |
${{env.WX_WIDGETS_BUILD_OUT_DIR}}
key: ${{ github.job }}-${{ runner.os }}-${{ hashFiles('espanso-modulo/build.rs') }}-${{ hashFiles('espanso-modulo/vendor/*') }}
- name: Build
run: |
cargo make build-binary
- name: Check clippy
run: |
rustup component add clippy
cargo clippy -- -D warnings
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
test:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- name: Check formatting
run: |
rustup component add rustfmt
cargo fmt --all -- --check
- name: Install Linux dependencies
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install cargo-make --version 0.34.0
- name: Run test suite
run: cargo make test-binary
build-wayland:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- name: Check formatting
run: |
rustup component add rustfmt
cargo fmt --all -- --check
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install cargo-make --version 0.34.0
- name: Build
run: cargo make build-binary --env NO_X11=true
- name: Check clippy
run: |
rustup component add clippy
cargo clippy -p espanso --features wayland -- -D warnings
test-wayland:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- name: Check formatting
run: |
rustup component add rustfmt
cargo fmt --all -- --check
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install cargo-make --version 0.34.0
- name: Run test suite
run: cargo make test-binary --env NO_X11=true
build-macos-arm:
runs-on: macos-11
env:
WX_WIDGETS_BUILD_OUT_DIR: "${{github.workspace}}/wx-widgets-build"
steps:
- uses: actions/checkout@v2
- name: Install target
run: rustup update && rustup target add aarch64-apple-darwin
- uses: Swatinem/rust-cache@v1
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install cargo-make --version 0.34.0
- name: "Cache wxWidgets builds"
if: ${{ runner.os != 'Linux' }}
uses: actions/cache@v3
with:
path: |
${{env.WX_WIDGETS_BUILD_OUT_DIR}}
key: ${{ github.job }}-${{ runner.os }}-${{ hashFiles('espanso-modulo/build.rs') }}-${{ hashFiles('espanso-modulo/vendor/*') }}
- name: Build
run: |
cargo make build-macos-arm-binary
# - name: Setup tmate session
# uses: mxschmitt/action-tmate@v3
# with:
# limit-access-to-actor: true

321
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,321 @@
# Huge thanks to Alacritty, as their configuration served as a starting point for this one!
# See: https://github.com/alacritty/alacritty
name: Release
on:
push:
branches:
- master
- dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_TERM_COLOR: always
jobs:
extract-version:
name: extract-version
runs-on: ubuntu-latest
outputs:
espanso_version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v2
- name: "Extract version"
id: "version"
run: |
ESPANSO_VERSION=$(grep '^version' espanso/Cargo.toml | awk -F '"' '{ print $2 }')
echo version: $ESPANSO_VERSION
echo "::set-output name=version::v$ESPANSO_VERSION"
create-release:
name: create-release
needs: ["extract-version"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create new release (only on master)
if: ${{ github.ref == 'refs/heads/master' }}
run: |
COMMIT_HASH=$(git rev-list --max-count=1 HEAD)
echo "Creating release: ${{ needs.extract-version.outputs.espanso_version }}"
echo "for hash: $COMMIT_HASH"
gh release create ${{ needs.extract-version.outputs.espanso_version }} \
-t ${{ needs.extract-version.outputs.espanso_version }} \
--notes "Automatically released by CI" \
--prerelease \
--target $COMMIT_HASH
windows:
needs: ["extract-version", "create-release"]
runs-on: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v2
- name: Print target version
run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0
- name: Test
run: cargo make test-binary --profile release
- name: Build resources
run: cargo make build-windows-resources --profile release
- name: Sign resources
run: cargo make sign-windows-resources
env:
CODESIGN_PWD: ${{ secrets.WIN_CODESIGN_PWD }}
CODESIGN_CROSS_SIGNED_B64: ${{ secrets.WIN_CODESIGN_INTERMEDIATE_B64 }}
CODESIGN_CERTIFICATE_B64: ${{ secrets.WIN_CODESIGN_CERTIFICATE_B64 }}
- name: Build installer
run: cargo make build-windows-installer --profile release --skip-tasks build-windows-resources
- name: Sign installer
run: cargo make sign-windows-installer
env:
CODESIGN_PWD: ${{ secrets.WIN_CODESIGN_PWD }}
CODESIGN_CROSS_SIGNED_B64: ${{ secrets.WIN_CODESIGN_INTERMEDIATE_B64 }}
CODESIGN_CERTIFICATE_B64: ${{ secrets.WIN_CODESIGN_CERTIFICATE_B64 }}
- name: Build portable mode archive
run: cargo make build-windows-portable --profile release --skip-tasks build-windows-resources
- name: Create portable mode archive
shell: powershell
run: |
Rename-Item target/windows/portable espanso-portable
Compress-Archive target/windows/espanso-portable target/windows/Espanso-Win-Portable-x86_64.zip
- name: Calculate hashes
shell: powershell
run: |
Get-FileHash target/windows/Espanso-Win-Portable-x86_64.zip -Algorithm SHA256 | select-object -ExpandProperty Hash > target/windows/Espanso-Win-Portable-x86_64.zip.sha256.txt
Get-FileHash target/windows/installer/Espanso-Win-Installer-x86_64.exe -Algorithm SHA256 | select-object -ExpandProperty Hash > target/windows/installer/Espanso-Win-Installer-x86_64.exe.sha256.txt
- uses: actions/upload-artifact@v2
name: "Upload artifacts"
with:
name: Windows Artifacts
path: |
target/windows/installer/Espanso-Win-Installer-x86_64.exe
target/windows/Espanso-Win-Portable-x86_64.zip
target/windows/installer/Espanso-Win-Installer-x86_64.exe.sha256.txt
target/windows/Espanso-Win-Portable-x86_64.zip.sha256.txt
- name: Upload artifacts to Github Releases (if master)
if: ${{ github.ref == 'refs/heads/master' }}
run: |
gh release upload ${{ needs.extract-version.outputs.espanso_version }} target/windows/installer/Espanso-Win-Installer-x86_64.exe target/windows/Espanso-Win-Portable-x86_64.zip target/windows/installer/Espanso-Win-Installer-x86_64.exe.sha256.txt target/windows/Espanso-Win-Portable-x86_64.zip.sha256.txt
linux-x11:
needs: ["extract-version", "create-release"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Print target version
run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Build docker image
run: |
sudo docker build -t espanso-ubuntu . -f .github/scripts/ubuntu/Dockerfile
- name: Build AppImage
run: |
sudo docker run --rm -v "$(pwd):/shared" espanso-ubuntu espanso/.github/scripts/ubuntu/build_appimage.sh
- uses: actions/upload-artifact@v2
name: "Upload artifacts"
with:
name: Linux X11 Artifacts
path: |
Espanso-X11.AppImage
Espanso-X11.AppImage.sha256.txt
- name: Upload artifacts to Github Releases (if master)
if: ${{ github.ref == 'refs/heads/master' }}
run: |
gh release upload ${{ needs.extract-version.outputs.espanso_version }} Espanso-X11.AppImage Espanso-X11.AppImage.sha256.txt
linux-deb:
needs: ["extract-version", "create-release"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Print target version
run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Build docker image
run: |
sudo docker build -t espanso-ubuntu . -f .github/scripts/ubuntu/Dockerfile
- name: Build Deb packages
run: |
sudo docker run --rm -v "$(pwd):/shared" espanso-ubuntu espanso/.github/scripts/ubuntu/build_deb.sh
- uses: actions/upload-artifact@v2
name: "Upload artifacts"
with:
name: Ubuntu-Debian Artifacts
path: |
espanso-debian-x11-amd64.deb
espanso-debian-wayland-amd64.deb
- name: Upload artifacts to Github Releases (if master)
if: ${{ github.ref == 'refs/heads/master' }}
run: |
gh release upload ${{ needs.extract-version.outputs.espanso_version }} espanso-debian-x11-amd64.deb espanso-debian-wayland-amd64.deb espanso-debian-x11-amd64-sha256.txt espanso-debian-wayland-amd64-sha256.txt
macos-intel:
needs: ["extract-version", "create-release"]
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- name: Print target version
run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0
- name: Test
run: cargo make test-binary --profile release
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Build
run: cargo make create-bundle --profile release
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Codesign executable
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: |
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" buildespanso.keychain
security default-keychain -s buildespanso.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" buildespanso.keychain
security import certificate.p12 -k buildespanso.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" buildespanso.keychain
/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime target/mac/Espanso.app -v
- name: "Notarize executable"
env:
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
echo "Create keychain profile"
xcrun notarytool store-credentials "espanso-notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"
echo "Creating temp notarization archive"
ditto -c -k --keepParent "target/mac/Espanso.app" "notarization.zip"
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "espanso-notarytool-profile" --wait
echo "Attach staple"
xcrun stapler staple "target/mac/Espanso.app"
- name: Create ZIP archive
run: |
ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-Intel.zip
- name: Calculate hashes
run: |
shasum -a 256 Espanso-Mac-Intel.zip > Espanso-Mac-Intel.zip.sha256.txt
- uses: actions/upload-artifact@v2
name: "Upload artifacts"
with:
name: Mac Intel Artifacts
path: |
Espanso-Mac-Intel.zip
Espanso-Mac-Intel.zip.sha256.txt
- name: Upload artifacts to Github Releases (if master)
if: ${{ github.ref == 'refs/heads/master' }}
run: |
gh release upload ${{ needs.extract-version.outputs.espanso_version }} Espanso-Mac-Intel.zip Espanso-Mac-Intel.zip.sha256.txt
macos-m1:
needs: ["extract-version", "create-release"]
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- name: Print target version
run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Install rust target
run: rustup update && rustup target add aarch64-apple-darwin
- name: Install rust-script and cargo-make
run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0
- name: Build
run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin
- name: Codesign executable
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: |
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" buildespanso.keychain
security default-keychain -s buildespanso.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" buildespanso.keychain
security import certificate.p12 -k buildespanso.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" buildespanso.keychain
/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime target/mac/Espanso.app -v
- name: "Notarize executable"
env:
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
echo "Create keychain profile"
xcrun notarytool store-credentials "espanso-notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"
echo "Creating temp notarization archive"
ditto -c -k --keepParent "target/mac/Espanso.app" "notarization.zip"
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "espanso-notarytool-profile" --wait
echo "Attach staple"
xcrun stapler staple "target/mac/Espanso.app"
- name: Create ZIP archive
run: |
ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-M1.zip
- name: Calculate hashes
run: |
shasum -a 256 Espanso-Mac-M1.zip > Espanso-Mac-M1.zip.sha256.txt
- uses: actions/upload-artifact@v2
name: "Upload artifacts"
with:
name: Mac M1 Artifacts
path: |
Espanso-Mac-M1.zip
Espanso-Mac-M1.zip.sha256.txt
- name: Upload artifacts to Github Releases (if master)
if: ${{ github.ref == 'refs/heads/master' }}
run: |
gh release upload ${{ needs.extract-version.outputs.espanso_version }} Espanso-Mac-M1.zip Espanso-Mac-M1.zip.sha256.txt
macos-publish-homebrew:
needs: ["extract-version", "create-release", "macos-m1", "macos-intel"]
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- name: Print target version
run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: "Setup SSH deploy key"
uses: webfactory/ssh-agent@fc49353b67b2b7c1e0e6a600572d01a69f2672dd
with:
ssh-private-key: ${{ secrets.HOMEBREW_CASK_SSH_PRIVATE_KEY }}
- name: Create and Publish Homebrew Cask
if: ${{ github.ref == 'refs/heads/master' }}
run: |
VERSION="${{ needs.extract-version.outputs.espanso_version }}" ./scripts/publish_homebrew_version.sh
echo "Cask formula has been published here: "

3682
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +1,21 @@
[package] [workspace]
name = "espanso"
version = "0.7.2"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust"
readme = "README.md"
homepage = "https://github.com/federico-terzi/espanso"
edition = "2018"
build="build.rs"
[modulo] members = [
version = "0.1.1" "espanso",
"espanso-detect",
[dependencies] "espanso-ui",
widestring = "0.4.0" "espanso-inject",
serde = { version = "1.0", features = ["derive"] } "espanso-ipc",
serde_yaml = "0.8" "espanso-config",
dirs = "2.0.2" "espanso-match",
clap = "2.33.0" "espanso-clipboard",
regex = "1.3.1" "espanso-render",
log = "0.4.8" "espanso-info",
simplelog = "0.7.1" "espanso-path",
fs2 = "0.4.3" "espanso-modulo",
serde_json = "1.0.40" "espanso-migrate",
log-panics = {version = "2.0.0", features = ["with-backtrace"]} "espanso-mac-utils",
backtrace = "0.3.37" "espanso-kvs",
chrono = "0.4.9" "espanso-engine",
lazy_static = "1.4.0" "espanso-package",
walkdir = "2.2.9" ]
reqwest = "0.9.20"
tempfile = "3.1.0"
dialoguer = "0.4.0"
rand = "0.7.2"
zip = "0.5.3"
notify = "4.0.13"
[target.'cfg(unix)'.dependencies]
libc = "0.2.62"
signal-hook = "0.1.15"
[target.'cfg(windows)'.dependencies]
named_pipe = "0.4.1"
winapi = { version = "0.3.9", features = ["wincon"] }
[build-dependencies]
cmake = "0.1.31"
[package.metadata.deb]
maintainer = "Federico Terzi <federicoterzi96@gmail.com>"
depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin"
section = "utility"
license-file = ["LICENSE", "1"]

67
Compilation.md Normal file
View File

@ -0,0 +1,67 @@
# Compilation
This document tries to explain the various steps needed to build espanso. (Work in progress).
# Prerequisites
These are the basic tools required to build espanso:
* A recent Rust compiler. You can install it following these instructions: https://www.rust-lang.org/tools/install
* A C/C++ compiler. There are multiple of them depending on the platform, but espanso officially supports the following:
* On Windows, you should use the MSVC compiler. The easiest way to install it is by downloading Visual Studio and checking "Desktop development with C++" in the installer: https://visualstudio.microsoft.com/
* On macOS, you should use the official build tools that come with Xcode. If you don't want to install Xcode, you should be able to download only the build tools by executing `xcode-select —install` and following the instructions.
* On Linux, you should use the default C/C++ compiler (it's usually GCC). On Ubuntu/Debian systems, you can install them with `sudo apt install build-essential`
* Espanso heavily relies on [cargo make](https://github.com/sagiegurari/cargo-make) for the various packaging
steps. You can install it by running:
```
cargo install --force cargo-make --version 0.34.0
```
# Linux
Espanso on Linux comes in two different flavors: one for X11 and one for Wayland.
If you don't know which one to choose, follow these steps to determine which one you are running: https://unix.stackexchange.com/a/325972
## Compiling for X11
### Necessary packages
If compiling on Ubuntu X11:
* `sudo apt install libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev`
### AppImage
The AppImage is a convenient format to distribute Linux applications, as besides the binary,
it also bundles all the required libraries.
You can create the AppImage by running (this will work on X11 systems):
```
cargo make create-app-image --profile release
```
You will find the resulting AppImage in the `target/linux/AppImage/out` folder.
### Binary
TODO
## Compiling on Wayland
TODO
## Advanced
Espanso offers a few flags that might be necessary if you want to further tune the resulting binary.
### Disabling modulo (GUI features)
Espanso includes a component known as _modulo_, which handles most of the graphical-related parts of the tool.
For example, the Search bar or Forms are handled by it.
If you don't want them, you can pass the `--env NO_MODULO=true` flag to any of the previous `cargo make` commands
to remove support for it.
Keep in mind that espanso was designed with modulo as a first class citizen, so the experience might be far from perfect without it.

View File

@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.> Espanso
Copyright (C) <year> <name of author> Copyright (C) 2019-2022 Federico Terzi
This program is free software: you can redistribute it and/or modify This program 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
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author> Espanso Copyright (C) 2019-2022 Federico Terzi
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

115
Makefile.toml Normal file
View File

@ -0,0 +1,115 @@
[config]
default_to_workspace = false
[env]
DEBUG = true
RELEASE = false
NO_X11 = false
NO_MODULO = false
EXEC_PATH = "target/debug/espanso"
BUILD_ARCH = "current"
[env.release]
DEBUG = false
RELEASE = true
EXEC_PATH = "target/release/espanso"
# Build variants
# This one was written in Rust instead of bash because it has to run on Windows as well
[tasks.build-binary]
script_runner = "@rust"
script = { file = "scripts/build_binary.rs" }
[tasks.run-binary]
command = "${EXEC_PATH}"
args = ["${@}"]
dependencies = ["build-binary"]
[tasks.test-binary]
script_runner = "@rust"
script = { file = "scripts/test_binary.rs" }
# Windows
[tasks.build-windows-resources]
script_runner = "@rust"
script = { file = "scripts/build_windows_resources.rs" }
dependencies = ["build-binary"]
[tasks.build-windows-portable]
script_runner = "@rust"
script = { file = "scripts/build_windows_portable.rs" }
dependencies = ["build-windows-resources"]
[tasks.build-windows-installer]
script_runner = "@rust"
script = { file = "scripts/build_windows_installer.rs" }
dependencies = ["build-windows-resources"]
[tasks.build-windows-all]
dependencies = ["build-windows-portable", "build-windows-installer"]
[tasks.sign-windows-resources]
env = { "TARGET_SIGNTOOL_FILE" = "target/windows/resources/espansod.exe" }
script_runner = "@rust"
script = { file = "scripts/sign_windows_exe.rs" }
[tasks.sign-windows-installer]
env = { "TARGET_SIGNTOOL_FILE" = "target/windows/installer/Espanso-Win-Installer-x86_64.exe" }
script_runner = "@rust"
script = { file = "scripts/sign_windows_exe.rs" }
# macOS
[tasks.build-macos-arm-binary]
env = { "BUILD_ARCH" = "aarch64-apple-darwin" }
run_task = [
{ name = "build-binary" }
]
[tasks.build-macos-x86-binary]
env = { "BUILD_ARCH" = "x86_64-apple-darwin" }
run_task = [
{ name = "build-binary" }
]
[tasks.build-universal-binary]
script = { file = "scripts/join_universal_binary.sh"}
dependencies=["build-macos-arm-binary", "build-macos-x86-binary"]
[tasks.create-bundle]
script = { file = "scripts/create_bundle.sh" }
dependencies=["build-binary"]
[tasks.create-universal-bundle]
env = { "EXEC_PATH" = "target/universal/espanso" }
script = { file = "scripts/create_bundle.sh" }
dependencies=["build-universal-binary"]
[tasks.run-bundle]
command="target/mac/Espanso.app/Contents/MacOS/espanso"
args=["${@}"]
dependencies=["create-bundle"]
# Linux
[tasks.create-app-image]
script = { file = "scripts/create_app_image.sh" }
dependencies=["build-binary"]
[tasks.run-app-image]
args=["${@}"]
script='''
#!/usr/bin/env bash
set -e
echo Launching AppImage with args: "$@"
./target/linux/AppImage/out/Espanso-*.AppImage "$@"
'''
dependencies=["create-app-image"]
# Test runs
[tasks.test-output]
command = "cargo"
args = ["test", "--workspace", "--exclude", "espanso-modulo", "--exclude", "espanso-ipc", "--no-default-features", "--", "--nocapture"]

View File

@ -1,4 +1,4 @@
![espanso](images/titlebar.png) ![espanso](images/logo_extended.png)
> A cross-platform Text Expander written in Rust > A cross-platform Text Expander written in Rust
@ -6,7 +6,6 @@
![Language](https://img.shields.io/badge/language-rust-orange) ![Language](https://img.shields.io/badge/language-rust-orange)
![Platforms](https://img.shields.io/badge/platforms-Windows%2C%20macOS%20and%20Linux-blue) ![Platforms](https://img.shields.io/badge/platforms-Windows%2C%20macOS%20and%20Linux-blue)
![License](https://img.shields.io/github/license/federico-terzi/espanso) ![License](https://img.shields.io/github/license/federico-terzi/espanso)
[![Build Status](https://dev.azure.com/freddy6896/espanso/_apis/build/status/federico-terzi.espanso?branchName=master)](https://dev.azure.com/freddy6896/espanso/_build/latest?definitionId=1&branchName=master)
![example](images/example.gif) ![example](images/example.gif)
@ -30,13 +29,17 @@ ___
* Works with almost **any program** * Works with almost **any program**
* Works with **Emojis** 😄 * Works with **Emojis** 😄
* Works with **Images** * Works with **Images**
* Includes a powerful **Search Bar** 🔎
* **Date** expansion support * **Date** expansion support
* **Custom scripts** support * **Custom scripts** support
* **Shell commands** support * **Shell commands** support
* **App-specific** configurations * **App-specific** configurations
* Support [Forms](https://espanso.org/docs/matches/forms/)
* Expandable with **packages** * Expandable with **packages**
* Built-in **package manager** for [espanso hub](https://hub.espanso.org/) * Built-in **package manager** for [espanso hub](https://hub.espanso.org/)
* File based configuration * File based configuration
* Support Regex triggers
* Experimental Wayland support
## Get Started ## Get Started
@ -59,20 +62,17 @@ please consider making a small donation, it really helps :)
## Contributors ## Contributors
Many people helped the project along the way, thanks to all of you. In particular, I want to thank: Many people helped the project along the way, thank you to all of you!
* [Scrumplex](https://scrumplex.net/) - Official AUR repo mantainer and Linux Guru <a href="https://github.com/federico-terzi/espanso/graphs/contributors">
* [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester <img src="https://contrib.rocks/image?repo=federico-terzi/espanso" />
* [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester </a>
* [Timo Runge](http://timorunge.com/) - MacOS contributor
* [NickSeagull](http://nickseagull.github.io/) - Contributor
* [matt-h](https://github.com/matt-h) - Contributor
## Remarks ## Remarks
* Thanks to [libxdo](https://github.com/jordansissel/xdotool) and [xclip](https://github.com/astrand/xclip), used to implement the Linux port. * Thanks to [libxdo](https://github.com/jordansissel/xdotool) and [xclip](https://github.com/astrand/xclip), used to implement the Linux port.
* Thanks to the ModifyPath * Thanks to [libxkbcommon](https://xkbcommon.org/) and [wl-clipboard](https://github.com/bugaevc/wl-clipboard), used to implement the Wayland port.
script, used by espanso to improve the Windows installer. * Thanks to [wxWidgets](https://www.wxwidgets.org/) for providing a powerful cross-platform GUI library.
## License ## License

View File

@ -1,3 +1,8 @@
> TODO: this document is relative to version 1 and will be updated soon for changes introduced in version 2
>
> Despite significant architectural differences, the following points are still a good approximation
> of the internals.
# Security # Security
Espanso has always been designed with a strong focus on security. Espanso has always been designed with a strong focus on security.

View File

@ -1,47 +0,0 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- master
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
sudo apt -y update
sudo apt install -y libxtst-dev libx11-dev libxdo3 libxdo-dev
displayName: Install library dependencies
- template: ci/test.yml
- template: ci/build-linux.yml
- template: ci/deploy.yml
- job: UbuntuDEB
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
sudo docker build -t espanso-ubuntu . -f ci/ubuntu/Dockerfile
sudo docker run --rm -v "$(pwd):/shared" espanso-ubuntu espanso/ci/ubuntu/build_deb.sh
displayName: Setting up docker
- template: ci/deploy.yml
- job: macOS
pool:
vmImage: 'macOS-10.14'
steps:
- template: ci/test.yml
- template: ci/build-macos.yml
- template: ci/deploy.yml
- template: ci/publish-homebrew.yml
- job: Windows
pool:
vmImage: 'windows-2019'
steps:
- template: ci/test.yml
- template: ci/build-win.yml
- template: ci/deploy.yml

View File

@ -1,55 +0,0 @@
extern crate cmake;
use cmake::Config;
use std::path::PathBuf;
/* OS SPECIFIC CONFIGS */
#[cfg(target_os = "windows")]
fn get_config() -> PathBuf {
Config::new("native/libwinbridge").build()
}
#[cfg(target_os = "linux")]
fn get_config() -> PathBuf {
Config::new("native/liblinuxbridge").build()
}
#[cfg(target_os = "macos")]
fn get_config() -> PathBuf {
Config::new("native/libmacbridge").build()
}
/*
OS CUSTOM CARGO CONFIG LINES
Note: this is where linked libraries should be specified.
*/
#[cfg(target_os = "windows")]
fn print_config() {
println!("cargo:rustc-link-lib=static=winbridge");
println!("cargo:rustc-link-lib=dylib=user32");
}
#[cfg(target_os = "linux")]
fn print_config() {
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
println!("cargo:rustc-link-lib=static=linuxbridge");
println!("cargo:rustc-link-lib=dylib=X11");
println!("cargo:rustc-link-lib=dylib=Xtst");
println!("cargo:rustc-link-lib=dylib=xdo");
}
#[cfg(target_os = "macos")]
fn print_config() {
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=macbridge");
println!("cargo:rustc-link-lib=framework=Cocoa");
println!("cargo:rustc-link-lib=framework=IOKit");
}
fn main() {
let dst = get_config();
println!("cargo:rustc-link-search=native={}", dst.display());
print_config();
}

View File

@ -1,11 +0,0 @@
steps:
- script: |
cargo build --release
cd target/release/
tar czf "espanso-linux.tar.gz" espanso
cd ../..
cp target/release/espanso-*.gz .
sha256sum espanso-*.gz | awk '{ print $1 }' > espanso-linux-sha256.txt
ls -la
displayName: "Cargo build and packaging for Linux"

View File

@ -1,19 +0,0 @@
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
addToPath: true
- script: |
python --version
python -m pip install toml click
displayName: Installing python dependencies
- script: |
set -e
python packager.py build
cp target/packager/mac/espanso-*.gz .
cp target/packager/mac/espanso-*.txt .
cp target/packager/mac/espanso.rb .
ls -la
displayName: "Cargo build and packaging for MacOS"

View File

@ -1,18 +0,0 @@
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
addToPath: true
- script: |
python --version
python -m pip install toml click
displayName: Installing python dependencies
- script: |
python packager.py build
copy "target\\packager\\win\\espanso-win-installer.exe" "espanso-win-installer.exe"
copy "target\\packager\\win\\espanso-win-installer-sha256.txt" "espanso-win-installer-sha256.txt"
dir
displayName: "Build and packaging for Windows"

View File

@ -1,36 +0,0 @@
parameters:
github:
isPreRelease: false
repositoryName: '$(Build.Repository.Name)'
gitHubConnection: "MyGithubConnection"
dependsOn: []
displayName: "Release to github"
steps:
- script: |
VER=$(cat Cargo.toml| grep version -m 1 | awk -F '"' '{ print $2 }')
echo '##vso[task.setvariable variable=vers]'v$VER
condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
displayName: Obtain version from Cargo.toml on Unix
- powershell: |
Select-String -Path "Cargo.toml" -Pattern "version" | Select-Object -First 1 -outvariable v
$vv = [regex]::match($v, '"([^"]+)"').Groups[1].Value
echo "##vso[task.setvariable variable=vers]v$vv"
condition: eq(variables['Agent.OS'], 'Windows_NT')
displayName: Obtain version from Cargo.toml on Windows
- task: GitHubRelease@0
displayName: Create GitHub release
inputs:
gitHubConnection: ${{ parameters.github.gitHubConnection }}
tagSource: manual
title: '$(vers)'
tag: '$(vers)'
assetUploadMode: replace
action: edit
assets: 'espanso-*'
addChangeLog: false
repositoryName: ${{ parameters.github.repositoryName }}
isPreRelease: ${{ parameters.github.isPreRelease }}
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))

View File

@ -1,35 +0,0 @@
# defaults for any parameters that aren't specified
parameters:
rust_version: stable
steps:
# Linux and macOS.
- script: |
set -e
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN
echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin"
env:
RUSTUP_TOOLCHAIN: ${{parameters.rust_version}}
displayName: "Install rust (*nix)"
condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
# Windows.
- script: |
curl -sSf -o rustup-init.exe https://win.rustup.rs
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% --default-host x86_64-pc-windows-msvc
set PATH=%PATH%;%USERPROFILE%\.cargo\bin
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
env:
RUSTUP_TOOLCHAIN: ${{parameters.rust_version}}
displayName: "Install rust (windows)"
condition: eq(variables['Agent.OS'], 'Windows_NT')
# Install additional components:
- ${{ each component in parameters.components }}:
- script: rustup component add ${{ component }}
# All platforms.
- script: |
rustup -V
rustup component list --installed
rustc -Vv
cargo -V
displayName: Query rust and cargo versions

View File

@ -1,22 +0,0 @@
steps:
- task: InstallSSHKey@0
inputs:
knownHostsEntry: "github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
sshPublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsB9zcHN84/T5URAsfIpb52HnJl2kUK7WWXyV9pFXaO6yz722JxzVq56J3TTrcUCDhM3DKSGKivB6n/tmLw4mefcY3t7kh8puAtaNrNnB4TWqVPFHZtnpYuYslp1rM92r7Bz1FHfVfsDZxqSWlGU/lp0gNEEgXbr2PCExbCh3TGTsKePARhMAtPEvyEZk1+8uA/HvUTjhuDp7P+BbejAsqtgVF0QoEvqDE5af8DZY6+i1cHRgwBYgSnOus8FHsZUGMyAJQtb+dD7imGw/nzokPJzbmQJwQetyhp52CfThpAm12EFtIU43imb8nndlVAmsIHF6czbmI5LP3U0UcTLct freddy@freddy-Z97M-DS3H"
sshKeySecureFile: "azuressh"
- script: |
set -ex
cat ~/.ssh/known_hosts
git config --global user.email "federicoterzi96@gmail.com"
git config --global user.email "Federico Terzi"
VER=$(cat Cargo.toml| grep version -m 1 | awk -F '"' '{ print $2 }')
git clone git@github.com:federico-terzi/homebrew-espanso.git
rm homebrew-espanso/Formula/espanso.rb
cp espanso.rb homebrew-espanso/Formula/espanso.rb
cd homebrew-espanso
git add -A
git commit -m "Update to version: $VER"
git push
displayName: "Publishing to Homebrew"
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))

View File

@ -1,21 +0,0 @@
parameters:
rust_version: stable
steps:
- script: |
echo Master check
displayName: Master branch check
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
- template: install-rust.yml
- script: |
set -e
cargo test --release
displayName: Cargo tests on Unix
condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
- script: |
cargo test --release
displayName: Cargo tests on Windows
condition: eq(variables['Agent.OS'], 'Windows_NT')

View File

@ -1,16 +0,0 @@
#!/bin/bash
echo "Testing espanso..."
cd espanso
cargo test --release
echo "Building espanso and packaging deb"
cargo deb
cd ..
cp espanso/target/debian/espanso*.deb espanso-debian-amd64.deb
sha256sum espanso-debian-amd64.deb > espanso-debian-amd64-sha256.txt
ls -la
echo "Copying to mounted volume"
cp espanso-debian-* /shared

View File

@ -0,0 +1,31 @@
[package]
name = "espanso-clipboard"
version = "0.1.0"
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
edition = "2018"
build="build.rs"
[features]
# If the wayland feature is enabled, all X11 dependencies will be dropped
# and wayland support will be enabled
wayland = ["wait-timeout"]
# If enabled, avoid linking with the gdiplus library on Windows, which
# might conflict with wxWidgets
avoid-gdi = []
[dependencies]
log = "0.4.14"
lazycell = "1.3.0"
anyhow = "1.0.38"
thiserror = "1.0.23"
lazy_static = "1.4.0"
[target.'cfg(windows)'.dependencies]
widestring = "0.4.3"
[target.'cfg(target_os = "linux")'.dependencies]
wait-timeout = { version = "0.2.0", optional = true }
[build-dependencies]
cc = "1.0.73"

View File

@ -0,0 +1,82 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#[cfg(target_os = "windows")]
fn cc_config() {
println!("cargo:rerun-if-changed=src/win32/native.cpp");
println!("cargo:rerun-if-changed=src/win32/native.h");
cc::Build::new()
.cpp(true)
.include("src/win32/native.h")
.file("src/win32/native.cpp")
.compile("espansoclipboard");
println!("cargo:rustc-link-lib=static=espansoclipboard");
println!("cargo:rustc-link-lib=dylib=user32");
println!("cargo:rustc-link-lib=dylib=gdi32");
if cfg!(not(feature = "avoid-gdi")) {
println!("cargo:rustc-link-lib=dylib=gdiplus");
}
#[cfg(target_env = "gnu")]
println!("cargo:rustc-link-lib=dylib=stdc++");
}
#[cfg(target_os = "linux")]
fn cc_config() {
if cfg!(not(feature = "wayland")) {
println!("cargo:rerun-if-changed=src/x11/native/native.h");
println!("cargo:rerun-if-changed=src/x11/native/native.c");
cc::Build::new()
.cpp(true)
.include("src/x11/native/clip")
.include("src/x11/native")
.file("src/x11/native/clip/clip.cpp")
.file("src/x11/native/clip/clip_x11.cpp")
.file("src/x11/native/clip/image.cpp")
.file("src/x11/native/native.cpp")
.compile("espansoclipboardx11");
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
println!("cargo:rustc-link-lib=static=espansoclipboardx11");
println!("cargo:rustc-link-lib=dylib=xcb");
println!("cargo:rustc-link-lib=dylib=stdc++");
} else {
// Nothing to compile on wayland
}
}
#[cfg(target_os = "macos")]
fn cc_config() {
println!("cargo:rerun-if-changed=src/cocoa/native.mm");
println!("cargo:rerun-if-changed=src/cocoa/native.h");
cc::Build::new()
.cpp(true)
.include("src/cocoa/native.h")
.file("src/cocoa/native.mm")
.compile("espansoclipboard");
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=espansoclipboard");
println!("cargo:rustc-link-lib=framework=Cocoa");
}
fn main() {
cc_config();
}

View File

@ -0,0 +1,28 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::os::raw::c_char;
#[link(name = "espansoclipboard", kind = "static")]
extern "C" {
pub fn clipboard_get_text(buffer: *mut c_char, buffer_size: i32) -> i32;
pub fn clipboard_set_text(text: *const c_char) -> i32;
pub fn clipboard_set_image(image_path: *const c_char) -> i32;
pub fn clipboard_set_html(html_descriptor: *const c_char, fallback_text: *const c_char) -> i32;
}

View File

@ -0,0 +1,112 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
mod ffi;
use std::{
ffi::{CStr, CString},
path::PathBuf,
};
use crate::{Clipboard, ClipboardOperationOptions};
use anyhow::Result;
use log::error;
use thiserror::Error;
pub struct CocoaClipboard {}
impl CocoaClipboard {
pub fn new() -> Result<Self> {
Ok(Self {})
}
}
impl Clipboard for CocoaClipboard {
fn get_text(&self, _: &ClipboardOperationOptions) -> Option<String> {
let mut buffer: [i8; 2048] = [0; 2048];
let native_result =
unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) };
if native_result > 0 {
let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
Some(string.to_string_lossy().to_string())
} else {
None
}
}
fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> {
let string = CString::new(text)?;
let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) };
if native_result > 0 {
Ok(())
} else {
Err(CocoaClipboardError::SetOperationFailed().into())
}
}
fn set_image(
&self,
image_path: &std::path::Path,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if !image_path.exists() || !image_path.is_file() {
return Err(CocoaClipboardError::ImageNotFound(image_path.to_path_buf()).into());
}
let path = CString::new(image_path.to_string_lossy().to_string())?;
let native_result = unsafe { ffi::clipboard_set_image(path.as_ptr()) };
if native_result > 0 {
Ok(())
} else {
Err(CocoaClipboardError::SetOperationFailed().into())
}
}
fn set_html(
&self,
html: &str,
fallback_text: Option<&str>,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
let html_string = CString::new(html)?;
let fallback_string = CString::new(fallback_text.unwrap_or_default())?;
let fallback_ptr = if fallback_text.is_some() {
fallback_string.as_ptr()
} else {
std::ptr::null()
};
let native_result = unsafe { ffi::clipboard_set_html(html_string.as_ptr(), fallback_ptr) };
if native_result > 0 {
Ok(())
} else {
Err(CocoaClipboardError::SetOperationFailed().into())
}
}
}
#[derive(Error, Debug)]
pub enum CocoaClipboardError {
#[error("clipboard set operation failed")]
SetOperationFailed(),
#[error("image not found: `{0}`")]
ImageNotFound(PathBuf),
}

View File

@ -0,0 +1,30 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ESPANSO_CLIPBOARD_H
#define ESPANSO_CLIPBOARD_H
#include <stdint.h>
extern "C" int32_t clipboard_get_text(char * buffer, int32_t buffer_size);
extern "C" int32_t clipboard_set_text(char * text);
extern "C" int32_t clipboard_set_image(char * image_path);
extern "C" int32_t clipboard_set_html(char * html, char * fallback_text);
#endif //ESPANSO_CLIPBOARD_H

View File

@ -0,0 +1,89 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#include "native.h"
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#include <string.h>
int32_t clipboard_get_text(char * buffer, int32_t buffer_size) {
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
for (id element in pasteboard.pasteboardItems) {
NSString *string = [element stringForType: NSPasteboardTypeString];
if (string != NULL) {
const char * text = [string UTF8String];
strncpy(buffer, text, buffer_size);
return 1;
}
}
return 0;
}
int32_t clipboard_set_text(char * text) {
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSArray *array = @[NSPasteboardTypeString];
[pasteboard declareTypes:array owner:nil];
NSString *nsText = [NSString stringWithUTF8String:text];
if (![pasteboard setString:nsText forType:NSPasteboardTypeString]) {
return 0;
}
return 1;
}
int32_t clipboard_set_image(char * image_path) {
NSString *pathString = [NSString stringWithUTF8String:image_path];
NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString];
int result = 0;
if (image != nil) {
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSArray *copiedObjects = [NSArray arrayWithObject:image];
[pasteboard writeObjects:copiedObjects];
result = 1;
}
[image release];
return result;
}
int32_t clipboard_set_html(char * html, char * fallback_text) {
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString];
[pasteboard declareTypes:array owner:nil];
NSString *nsHtml = [NSString stringWithUTF8String:html];
NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil];
NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil];
NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length])
documentAttributes:nil];
[pasteboard setData:rtf forType:NSRTFPboardType];
if (fallback_text) {
NSString *nsText = [NSString stringWithUTF8String:fallback_text];
[pasteboard setString:nsText forType:NSPasteboardTypeString];
}
return 1;
}

View File

@ -0,0 +1,108 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::path::Path;
use anyhow::Result;
use log::info;
#[cfg(target_os = "windows")]
mod win32;
#[cfg(target_os = "linux")]
#[cfg(not(feature = "wayland"))]
mod x11;
#[cfg(target_os = "linux")]
#[cfg(feature = "wayland")]
mod wayland;
#[cfg(target_os = "macos")]
mod cocoa;
pub trait Clipboard {
fn get_text(&self, options: &ClipboardOperationOptions) -> Option<String>;
fn set_text(&self, text: &str, options: &ClipboardOperationOptions) -> Result<()>;
fn set_image(&self, image_path: &Path, options: &ClipboardOperationOptions) -> Result<()>;
fn set_html(
&self,
html: &str,
fallback_text: Option<&str>,
options: &ClipboardOperationOptions,
) -> Result<()>;
}
#[allow(dead_code)]
#[derive(Default)]
pub struct ClipboardOperationOptions {
pub use_xclip_backend: bool,
}
#[allow(dead_code)]
pub struct ClipboardOptions {
// Wayland-only
// The number of milliseconds the wl-clipboard commands are allowed
// to run before triggering a time-out event.
wayland_command_timeout_ms: u64,
}
impl Default for ClipboardOptions {
fn default() -> Self {
Self {
wayland_command_timeout_ms: 2000,
}
}
}
#[cfg(target_os = "windows")]
pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
info!("using Win32Clipboard");
Ok(Box::new(win32::Win32Clipboard::new()?))
}
#[cfg(target_os = "macos")]
pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
info!("using CocoaClipboard");
Ok(Box::new(cocoa::CocoaClipboard::new()?))
}
#[cfg(target_os = "linux")]
#[cfg(not(feature = "wayland"))]
pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
info!("using X11Clipboard");
Ok(Box::new(x11::X11Clipboard::new()?))
}
#[cfg(target_os = "linux")]
#[cfg(feature = "wayland")]
pub fn get_clipboard(options: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
// TODO: On some Wayland compositors (currently sway), the "wlr-data-control" protocol
// could enable the use of a much more efficient implementation relying on the "wl-clipboard-rs" crate.
// Useful links: https://github.com/YaLTeR/wl-clipboard-rs/issues/8
//
// We could even decide the correct implementation at runtime by checking if the
// required protocol is available, if so use the efficient implementation
// instead of the fallback one, which calls the wl-copy and wl-paste binaries, and is thus
// less efficient
info!("using WaylandFallbackClipboard");
Ok(Box::new(wayland::fallback::WaylandFallbackClipboard::new(
options,
)?))
}

View File

@ -0,0 +1,33 @@
# Notes on Wayland and clipboard support
### Running espanso as another user
When running espanso as another user, we need to set up a couple of permissions
in order to enable the clipboard tools to correctly connect to the Wayland desktop.
In particular, we need to add the `espanso` user to the same group as the current user
so that it can access the `/run/user/X` directory (with X depending on the user).
```
# Find the current user wayland dir with
echo $XDG_RUNTIME_DIR # in my case output: /run/user/1000
ls -la /run/user/1000
# Now add the `espanso` user to the current user group
sudo usermod -a -G freddy espanso
# Give permissions to the group
chmod g+rwx /run/user/1000
# Give write permission to the wayland socket
chmod g+w /run/user/1000/wayland-0
```
Now the clipboard should work as expected
## Better implementation
On some Wayland compositors (currently sway), the "wlr-data-control" protocol could enable the use of a much more efficient implementation relying on the "wl-clipboard-rs" crate.
Useful links: https://github.com/YaLTeR/wl-clipboard-rs/issues/8

View File

@ -0,0 +1,218 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{
io::{Read, Write},
os::unix::net::UnixStream,
path::PathBuf,
process::Stdio,
};
use crate::{Clipboard, ClipboardOperationOptions, ClipboardOptions};
use anyhow::Result;
use log::{error, warn};
use std::process::Command;
use thiserror::Error;
use wait_timeout::ChildExt;
pub(crate) struct WaylandFallbackClipboard {
command_timeout: u64,
}
impl WaylandFallbackClipboard {
pub fn new(options: ClipboardOptions) -> Result<Self> {
// Make sure wl-paste and wl-copy are available
if Command::new("wl-paste").arg("--version").output().is_err() {
error!("unable to call 'wl-paste' binary, please install the wl-clipboard package.");
return Err(WaylandFallbackClipboardError::MissingWLClipboard().into());
}
if Command::new("wl-copy").arg("--version").output().is_err() {
error!("unable to call 'wl-copy' binary, please install the wl-clipboard package.");
return Err(WaylandFallbackClipboardError::MissingWLClipboard().into());
}
// Try to connect to the wayland display
let wayland_socket = if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
let wayland_display = if let Ok(display) = std::env::var("WAYLAND_DISPLAY") {
display
} else {
warn!("Could not determine wayland display from WAYLAND_DISPLAY env variable, falling back to 'wayland-0'");
warn!("Note that this might not work on some systems.");
"wayland-0".to_string()
};
PathBuf::from(runtime_dir).join(wayland_display)
} else {
error!("environment variable XDG_RUNTIME_DIR is missing, can't initialize the clipboard");
return Err(WaylandFallbackClipboardError::MissingEnvVariable().into());
};
if UnixStream::connect(wayland_socket).is_err() {
error!("failed to connect to Wayland display");
return Err(WaylandFallbackClipboardError::ConnectionFailed().into());
}
Ok(Self {
command_timeout: options.wayland_command_timeout_ms,
})
}
}
impl Clipboard for WaylandFallbackClipboard {
fn get_text(&self, _: &ClipboardOperationOptions) -> Option<String> {
let timeout = std::time::Duration::from_millis(self.command_timeout);
match Command::new("wl-paste")
.arg("--no-newline")
.stdout(Stdio::piped())
.spawn()
{
Ok(mut child) => match child.wait_timeout(timeout) {
Ok(status_code) => {
if let Some(status) = status_code {
if status.success() {
if let Some(mut io) = child.stdout {
let mut output = Vec::new();
io.read_to_end(&mut output).ok()?;
Some(String::from_utf8_lossy(&output).to_string())
} else {
None
}
} else {
error!("error, wl-paste exited with non-zero exit code");
None
}
} else {
error!("error, wl-paste has timed-out, killing the process");
if child.kill().is_err() {
error!("unable to kill wl-paste");
}
None
}
}
Err(err) => {
error!("error while executing 'wl-paste': {}", err);
None
}
},
Err(err) => {
error!("could not invoke 'wl-paste': {}", err);
None
}
}
}
fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> {
self.invoke_command_with_timeout(&mut Command::new("wl-copy"), text.as_bytes(), "wl-copy")
}
fn set_image(
&self,
image_path: &std::path::Path,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if !image_path.exists() || !image_path.is_file() {
return Err(WaylandFallbackClipboardError::ImageNotFound(image_path.to_path_buf()).into());
}
// Load the image data
let mut file = std::fs::File::open(image_path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
self.invoke_command_with_timeout(
Command::new("wl-copy").arg("--type").arg("image/png"),
&data,
"wl-copy",
)
}
fn set_html(
&self,
html: &str,
_fallback_text: Option<&str>,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
self.invoke_command_with_timeout(
Command::new("wl-copy").arg("--type").arg("text/html"),
html.as_bytes(),
"wl-copy",
)
}
}
impl WaylandFallbackClipboard {
fn invoke_command_with_timeout(
&self,
command: &mut Command,
data: &[u8],
name: &str,
) -> Result<()> {
let timeout = std::time::Duration::from_millis(self.command_timeout);
match command.stdin(Stdio::piped()).spawn() {
Ok(mut child) => {
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(data)?;
}
match child.wait_timeout(timeout) {
Ok(status_code) => {
if let Some(status) = status_code {
if status.success() {
Ok(())
} else {
error!("error, {} exited with non-zero exit code", name);
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
}
} else {
error!("error, {} has timed-out, killing the process", name);
if child.kill().is_err() {
error!("unable to kill {}", name);
}
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
}
}
Err(err) => {
error!("error while executing '{}': {}", name, err);
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
}
}
}
Err(err) => {
error!("could not invoke '{}': {}", name, err);
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
}
}
}
}
#[derive(Error, Debug)]
pub(crate) enum WaylandFallbackClipboardError {
#[error("wl-clipboard binaries are missing")]
MissingWLClipboard(),
#[error("missing XDG_RUNTIME_DIR env variable")]
MissingEnvVariable(),
#[error("can't connect to Wayland display")]
ConnectionFailed(),
#[error("clipboard set operation failed")]
SetOperationFailed(),
#[error("image not found: `{0}`")]
ImageNotFound(PathBuf),
}

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of espanso. * This file is part of espanso.
* *
* Copyright (C) 2019 Federico Terzi * Copyright (C) 2019-2021 Federico Terzi
* *
* espanso is free software: you can redistribute it and/or modify * 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,10 +17,4 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
#import <Cocoa/Cocoa.h> pub(crate) mod fallback;
@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate>
@end

View File

@ -0,0 +1,28 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::os::raw::c_char;
#[link(name = "espansoclipboard", kind = "static")]
extern "C" {
pub fn clipboard_get_text(buffer: *mut u16, buffer_size: i32) -> i32;
pub fn clipboard_set_text(text: *const u16) -> i32;
pub fn clipboard_set_image(image_path: *const u16) -> i32;
pub fn clipboard_set_html(html_descriptor: *const c_char, fallback_text: *const u16) -> i32;
}

View File

@ -0,0 +1,156 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
mod ffi;
use std::{ffi::CString, path::PathBuf};
use crate::{Clipboard, ClipboardOperationOptions};
use anyhow::Result;
use log::error;
use thiserror::Error;
use widestring::{U16CStr, U16CString};
pub struct Win32Clipboard {}
impl Win32Clipboard {
pub fn new() -> Result<Self> {
Ok(Self {})
}
}
impl Clipboard for Win32Clipboard {
fn get_text(&self, _: &ClipboardOperationOptions) -> Option<String> {
let mut buffer: [u16; 2048] = [0; 2048];
let native_result =
unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) };
if native_result > 0 {
let string = unsafe { U16CStr::from_ptr_str(buffer.as_ptr()) };
Some(string.to_string_lossy())
} else {
None
}
}
fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> {
let string = U16CString::from_str(text)?;
let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) };
if native_result > 0 {
Ok(())
} else {
Err(Win32ClipboardError::SetOperationFailed().into())
}
}
fn set_image(
&self,
image_path: &std::path::Path,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if !image_path.exists() || !image_path.is_file() {
return Err(Win32ClipboardError::ImageNotFound(image_path.to_path_buf()).into());
}
let path = U16CString::from_os_str(image_path.as_os_str())?;
let native_result = unsafe { ffi::clipboard_set_image(path.as_ptr()) };
if native_result > 0 {
Ok(())
} else {
Err(Win32ClipboardError::SetOperationFailed().into())
}
}
fn set_html(
&self,
html: &str,
fallback_text: Option<&str>,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
let html_descriptor = generate_html_descriptor(html);
let html_string = CString::new(html_descriptor)?;
let fallback_string = U16CString::from_str(fallback_text.unwrap_or_default())?;
let fallback_ptr = if fallback_text.is_some() {
fallback_string.as_ptr()
} else {
std::ptr::null()
};
let native_result = unsafe { ffi::clipboard_set_html(html_string.as_ptr(), fallback_ptr) };
if native_result > 0 {
Ok(())
} else {
Err(Win32ClipboardError::SetOperationFailed().into())
}
}
}
fn generate_html_descriptor(html: &str) -> String {
// In order to set the HTML clipboard, we have to create a prefix with a specific format
// For more information, look here:
// https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
// https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
let content = format!("<!--StartFragment-->{}<!--EndFragment-->", html);
let tokens = vec![
"Version:0.9",
"StartHTML:<<STR*#>",
"EndHTML:<<END*#>",
"StartFragment:<<SFG#*>",
"EndFragment:<<EFG#*>",
"<html>",
"<body>",
&content,
"</body>",
"</html>",
];
let mut render = tokens.join("\r\n");
// Now replace the placeholders with the actual positions
render = render.replace(
"<<STR*#>",
&format!("{:0>8}", render.find("<html>").unwrap_or_default()),
);
render = render.replace("<<END*#>", &format!("{:0>8}", render.len()));
render = render.replace(
"<<SFG#*>",
&format!(
"{:0>8}",
render.find("<!--StartFragment-->").unwrap_or_default() + "<!--StartFragment-->".len()
),
);
render = render.replace(
"<<EFG#*>",
&format!(
"{:0>8}",
render.find("<!--EndFragment-->").unwrap_or_default()
),
);
render
}
#[derive(Error, Debug)]
pub enum Win32ClipboardError {
#[error("clipboard set operation failed")]
SetOperationFailed(),
#[error("image not found: `{0}`")]
ImageNotFound(PathBuf),
}

View File

@ -0,0 +1,184 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#include "native.h"
#include <iostream>
#include <stdio.h>
#include <string>
#include <vector>
#include <memory>
#include <array>
#define UNICODE
#ifdef __MINGW32__
#ifndef WINVER
#define WINVER 0x0606
#endif
#define STRSAFE_NO_DEPRECATE
#endif
#include <windows.h>
#include <winuser.h>
#include <strsafe.h>
#include <gdiplus.h>
#include <Windows.h>
int32_t clipboard_get_text(wchar_t *buffer, int32_t buffer_size)
{
int32_t result = 0;
if (OpenClipboard(NULL))
{
HANDLE hData;
if (hData = GetClipboardData(CF_UNICODETEXT))
{
HGLOBAL hMem;
if (hMem = GlobalLock(hData))
{
GlobalUnlock(hMem);
wcsncpy(buffer, (wchar_t *)hMem, buffer_size);
if (wcsnlen_s(buffer, buffer_size) > 0)
{
result = 1;
}
}
}
CloseClipboard();
}
return result;
}
int32_t clipboard_set_text(wchar_t *text)
{
int32_t result = 0;
const size_t len = wcslen(text) + 1;
if (OpenClipboard(NULL))
{
EmptyClipboard();
HGLOBAL hMem;
if (hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t)))
{
memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t));
GlobalUnlock(hMem);
if (SetClipboardData(CF_UNICODETEXT, hMem))
{
result = 1;
}
}
CloseClipboard();
}
return result;
}
int32_t clipboard_set_image(wchar_t *path)
{
int32_t result = 0;
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path);
if (gdibmp)
{
HBITMAP hbitmap;
gdibmp->GetHBITMAP(0, &hbitmap);
if (OpenClipboard(NULL))
{
EmptyClipboard();
DIBSECTION ds;
if (GetObject(hbitmap, sizeof(DIBSECTION), &ds))
{
HDC hdc = GetDC(HWND_DESKTOP);
//create compatible bitmap (get DDB from DIB)
HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT,
ds.dsBm.bmBits, (BITMAPINFO *)&ds.dsBmih, DIB_RGB_COLORS);
ReleaseDC(HWND_DESKTOP, hdc);
SetClipboardData(CF_BITMAP, hbitmap_ddb);
DeleteObject(hbitmap_ddb);
result = 1;
}
CloseClipboard();
}
DeleteObject(hbitmap);
delete gdibmp;
}
Gdiplus::GdiplusShutdown(gdiplusToken);
return result;
}
// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
int32_t clipboard_set_html(char * html_descriptor, wchar_t * fallback_text) {
// Get clipboard id for HTML format
static int cfid = 0;
if(!cfid) {
cfid = RegisterClipboardFormat(L"HTML Format");
}
int32_t result = 0;
const size_t html_len = strlen(html_descriptor) + 1;
const size_t fallback_len = (fallback_text != nullptr) ? wcslen(fallback_text) + 1 : 0;
if (OpenClipboard(NULL))
{
EmptyClipboard();
// First copy the HTML
HGLOBAL hMem;
if (hMem = GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char)))
{
memcpy(GlobalLock(hMem), html_descriptor, html_len * sizeof(char));
GlobalUnlock(hMem);
if (SetClipboardData(cfid, hMem))
{
result = 1;
}
}
// Then try to set the fallback text, if present.
if (fallback_len > 0) {
if (hMem = GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t)))
{
memcpy(GlobalLock(hMem), fallback_text, fallback_len * sizeof(wchar_t));
GlobalUnlock(hMem);
SetClipboardData(CF_UNICODETEXT, hMem);
}
}
CloseClipboard();
}
return result;
}

View File

@ -0,0 +1,30 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ESPANSO_CLIPBOARD_H
#define ESPANSO_CLIPBOARD_H
#include <stdint.h>
extern "C" int32_t clipboard_get_text(wchar_t * buffer, int32_t buffer_size);
extern "C" int32_t clipboard_set_text(wchar_t * text);
extern "C" int32_t clipboard_set_image(wchar_t * image);
extern "C" int32_t clipboard_set_html(char * html_descriptor, wchar_t * fallback_text);
#endif //ESPANSO_CLIPBOARD_H

View File

@ -0,0 +1,82 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use crate::{Clipboard, ClipboardOperationOptions};
mod native;
mod xclip;
pub(crate) struct X11Clipboard {
native_backend: native::X11NativeClipboard,
xclip_backend: xclip::XClipClipboard,
}
impl X11Clipboard {
pub fn new() -> Result<Self> {
Ok(Self {
native_backend: native::X11NativeClipboard::new()?,
xclip_backend: xclip::XClipClipboard::new(),
})
}
}
impl Clipboard for X11Clipboard {
fn get_text(&self, options: &ClipboardOperationOptions) -> Option<String> {
if options.use_xclip_backend {
self.xclip_backend.get_text(options)
} else {
self.native_backend.get_text(options)
}
}
fn set_text(&self, text: &str, options: &ClipboardOperationOptions) -> anyhow::Result<()> {
if options.use_xclip_backend {
self.xclip_backend.set_text(text, options)
} else {
self.native_backend.set_text(text, options)
}
}
fn set_image(
&self,
image_path: &std::path::Path,
options: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if options.use_xclip_backend {
self.xclip_backend.set_image(image_path, options)
} else {
self.native_backend.set_image(image_path, options)
}
}
fn set_html(
&self,
html: &str,
fallback_text: Option<&str>,
options: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if options.use_xclip_backend {
self.xclip_backend.set_html(html, fallback_text, options)
} else {
self.native_backend.set_html(html, fallback_text, options)
}
}
}

View File

@ -0,0 +1,4 @@
The X11NativeClipboard modules uses the wonderful [clip](https://github.com/dacap/clip) library
by David Capello to manipulate the clipboard.
At the time of writing, the library is MIT licensed.

View File

@ -0,0 +1,20 @@
Copyright (c) 2015-2020 David Capello
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,174 @@
// Clip Library
// Copyright (c) 2015-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#include "clip.h"
#include "clip_lock_impl.h"
#include <vector>
#include <stdexcept>
namespace clip {
namespace {
void default_error_handler(ErrorCode code) {
static const char* err[] = {
"Cannot lock clipboard",
"Image format is not supported"
};
throw std::runtime_error(err[static_cast<int>(code)]);
}
} // anonymous namespace
error_handler g_error_handler = default_error_handler;
lock::lock(void* native_window_handle)
: p(new impl(native_window_handle)) {
}
lock::~lock() = default;
bool lock::locked() const {
return p->locked();
}
bool lock::clear() {
return p->clear();
}
bool lock::is_convertible(format f) const {
return p->is_convertible(f);
}
bool lock::set_data(format f, const char* buf, size_t length) {
return p->set_data(f, buf, length);
}
bool lock::get_data(format f, char* buf, size_t len) const {
return p->get_data(f, buf, len);
}
size_t lock::get_data_length(format f) const {
return p->get_data_length(f);
}
bool lock::set_image(const image& img) {
return p->set_image(img);
}
bool lock::get_image(image& img) const {
return p->get_image(img);
}
bool lock::get_image_spec(image_spec& spec) const {
return p->get_image_spec(spec);
}
format empty_format() { return 0; }
format text_format() { return 1; }
format image_format() { return 2; }
bool has(format f) {
lock l;
if (l.locked())
return l.is_convertible(f);
else
return false;
}
bool clear() {
lock l;
if (l.locked())
return l.clear();
else
return false;
}
bool set_text(const std::string& value) {
lock l;
if (l.locked()) {
l.clear();
return l.set_data(text_format(), value.c_str(), value.size());
}
else
return false;
}
bool get_text(std::string& value) {
lock l;
if (!l.locked())
return false;
format f = text_format();
if (!l.is_convertible(f))
return false;
size_t len = l.get_data_length(f);
if (len > 0) {
std::vector<char> buf(len);
l.get_data(f, &buf[0], len);
value = &buf[0];
return true;
}
else {
value.clear();
return true;
}
}
bool set_image(const image& img) {
lock l;
if (l.locked()) {
l.clear();
return l.set_image(img);
}
else
return false;
}
bool get_image(image& img) {
lock l;
if (!l.locked())
return false;
format f = image_format();
if (!l.is_convertible(f))
return false;
return l.get_image(img);
}
bool get_image_spec(image_spec& spec) {
lock l;
if (!l.locked())
return false;
format f = image_format();
if (!l.is_convertible(f))
return false;
return l.get_image_spec(spec);
}
void set_error_handler(error_handler handler) {
g_error_handler = handler;
}
error_handler get_error_handler() {
return g_error_handler;
}
#ifdef HAVE_XCB_XLIB_H
static int g_x11_timeout = 1000;
void set_x11_wait_timeout(int msecs) { g_x11_timeout = msecs; }
int get_x11_wait_timeout() { return g_x11_timeout; }
#else
void set_x11_wait_timeout(int) { }
int get_x11_wait_timeout() { return 1000; }
#endif
} // namespace clip

View File

@ -0,0 +1,178 @@
// Clip Library
// Copyright (c) 2015-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef CLIP_H_INCLUDED
#define CLIP_H_INCLUDED
#pragma once
#include <cassert>
#include <memory>
#include <string>
namespace clip {
// ======================================================================
// Low-level API to lock the clipboard/pasteboard and modify it
// ======================================================================
// Clipboard format identifier.
typedef size_t format;
class image;
struct image_spec;
class lock {
public:
// You can give your current HWND as the "native_window_handle."
// Windows clipboard functions use this handle to open/close
// (lock/unlock) the clipboard. From the MSDN documentation we
// need this handler so SetClipboardData() doesn't fail after a
// EmptyClipboard() call. Anyway it looks to work just fine if we
// call OpenClipboard() with a null HWND.
lock(void* native_window_handle = nullptr);
~lock();
// Returns true if we've locked the clipboard successfully in
// lock() constructor.
bool locked() const;
// Clears the clipboard content. If you don't clear the content,
// previous clipboard content (in unknown formats) could persist
// after the unlock.
bool clear();
// Returns true if the clipboard can be converted to the given
// format.
bool is_convertible(format f) const;
bool set_data(format f, const char* buf, size_t len);
bool get_data(format f, char* buf, size_t len) const;
size_t get_data_length(format f) const;
// For images
bool set_image(const image& image);
bool get_image(image& image) const;
bool get_image_spec(image_spec& spec) const;
private:
class impl;
std::unique_ptr<impl> p;
};
format register_format(const std::string& name);
// This format is when the clipboard has no content.
format empty_format();
// When the clipboard has UTF8 text.
format text_format();
// When the clipboard has an image.
format image_format();
// Returns true if the clipboard has content of the given type.
bool has(format f);
// Clears the clipboard content.
bool clear();
// ======================================================================
// Error handling
// ======================================================================
enum class ErrorCode {
CannotLock,
ImageNotSupported,
};
typedef void (*error_handler)(ErrorCode code);
void set_error_handler(error_handler f);
error_handler get_error_handler();
// ======================================================================
// Text
// ======================================================================
// High-level API to put/get UTF8 text in/from the clipboard. These
// functions returns false in case of error.
bool set_text(const std::string& value);
bool get_text(std::string& value);
// ======================================================================
// Image
// ======================================================================
struct image_spec {
unsigned long width = 0;
unsigned long height = 0;
unsigned long bits_per_pixel = 0;
unsigned long bytes_per_row = 0;
unsigned long red_mask = 0;
unsigned long green_mask = 0;
unsigned long blue_mask = 0;
unsigned long alpha_mask = 0;
unsigned long red_shift = 0;
unsigned long green_shift = 0;
unsigned long blue_shift = 0;
unsigned long alpha_shift = 0;
};
// The image data must contain straight RGB values
// (non-premultiplied by alpha). The image retrieved from the
// clipboard will be non-premultiplied too. Basically you will be
// always dealing with straight alpha images.
//
// Details: Windows expects premultiplied images on its clipboard
// content, so the library code make the proper conversion
// automatically. macOS handles straight alpha directly, so there is
// no conversion at all. Linux/X11 images are transferred in
// image/png format which are specified in straight alpha.
class image {
public:
image();
image(const image_spec& spec);
image(const void* data, const image_spec& spec);
image(const image& image);
image(image&& image);
~image();
image& operator=(const image& image);
image& operator=(image&& image);
char* data() const { return m_data; }
const image_spec& spec() const { return m_spec; }
bool is_valid() const { return m_data != nullptr; }
void reset();
private:
void copy_image(const image& image);
void move_image(image&& image);
bool m_own_data;
char* m_data;
image_spec m_spec;
};
// High-level API to set/get an image in/from the clipboard. These
// functions returns false in case of error.
bool set_image(const image& img);
bool get_image(image& img);
bool get_image_spec(image_spec& spec);
// ======================================================================
// Platform-specific
// ======================================================================
// Only for X11: Sets the time (in milliseconds) that we must wait
// for the selection/clipboard owner to receive the content. This
// value is 1000 (one second) by default.
void set_x11_wait_timeout(int msecs);
int get_x11_wait_timeout();
} // namespace clip
#endif // CLIP_H_INCLUDED

View File

@ -0,0 +1,76 @@
// Clip Library
// Copyright (C) 2020 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef CLIP_COMMON_H_INCLUDED
#define CLIP_COMMON_H_INCLUDED
#pragma once
namespace clip {
namespace details {
inline void divide_rgb_by_alpha(image& img,
bool hasAlphaGreaterThanZero = false) {
const image_spec& spec = img.spec();
bool hasValidPremultipliedAlpha = true;
for (unsigned long y=0; y<spec.height; ++y) {
const uint32_t* dst = (uint32_t*)(img.data()+y*spec.bytes_per_row);
for (unsigned long x=0; x<spec.width; ++x, ++dst) {
const uint32_t c = *dst;
const int r = ((c & spec.red_mask ) >> spec.red_shift );
const int g = ((c & spec.green_mask) >> spec.green_shift);
const int b = ((c & spec.blue_mask ) >> spec.blue_shift );
const int a = ((c & spec.alpha_mask) >> spec.alpha_shift);
if (a > 0)
hasAlphaGreaterThanZero = true;
if (r > a || g > a || b > a)
hasValidPremultipliedAlpha = false;
}
}
for (unsigned long y=0; y<spec.height; ++y) {
uint32_t* dst = (uint32_t*)(img.data()+y*spec.bytes_per_row);
for (unsigned long x=0; x<spec.width; ++x, ++dst) {
const uint32_t c = *dst;
int r = ((c & spec.red_mask ) >> spec.red_shift );
int g = ((c & spec.green_mask) >> spec.green_shift);
int b = ((c & spec.blue_mask ) >> spec.blue_shift );
int a = ((c & spec.alpha_mask) >> spec.alpha_shift);
// If all alpha values = 0, we make the image opaque.
if (!hasAlphaGreaterThanZero) {
a = 255;
// We cannot change the image spec (e.g. spec.alpha_mask=0) to
// make the image opaque, because the "spec" of the image is
// read-only. The image spec used by the client is the one
// returned by get_image_spec().
}
// If there is alpha information and it's pre-multiplied alpha
else if (hasValidPremultipliedAlpha) {
if (a > 0) {
// Convert it to straight alpha
r = r * 255 / a;
g = g * 255 / a;
b = b * 255 / a;
}
}
*dst =
(r << spec.red_shift ) |
(g << spec.green_shift) |
(b << spec.blue_shift ) |
(a << spec.alpha_shift);
}
}
}
} // namespace details
} // namespace clip
#endif // CLIP_H_INCLUDED

View File

@ -0,0 +1,33 @@
// Clip Library
// Copyright (c) 2015-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef CLIP_LOCK_IMPL_H_INCLUDED
#define CLIP_LOCK_IMPL_H_INCLUDED
namespace clip {
class lock::impl {
public:
impl(void* native_window_handle);
~impl();
bool locked() const { return m_locked; }
bool clear();
bool is_convertible(format f) const;
bool set_data(format f, const char* buf, size_t len);
bool get_data(format f, char* buf, size_t len) const;
size_t get_data_length(format f) const;
bool set_image(const image& image);
bool get_image(image& image) const;
bool get_image_spec(image_spec& spec) const;
private:
bool m_locked;
};
} // namespace clip
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,225 @@
// Clip Library
// Copyright (c) 2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#include "clip.h"
#include <algorithm>
#include <vector>
#include "png.h"
namespace clip {
namespace x11 {
//////////////////////////////////////////////////////////////////////
// Functions to convert clip::image into png data to store it in the
// clipboard.
void write_data_fn(png_structp png, png_bytep buf, png_size_t len) {
std::vector<uint8_t>& output = *(std::vector<uint8_t>*)png_get_io_ptr(png);
const size_t i = output.size();
output.resize(i+len);
std::copy(buf, buf+len, output.begin()+i);
}
bool write_png(const image& image,
std::vector<uint8_t>& output) {
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr, nullptr, nullptr);
if (!png)
return false;
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_write_struct(&png, nullptr);
return false;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
return false;
}
png_set_write_fn(png,
(png_voidp)&output,
write_data_fn,
nullptr); // No need for a flush function
const image_spec& spec = image.spec();
int color_type = (spec.alpha_mask ?
PNG_COLOR_TYPE_RGB_ALPHA:
PNG_COLOR_TYPE_RGB);
png_set_IHDR(png, info,
spec.width, spec.height, 8, color_type,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
png_write_info(png, info);
png_set_packing(png);
png_bytep row =
(png_bytep)png_malloc(png, png_get_rowbytes(png, info));
for (png_uint_32 y=0; y<spec.height; ++y) {
const uint32_t* src =
(const uint32_t*)(((const uint8_t*)image.data())
+ y*spec.bytes_per_row);
uint8_t* dst = row;
unsigned int x, c;
for (x=0; x<spec.width; x++) {
c = *(src++);
*(dst++) = (c & spec.red_mask ) >> spec.red_shift;
*(dst++) = (c & spec.green_mask) >> spec.green_shift;
*(dst++) = (c & spec.blue_mask ) >> spec.blue_shift;
if (color_type == PNG_COLOR_TYPE_RGB_ALPHA)
*(dst++) = (c & spec.alpha_mask) >> spec.alpha_shift;
}
png_write_rows(png, &row, 1);
}
png_free(png, row);
png_write_end(png, info);
png_destroy_write_struct(&png, &info);
return true;
}
//////////////////////////////////////////////////////////////////////
// Functions to convert png data stored in the clipboard to a
// clip::image.
struct read_png_io {
const uint8_t* buf;
size_t len;
size_t pos;
};
void read_data_fn(png_structp png, png_bytep buf, png_size_t len) {
read_png_io& io = *(read_png_io*)png_get_io_ptr(png);
if (io.pos < io.len) {
size_t n = std::min(len, io.len-io.pos);
if (n > 0) {
std::copy(io.buf+io.pos,
io.buf+io.pos+n,
buf);
io.pos += n;
}
}
}
bool read_png(const uint8_t* buf,
const size_t len,
image* output_image,
image_spec* output_spec) {
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr, nullptr, nullptr);
if (!png)
return false;
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, nullptr, nullptr);
return false;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, nullptr);
return false;
}
read_png_io io = { buf, len, 0 };
png_set_read_fn(png, (png_voidp)&io, read_data_fn);
png_read_info(png, info);
png_uint_32 width, height;
int bit_depth, color_type, interlace_type;
png_get_IHDR(png, info, &width, &height,
&bit_depth, &color_type,
&interlace_type,
nullptr, nullptr);
image_spec spec;
spec.width = width;
spec.height = height;
spec.bits_per_pixel = 32;
spec.bytes_per_row = png_get_rowbytes(png, info);
spec.red_mask = 0x000000ff;
spec.green_mask = 0x0000ff00;
spec.blue_mask = 0x00ff0000;
spec.red_shift = 0;
spec.green_shift = 8;
spec.blue_shift = 16;
if (color_type == PNG_COLOR_TYPE_RGB_ALPHA ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
spec.alpha_mask = 0xff000000;
spec.alpha_shift = 24;
}
else {
spec.alpha_mask = 0;
spec.alpha_shift = 0;
}
if (output_spec)
*output_spec = spec;
if (output_image &&
width > 0 &&
height > 0) {
image img(spec);
// We want RGB 24-bit or RGBA 32-bit as a result
png_set_strip_16(png); // Down to 8-bit
png_set_packing(png); // Use one byte if color depth < 8-bit
png_set_expand_gray_1_2_4_to_8(png);
png_set_palette_to_rgb(png);
png_set_gray_to_rgb(png);
png_set_tRNS_to_alpha(png);
int number_passes = png_set_interlace_handling(png);
png_read_update_info(png, info);
png_bytepp rows = (png_bytepp)png_malloc(png, sizeof(png_bytep)*height);
png_uint_32 y;
for (y=0; y<height; ++y)
rows[y] = (png_bytep)png_malloc(png, spec.bytes_per_row);
for (int pass=0; pass<number_passes; ++pass)
for (y=0; y<height; ++y)
png_read_rows(png, rows+y, nullptr, 1);
for (y=0; y<height; ++y) {
const uint8_t* src = rows[y];
uint32_t* dst = (uint32_t*)(img.data() + y*spec.bytes_per_row);
unsigned int x, r, g, b, a = 255;
for (x=0; x<width; x++) {
r = *(src++);
g = *(src++);
b = *(src++);
if (spec.alpha_mask)
a = *(src++);
*(dst++) =
(r << spec.red_shift) |
(g << spec.green_shift) |
(b << spec.blue_shift) |
(a << spec.alpha_shift);
}
png_free(png, rows[y]);
}
png_free(png, rows);
std::swap(*output_image, img);
}
png_destroy_read_struct(&png, &info, nullptr);
return true;
}
} // namespace x11
} // namespace clip

View File

@ -0,0 +1,83 @@
// Clip Library
// Copyright (c) 2015-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#include "clip.h"
namespace clip {
image::image()
: m_own_data(false),
m_data(nullptr)
{
}
image::image(const image_spec& spec)
: m_own_data(true),
m_data(new char[spec.bytes_per_row*spec.height]),
m_spec(spec) {
}
image::image(const void* data, const image_spec& spec)
: m_own_data(false),
m_data((char*)data),
m_spec(spec) {
}
image::image(const image& image)
: m_own_data(false),
m_data(nullptr),
m_spec(image.m_spec) {
copy_image(image);
}
image::image(image&& image)
: m_own_data(false),
m_data(nullptr) {
move_image(std::move(image));
}
image::~image() {
reset();
}
image& image::operator=(const image& image) {
copy_image(image);
return *this;
}
image& image::operator=(image&& image) {
move_image(std::move(image));
return *this;
}
void image::reset() {
if (m_own_data) {
delete[] m_data;
m_own_data = false;
m_data = nullptr;
}
}
void image::copy_image(const image& image) {
reset();
m_spec = image.spec();
std::size_t n = m_spec.bytes_per_row*m_spec.height;
m_own_data = true;
m_data = new char[n];
std::copy(image.data(),
image.data()+n,
m_data);
}
void image::move_image(image&& image) {
std::swap(m_own_data, image.m_own_data);
std::swap(m_data, image.m_data);
std::swap(m_spec, image.m_spec);
}
} // namespace clip

View File

@ -0,0 +1,28 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::os::raw::c_char;
#[link(name = "espansoclipboardx11", kind = "static")]
extern "C" {
pub fn clipboard_x11_get_text(buffer: *mut c_char, buffer_size: i32) -> i32;
pub fn clipboard_x11_set_text(text: *const c_char) -> i32;
pub fn clipboard_x11_set_html(html: *const c_char, fallback_text: *const c_char) -> i32;
pub fn clipboard_x11_set_image(buffer: *const u8, buffer_size: i32) -> i32;
}

View File

@ -0,0 +1,117 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{
ffi::{CStr, CString},
io::Read,
path::PathBuf,
};
use crate::{Clipboard, ClipboardOperationOptions};
use anyhow::Result;
use std::os::raw::c_char;
use thiserror::Error;
mod ffi;
pub struct X11NativeClipboard {}
impl X11NativeClipboard {
pub fn new() -> Result<Self> {
Ok(Self {})
}
}
impl Clipboard for X11NativeClipboard {
fn get_text(&self, _: &ClipboardOperationOptions) -> Option<String> {
let mut buffer: [c_char; 2048] = [0; 2048];
let native_result =
unsafe { ffi::clipboard_x11_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) };
if native_result > 0 {
let string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
Some(string.to_string_lossy().to_string())
} else {
None
}
}
fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> {
let string = CString::new(text)?;
let native_result = unsafe { ffi::clipboard_x11_set_text(string.as_ptr()) };
if native_result > 0 {
Ok(())
} else {
Err(X11NativeClipboardError::SetOperationFailed().into())
}
}
fn set_image(
&self,
image_path: &std::path::Path,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if !image_path.exists() || !image_path.is_file() {
return Err(X11NativeClipboardError::ImageNotFound(image_path.to_path_buf()).into());
}
// Load the image data
let mut file = std::fs::File::open(image_path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
let native_result = unsafe { ffi::clipboard_x11_set_image(data.as_ptr(), data.len() as i32) };
if native_result > 0 {
Ok(())
} else {
Err(X11NativeClipboardError::SetOperationFailed().into())
}
}
fn set_html(
&self,
html: &str,
fallback_text: Option<&str>,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
let html_string = CString::new(html)?;
let fallback_string = CString::new(fallback_text.unwrap_or_default())?;
let fallback_ptr = if fallback_text.is_some() {
fallback_string.as_ptr()
} else {
std::ptr::null()
};
let native_result = unsafe { ffi::clipboard_x11_set_html(html_string.as_ptr(), fallback_ptr) };
if native_result > 0 {
Ok(())
} else {
Err(X11NativeClipboardError::SetOperationFailed().into())
}
}
}
#[derive(Error, Debug)]
pub enum X11NativeClipboardError {
#[error("clipboard set operation failed")]
SetOperationFailed(),
#[error("image not found: `{0}`")]
ImageNotFound(PathBuf),
}

View File

@ -0,0 +1,76 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#include "native.h"
#include "clip/clip.h"
#include "string.h"
#include <iostream>
clip::format html_format = clip::register_format("text/html");
clip::format png_format = clip::register_format("image/png");
int32_t clipboard_x11_get_text(char * buffer, int32_t buffer_size) {
std::string value;
if (!clip::get_text(value)) {
return 0;
}
if (value.length() == 0) {
return 0;
}
strncpy(buffer, value.c_str(), buffer_size - 1);
return 1;
}
int32_t clipboard_x11_set_text(char * text) {
if (!clip::set_text(text)) {
return 0;
} else {
return 1;
}
}
int32_t clipboard_x11_set_html(char * html, char * fallback_text) {
clip::lock l;
if (!l.clear()) {
return 0;
}
if (!l.set_data(html_format, html, strlen(html))) {
return 0;
}
if (fallback_text) {
// Best effort to set the fallback
l.set_data(clip::text_format(), fallback_text, strlen(fallback_text));
}
return 1;
}
int32_t clipboard_x11_set_image(char * buffer, int32_t size) {
clip::lock l;
if (!l.clear()) {
return 0;
}
if (!l.set_data(png_format, buffer, size)) {
return 0;
}
return 1;
}

View File

@ -0,0 +1,31 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ESPANSO_X11_CLIPBOARD_H
#define ESPANSO_X11_CLIPBOARD_H
#include <stdint.h>
extern "C" int32_t clipboard_x11_get_text(char * buffer, int32_t buffer_size);
extern "C" int32_t clipboard_x11_set_text(char * text);
extern "C" int32_t clipboard_x11_set_html(char * html, char * fallback_text);
extern "C" int32_t clipboard_x11_set_image(char * buffer, int32_t buffer_size);
#endif //ESPANSO_X11_CLIPBOARD_H

View File

@ -0,0 +1,139 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::bail;
use log::error;
use std::io::Write;
use std::process::{Command, Stdio};
use crate::{Clipboard, ClipboardOperationOptions};
pub struct XClipClipboard {
is_xclip_available: bool,
}
impl XClipClipboard {
pub fn new() -> Self {
let command = Command::new("xclip").arg("-h").output();
let is_xclip_available = command
.map(|output| output.status.success())
.unwrap_or(false);
Self { is_xclip_available }
}
}
impl Clipboard for XClipClipboard {
fn get_text(&self, _: &ClipboardOperationOptions) -> Option<String> {
if !self.is_xclip_available {
error!("attempted to use XClipClipboard, but `xclip` command can't be called");
return None;
}
match Command::new("xclip").args(&["-o", "-sel", "clip"]).output() {
Ok(output) => {
if output.status.success() {
let s = String::from_utf8_lossy(&output.stdout);
return Some(s.to_string());
}
}
Err(error) => {
error!("xclip reported an error: {}", error);
}
}
None
}
fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> {
if !self.is_xclip_available {
bail!("attempted to use XClipClipboard, but `xclip` command can't be called");
}
let mut child = Command::new("xclip")
.args(&["-sel", "clip"])
.stdin(Stdio::piped())
.spawn()?;
let stdin = child.stdin.as_mut();
if let Some(input) = stdin {
input.write_all(text.as_bytes())?;
child.wait()?;
}
Ok(())
}
fn set_image(
&self,
image_path: &std::path::Path,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if !self.is_xclip_available {
bail!("attempted to use XClipClipboard, but `xclip` command can't be called");
}
let extension = image_path.extension();
let mime = match extension {
Some(ext) => {
let ext = ext.to_string_lossy().to_lowercase();
match ext.as_ref() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg",
_ => "image/png",
}
}
None => "image/png",
};
let image_path = image_path.to_string_lossy();
Command::new("xclip")
.args(&["-selection", "clipboard", "-t", mime, "-i", &image_path])
.spawn()?;
Ok(())
}
fn set_html(
&self,
html: &str,
_: Option<&str>,
_: &ClipboardOperationOptions,
) -> anyhow::Result<()> {
if !self.is_xclip_available {
bail!("attempted to use XClipClipboard, but `xclip` command can't be called");
}
let mut child = Command::new("xclip")
.args(&["-sel", "clip", "-t", "text/html"])
.stdin(Stdio::piped())
.spawn()?;
let stdin = child.stdin.as_mut();
if let Some(input) = stdin {
input.write_all(html.as_bytes())?;
child.wait()?;
}
Ok(())
}
}

25
espanso-config/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "espanso-config"
version = "0.1.0"
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
edition = "2018"
[dependencies]
log = "0.4.14"
anyhow = "1.0.38"
thiserror = "1.0.23"
serde = { version = "1.0.123", features = ["derive"] }
serde_yaml = "0.8.17"
glob = "0.3.0"
regex = "1.4.3"
lazy_static = "1.4.0"
dunce = "1.0.1"
walkdir = "2.3.1"
enum-as-inner = "0.3.3"
ordered-float = "2.0"
indoc = "1.0.3"
[dev-dependencies]
tempdir = "0.3.7"
tempfile = "3.2.0"
mockall = "0.9.1"

View File

@ -0,0 +1,25 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
pub(crate) const DEFAULT_CLIPBOARD_THRESHOLD: usize = 100;
pub(crate) const DEFAULT_PRE_PASTE_DELAY: usize = 100;
pub(crate) const DEFAULT_SHORTCUT_EVENT_DELAY: usize = 10;
pub(crate) const DEFAULT_RESTORE_CLIPBOARD_DELAY: usize = 300;
pub(crate) const DEFAULT_POST_FORM_DELAY: usize = 200;
pub(crate) const DEFAULT_POST_SEARCH_DELAY: usize = 200;

View File

@ -0,0 +1,345 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use indoc::formatdoc;
use std::sync::Arc;
use std::{collections::HashSet, path::Path};
use thiserror::Error;
pub(crate) mod default;
mod parse;
mod path;
mod resolve;
pub(crate) mod store;
mod util;
#[cfg(test)]
use mockall::{automock, predicate::*};
use crate::error::NonFatalErrorSet;
#[cfg_attr(test, automock)]
pub trait Config: Send + Sync {
fn id(&self) -> i32;
fn label(&self) -> &str;
fn match_paths(&self) -> &[String];
// The mechanism used to perform the injection. Espanso can either
// inject text by simulating keypresses (Inject backend) or
// by using the clipboard (Clipboard backend). Both of them have pros
// and cons, so the "Auto" backend is used by default to automatically
// choose the most appropriate one based on the situation.
// If for whatever reason the Auto backend is not appropriate, you
// can change this option to override it.
fn backend(&self) -> Backend;
// If false, espanso will be disabled for the current configuration.
// This option can be used to selectively disable espanso when
// using a specific application (by creating an app-specific config).
fn enable(&self) -> bool;
// Number of chars after which a match is injected with the clipboard
// backend instead of the default one. This is done for efficiency
// reasons, as injecting a long match through separate events becomes
// slow for long strings.
fn clipboard_threshold(&self) -> usize;
// Delay (in ms) that espanso should wait to trigger the paste shortcut
// after copying the content in the clipboard. This is needed because
// if we trigger a "paste" shortcut before the content is actually
// copied in the clipboard, the operation will fail.
fn pre_paste_delay(&self) -> usize;
// Number of milliseconds between keystrokes when simulating the Paste shortcut
// For example: CTRL + (wait 5ms) + V + (wait 5ms) + release V + (wait 5ms) + release CTRL
// This is needed as sometimes (for example on macOS), without a delay some keystrokes
// were not registered correctly
fn paste_shortcut_event_delay(&self) -> usize;
// Customize the keyboard shortcut used to paste an expansion.
// This should follow this format: CTRL+SHIFT+V
fn paste_shortcut(&self) -> Option<String>;
// NOTE: This is only relevant on Linux under X11 environments
// Switch to a slower (but sometimes more supported) way of injecting
// key events based on XTestFakeKeyEvent instead of XSendEvent.
// From my experiements, disabling fast inject becomes particularly slow when
// using the Gnome desktop environment.
fn disable_x11_fast_inject(&self) -> bool;
// Defines the key that disables/enables espanso when double pressed
fn toggle_key(&self) -> Option<ToggleKey>;
// If true, instructs the daemon process to restart the worker (and refresh
// the configuration) after a configuration file change is detected on disk.
fn auto_restart(&self) -> bool;
// If true, espanso will attempt to preserve the previous clipboard content
// after an expansion has taken place (when using the Clipboard backend).
fn preserve_clipboard(&self) -> bool;
// The number of milliseconds to wait before restoring the previous clipboard
// content after an expansion. This is needed as without this delay, sometimes
// the target application detects the previous clipboard content instead of
// the expansion content.
fn restore_clipboard_delay(&self) -> usize;
// Number of milliseconds between text injection events. Increase if the target
// application is missing some characters.
fn inject_delay(&self) -> Option<usize>;
// Number of milliseconds between key injection events. Increase if the target
// application is missing some key events.
fn key_delay(&self) -> Option<usize>;
// Extra delay to apply when injecting modifiers under the EVDEV backend.
// This is useful on Wayland if espanso is injecting seemingly random
// cased letters, for example "Hi theRE1" instead of "Hi there!".
// Increase if necessary, decrease to speed up the injection.
fn evdev_modifier_delay(&self) -> Option<usize>;
// Chars that when pressed mark the start and end of a word.
// Examples of this are . or ,
fn word_separators(&self) -> Vec<String>;
// Maximum number of backspace presses espanso keeps track of.
// For example, this is needed to correctly expand even if typos
// are typed.
fn backspace_limit(&self) -> usize;
// If false, avoid applying the built-in patches to the current config.
fn apply_patch(&self) -> bool;
// On Wayland, overrides the auto-detected keyboard configuration (RMLVO)
// which is used both for the detection and injection process.
fn keyboard_layout(&self) -> Option<RMLVOConfig>;
// Trigger used to show the Search UI
fn search_trigger(&self) -> Option<String>;
// Hotkey used to trigger the Search UI
fn search_shortcut(&self) -> Option<String>;
// When enabled, espanso automatically "reverts" an expansion if the user
// presses the Backspace key afterwards.
fn undo_backspace(&self) -> bool;
// If false, disable all notifications
fn show_notifications(&self) -> bool;
// If false, avoid showing the espanso icon on the system's tray bar
// Note: currently not working on Linux
fn show_icon(&self) -> bool;
// If false, avoid showing the SecureInput notification on macOS
fn secure_input_notification(&self) -> bool;
// The number of milliseconds to wait after a form has been closed.
// This is useful to let the target application regain focus
// after a form has been closed, otherwise the injection might
// not be targeted to the right application.
fn post_form_delay(&self) -> usize;
// The number of milliseconds to wait after the search bar has been closed.
// This is useful to let the target application regain focus
// after the search bar has been closed, otherwise the injection might
// not be targeted to the right application.
fn post_search_delay(&self) -> usize;
// If enabled, Espanso emulates the Alt Code feature available on Windows
// (keeping ALT pressed and then typing a char code with the numpad).
// This feature is necessary on Windows because the mechanism used by Espanso
// to intercept keystrokes disables the Windows' native Alt code functionality
// as a side effect.
// Because many users relied on this feature, we try to bring it back by emulating it.
fn emulate_alt_codes(&self) -> bool;
// If true, use the `xclip` command to implement the clipboard instead of
// the built-in native module on X11.
fn x11_use_xclip_backend(&self) -> bool;
// If true, use an alternative injection backend based on the `xdotool` library.
// This might improve the situation for certain locales/layouts on X11.
fn x11_use_xdotool_backend(&self) -> bool;
// If true, filter out keyboard events without an explicit HID device source on Windows.
// This is needed to filter out the software-generated events, including
// those from espanso, but might need to be disabled when using some software-level keyboards.
// Disabling this option might conflict with the undo feature.
fn win32_exclude_orphan_events(&self) -> bool;
// The maximum interval (in milliseconds) for which a keyboard layout
// can be cached. If switching often between different layouts, you
// could lower this amount to avoid the "lost detection" effect described
// in this issue: https://github.com/federico-terzi/espanso/issues/745
fn win32_keyboard_layout_cache_interval(&self) -> i64;
fn is_match<'a>(&self, app: &AppProperties<'a>) -> bool;
fn pretty_dump(&self) -> String {
formatdoc! {"
[espanso config: {:?}]
backend: {:?}
enable: {:?}
paste_shortcut: {:?}
inject_delay: {:?}
key_delay: {:?}
apply_patch: {:?}
word_separators: {:?}
preserve_clipboard: {:?}
clipboard_threshold: {:?}
disable_x11_fast_inject: {}
pre_paste_delay: {}
paste_shortcut_event_delay: {}
toggle_key: {:?}
auto_restart: {:?}
restore_clipboard_delay: {:?}
post_form_delay: {:?}
post_search_delay: {:?}
backspace_limit: {}
search_trigger: {:?}
search_shortcut: {:?}
keyboard_layout: {:?}
show_icon: {:?}
show_notifications: {:?}
secure_input_notification: {:?}
x11_use_xclip_backend: {:?}
x11_use_xdotool_backend: {:?}
win32_exclude_orphan_events: {:?}
win32_keyboard_layout_cache_interval: {:?}
match_paths: {:#?}
",
self.label(),
self.backend(),
self.enable(),
self.paste_shortcut(),
self.inject_delay(),
self.key_delay(),
self.apply_patch(),
self.word_separators(),
self.preserve_clipboard(),
self.clipboard_threshold(),
self.disable_x11_fast_inject(),
self.pre_paste_delay(),
self.paste_shortcut_event_delay(),
self.toggle_key(),
self.auto_restart(),
self.restore_clipboard_delay(),
self.post_form_delay(),
self.post_search_delay(),
self.backspace_limit(),
self.search_trigger(),
self.search_shortcut(),
self.keyboard_layout(),
self.show_icon(),
self.show_notifications(),
self.secure_input_notification(),
self.x11_use_xclip_backend(),
self.x11_use_xdotool_backend(),
self.win32_exclude_orphan_events(),
self.win32_keyboard_layout_cache_interval(),
self.match_paths(),
}
}
}
pub trait ConfigStore: Send {
fn default(&self) -> Arc<dyn Config>;
fn active<'a>(&'a self, app: &AppProperties) -> Arc<dyn Config>;
fn configs(&self) -> Vec<Arc<dyn Config>>;
fn get_all_match_paths(&self) -> HashSet<String>;
}
pub struct AppProperties<'a> {
pub title: Option<&'a str>,
pub class: Option<&'a str>,
pub exec: Option<&'a str>,
}
#[derive(Debug, Copy, Clone)]
pub enum Backend {
Inject,
Clipboard,
Auto,
}
#[derive(Debug, Copy, Clone)]
pub enum ToggleKey {
Ctrl,
Meta,
Alt,
Shift,
RightCtrl,
RightAlt,
RightShift,
RightMeta,
LeftCtrl,
LeftAlt,
LeftShift,
LeftMeta,
}
#[derive(Debug, Clone, Default)]
pub struct RMLVOConfig {
pub rules: Option<String>,
pub model: Option<String>,
pub layout: Option<String>,
pub variant: Option<String>,
pub options: Option<String>,
}
impl std::fmt::Display for RMLVOConfig {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"[R={}, M={}, L={}, V={}, O={}]",
self.rules.as_deref().unwrap_or_default(),
self.model.as_deref().unwrap_or_default(),
self.layout.as_deref().unwrap_or_default(),
self.variant.as_deref().unwrap_or_default(),
self.options.as_deref().unwrap_or_default(),
)
}
}
pub fn load_store(config_dir: &Path) -> Result<(impl ConfigStore, Vec<NonFatalErrorSet>)> {
store::DefaultConfigStore::load(config_dir)
}
#[derive(Error, Debug)]
pub enum ConfigStoreError {
#[error("invalid config directory")]
InvalidConfigDir(),
#[error("missing default.yml config")]
MissingDefault(),
#[error("io error")]
IOError(#[from] std::io::Error),
}

View File

@ -0,0 +1,91 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use std::{collections::BTreeMap, convert::TryInto, path::Path};
use thiserror::Error;
mod yaml;
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct ParsedConfig {
pub label: Option<String>,
pub backend: Option<String>,
pub enable: Option<bool>,
pub clipboard_threshold: Option<usize>,
pub auto_restart: Option<bool>,
pub preserve_clipboard: Option<bool>,
pub toggle_key: Option<String>,
pub paste_shortcut: Option<String>,
pub disable_x11_fast_inject: Option<bool>,
pub word_separators: Option<Vec<String>>,
pub backspace_limit: Option<usize>,
pub apply_patch: Option<bool>,
pub search_trigger: Option<String>,
pub search_shortcut: Option<String>,
pub undo_backspace: Option<bool>,
pub show_notifications: Option<bool>,
pub show_icon: Option<bool>,
pub secure_input_notification: Option<bool>,
pub post_form_delay: Option<usize>,
pub post_search_delay: Option<usize>,
pub emulate_alt_codes: Option<bool>,
pub win32_exclude_orphan_events: Option<bool>,
pub win32_keyboard_layout_cache_interval: Option<i64>,
pub x11_use_xclip_backend: Option<bool>,
pub x11_use_xdotool_backend: Option<bool>,
pub pre_paste_delay: Option<usize>,
pub restore_clipboard_delay: Option<usize>,
pub paste_shortcut_event_delay: Option<usize>,
pub inject_delay: Option<usize>,
pub key_delay: Option<usize>,
pub keyboard_layout: Option<BTreeMap<String, String>>,
pub evdev_modifier_delay: Option<usize>,
// Includes
pub includes: Option<Vec<String>>,
pub excludes: Option<Vec<String>>,
pub extra_includes: Option<Vec<String>>,
pub extra_excludes: Option<Vec<String>>,
pub use_standard_includes: Option<bool>,
// Filters
pub filter_title: Option<String>,
pub filter_class: Option<String>,
pub filter_exec: Option<String>,
pub filter_os: Option<String>,
}
impl ParsedConfig {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
match yaml::YAMLConfig::parse_from_str(&content) {
Ok(config) => Ok(config.try_into()?),
Err(err) => Err(ParsedConfigError::LoadFailed(err).into()),
}
}
}
#[derive(Error, Debug)]
pub enum ParsedConfigError {
#[error("can't load config `{0}`")]
LoadFailed(#[from] anyhow::Error),
}

View File

@ -0,0 +1,365 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::convert::TryFrom;
use crate::util::is_yaml_empty;
use super::ParsedConfig;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct YAMLConfig {
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub backend: Option<String>,
#[serde(default)]
pub enable: Option<bool>,
#[serde(default)]
pub clipboard_threshold: Option<usize>,
#[serde(default)]
pub pre_paste_delay: Option<usize>,
#[serde(default)]
pub toggle_key: Option<String>,
#[serde(default)]
pub auto_restart: Option<bool>,
#[serde(default)]
pub preserve_clipboard: Option<bool>,
#[serde(default)]
pub restore_clipboard_delay: Option<usize>,
#[serde(default)]
pub paste_shortcut_event_delay: Option<usize>,
#[serde(default)]
pub paste_shortcut: Option<String>,
#[serde(default)]
pub disable_x11_fast_inject: Option<bool>,
#[serde(default)]
pub inject_delay: Option<usize>,
#[serde(default)]
pub key_delay: Option<usize>,
#[serde(default)]
pub backspace_delay: Option<usize>,
#[serde(default)]
pub evdev_modifier_delay: Option<usize>,
#[serde(default)]
pub word_separators: Option<Vec<String>>,
#[serde(default)]
pub backspace_limit: Option<usize>,
#[serde(default)]
pub apply_patch: Option<bool>,
#[serde(default)]
pub keyboard_layout: Option<Mapping>,
#[serde(default)]
pub search_trigger: Option<String>,
#[serde(default)]
pub search_shortcut: Option<String>,
#[serde(default)]
pub undo_backspace: Option<bool>,
#[serde(default)]
pub show_notifications: Option<bool>,
#[serde(default)]
pub show_icon: Option<bool>,
#[serde(default)]
pub post_form_delay: Option<usize>,
#[serde(default)]
pub post_search_delay: Option<usize>,
#[serde(default)]
pub secure_input_notification: Option<bool>,
#[serde(default)]
pub emulate_alt_codes: Option<bool>,
#[serde(default)]
pub win32_exclude_orphan_events: Option<bool>,
#[serde(default)]
pub win32_keyboard_layout_cache_interval: Option<i64>,
#[serde(default)]
pub x11_use_xclip_backend: Option<bool>,
#[serde(default)]
pub x11_use_xdotool_backend: Option<bool>,
// Include/Exclude
#[serde(default)]
pub includes: Option<Vec<String>>,
#[serde(default)]
pub excludes: Option<Vec<String>>,
#[serde(default)]
pub extra_includes: Option<Vec<String>>,
#[serde(default)]
pub extra_excludes: Option<Vec<String>>,
#[serde(default)]
pub use_standard_includes: Option<bool>,
// Filters
#[serde(default)]
pub filter_title: Option<String>,
#[serde(default)]
pub filter_class: Option<String>,
#[serde(default)]
pub filter_exec: Option<String>,
#[serde(default)]
pub filter_os: Option<String>,
}
impl YAMLConfig {
pub fn parse_from_str(yaml: &str) -> Result<Self> {
// Because an empty string is not valid YAML but we want to support it anyway
if is_yaml_empty(yaml) {
return Ok(serde_yaml::from_str(
"arbitrary_field_that_will_not_block_the_parser: true",
)?);
}
Ok(serde_yaml::from_str(yaml)?)
}
}
impl TryFrom<YAMLConfig> for ParsedConfig {
type Error = anyhow::Error;
fn try_from(yaml_config: YAMLConfig) -> Result<Self, Self::Error> {
Ok(Self {
label: yaml_config.label,
backend: yaml_config.backend,
enable: yaml_config.enable,
clipboard_threshold: yaml_config.clipboard_threshold,
auto_restart: yaml_config.auto_restart,
toggle_key: yaml_config.toggle_key,
preserve_clipboard: yaml_config.preserve_clipboard,
paste_shortcut: yaml_config.paste_shortcut,
disable_x11_fast_inject: yaml_config.disable_x11_fast_inject,
inject_delay: yaml_config.inject_delay,
key_delay: yaml_config.key_delay.or(yaml_config.backspace_delay),
evdev_modifier_delay: yaml_config.evdev_modifier_delay,
word_separators: yaml_config.word_separators,
backspace_limit: yaml_config.backspace_limit,
apply_patch: yaml_config.apply_patch,
keyboard_layout: yaml_config.keyboard_layout.map(|mapping| {
mapping
.into_iter()
.filter_map(|(key, value)| {
if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
Some((key.to_string(), value.to_string()))
} else {
None
}
})
.collect()
}),
search_trigger: yaml_config.search_trigger,
search_shortcut: yaml_config.search_shortcut,
undo_backspace: yaml_config.undo_backspace,
show_icon: yaml_config.show_icon,
show_notifications: yaml_config.show_notifications,
secure_input_notification: yaml_config.secure_input_notification,
pre_paste_delay: yaml_config.pre_paste_delay,
restore_clipboard_delay: yaml_config.restore_clipboard_delay,
paste_shortcut_event_delay: yaml_config.paste_shortcut_event_delay,
post_form_delay: yaml_config.post_form_delay,
post_search_delay: yaml_config.post_search_delay,
emulate_alt_codes: yaml_config.emulate_alt_codes,
win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval,
x11_use_xclip_backend: yaml_config.x11_use_xclip_backend,
x11_use_xdotool_backend: yaml_config.x11_use_xdotool_backend,
use_standard_includes: yaml_config.use_standard_includes,
includes: yaml_config.includes,
extra_includes: yaml_config.extra_includes,
excludes: yaml_config.excludes,
extra_excludes: yaml_config.extra_excludes,
filter_class: yaml_config.filter_class,
filter_exec: yaml_config.filter_exec,
filter_os: yaml_config.filter_os,
filter_title: yaml_config.filter_title,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{collections::BTreeMap, convert::TryInto};
#[test]
fn conversion_to_parsed_config_works_correctly() {
let config = YAMLConfig::parse_from_str(
r#"
label: "test"
backend: clipboard
enable: false
clipboard_threshold: 200
pre_paste_delay: 300
toggle_key: CTRL
auto_restart: false
preserve_clipboard: false
restore_clipboard_delay: 400
paste_shortcut: CTRL+ALT+V
paste_shortcut_event_delay: 10
disable_x11_fast_inject: true
inject_delay: 10
key_delay: 20
backspace_delay: 30
evdev_modifier_delay: 40
word_separators: ["'", "."]
backspace_limit: 10
apply_patch: false
keyboard_layout:
rules: test_rule
model: test_model
layout: test_layout
variant: test_variant
options: test_options
search_trigger: "search"
search_shortcut: "CTRL+SPACE"
undo_backspace: false
show_icon: false
show_notifications: false
secure_input_notification: false
post_form_delay: 300
post_search_delay: 400
emulate_alt_codes: true
win32_exclude_orphan_events: false
win32_keyboard_layout_cache_interval: 300
x11_use_xclip_backend: true
x11_use_xdotool_backend: true
use_standard_includes: true
includes: ["test1"]
extra_includes: ["test2"]
excludes: ["test3"]
extra_excludes: ["test4"]
filter_class: "test5"
filter_exec: "test6"
filter_os: "test7"
filter_title: "test8"
"#,
)
.unwrap();
let parsed_config: ParsedConfig = config.try_into().unwrap();
let keyboard_layout: BTreeMap<String, String> = vec![
("rules".to_string(), "test_rule".to_string()),
("model".to_string(), "test_model".to_string()),
("layout".to_string(), "test_layout".to_string()),
("variant".to_string(), "test_variant".to_string()),
("options".to_string(), "test_options".to_string()),
]
.into_iter()
.collect();
assert_eq!(
parsed_config,
ParsedConfig {
label: Some("test".to_string()),
backend: Some("clipboard".to_string()),
enable: Some(false),
clipboard_threshold: Some(200),
auto_restart: Some(false),
preserve_clipboard: Some(false),
restore_clipboard_delay: Some(400),
paste_shortcut: Some("CTRL+ALT+V".to_string()),
paste_shortcut_event_delay: Some(10),
disable_x11_fast_inject: Some(true),
inject_delay: Some(10),
key_delay: Some(20),
backspace_limit: Some(10),
apply_patch: Some(false),
keyboard_layout: Some(keyboard_layout),
search_trigger: Some("search".to_owned()),
search_shortcut: Some("CTRL+SPACE".to_owned()),
undo_backspace: Some(false),
show_icon: Some(false),
show_notifications: Some(false),
secure_input_notification: Some(false),
emulate_alt_codes: Some(true),
post_form_delay: Some(300),
post_search_delay: Some(400),
win32_exclude_orphan_events: Some(false),
win32_keyboard_layout_cache_interval: Some(300),
x11_use_xclip_backend: Some(true),
x11_use_xdotool_backend: Some(true),
pre_paste_delay: Some(300),
evdev_modifier_delay: Some(40),
toggle_key: Some("CTRL".to_string()),
word_separators: Some(vec!["'".to_owned(), ".".to_owned()]),
use_standard_includes: Some(true),
includes: Some(vec!["test1".to_string()]),
extra_includes: Some(vec!["test2".to_string()]),
excludes: Some(vec!["test3".to_string()]),
extra_excludes: Some(vec!["test4".to_string()]),
filter_class: Some("test5".to_string()),
filter_exec: Some("test6".to_string()),
filter_os: Some("test7".to_string()),
filter_title: Some("test8".to_string()),
}
)
}
}

View File

@ -0,0 +1,182 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{collections::HashSet, path::Path};
use glob::glob;
use log::error;
use regex::Regex;
lazy_static! {
static ref ABSOLUTE_PATH: Regex = Regex::new(r"(?m)^([a-zA-Z]:\\|/).*$").unwrap();
}
pub fn calculate_paths<'a>(
base_dir: &Path,
glob_patterns: impl Iterator<Item = &'a String>,
) -> HashSet<String> {
let mut path_set = HashSet::new();
for glob_pattern in glob_patterns {
// Handle relative and absolute paths appropriately
let pattern = if ABSOLUTE_PATH.is_match(glob_pattern) {
glob_pattern.clone()
} else {
format!("{}/{}", base_dir.to_string_lossy(), glob_pattern)
};
let entries = glob(&pattern);
match entries {
Ok(paths) => {
for path in paths {
match path {
Ok(path) => {
// Canonicalize the path
match dunce::canonicalize(&path) {
Ok(canonical_path) => {
path_set.insert(canonical_path.to_string_lossy().to_string());
}
Err(err) => {
error!(
"unable to canonicalize path from glob: {:?}, with error: {}",
path, err
);
}
}
}
Err(err) => error!(
"glob error when processing pattern: {}, with error: {}",
glob_pattern, err
),
}
}
}
Err(err) => {
error!(
"unable to calculate glob from pattern: {}, with error: {}",
glob_pattern, err
);
}
}
}
path_set
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::util::tests::use_test_directory;
use std::fs::create_dir_all;
#[test]
fn calculate_paths_relative_paths() {
use_test_directory(|base, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let result = calculate_paths(
base,
vec![
"**/*.yml".to_string(),
"match/sub/*.yml".to_string(),
// Invalid path
"invalid".to_string(),
]
.iter(),
);
let mut expected = HashSet::new();
expected.insert(base_file.to_string_lossy().to_string());
expected.insert(another_file.to_string_lossy().to_string());
expected.insert(under_file.to_string_lossy().to_string());
expected.insert(sub_file.to_string_lossy().to_string());
assert_eq!(result, expected);
});
}
#[test]
fn calculate_paths_relative_with_parent_modifier() {
use_test_directory(|base, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let result = calculate_paths(base, vec!["match/sub/../sub/*.yml".to_string()].iter());
let mut expected = HashSet::new();
expected.insert(sub_file.to_string_lossy().to_string());
assert_eq!(result, expected);
});
}
#[test]
fn calculate_paths_absolute_paths() {
use_test_directory(|base, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let result = calculate_paths(
base,
vec![
format!("{}/**/*.yml", base.to_string_lossy()),
format!("{}/match/sub/*.yml", base.to_string_lossy()),
// Invalid path
"invalid".to_string(),
]
.iter(),
);
let mut expected = HashSet::new();
expected.insert(base_file.to_string_lossy().to_string());
expected.insert(another_file.to_string_lossy().to_string());
expected.insert(under_file.to_string_lossy().to_string());
expected.insert(sub_file.to_string_lossy().to_string());
assert_eq!(result, expected);
});
}
}

View File

@ -0,0 +1,984 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use super::{
default::{
DEFAULT_CLIPBOARD_THRESHOLD, DEFAULT_POST_FORM_DELAY, DEFAULT_POST_SEARCH_DELAY,
DEFAULT_PRE_PASTE_DELAY, DEFAULT_RESTORE_CLIPBOARD_DELAY, DEFAULT_SHORTCUT_EVENT_DELAY,
},
parse::ParsedConfig,
path::calculate_paths,
util::os_matches,
AppProperties, Backend, Config, RMLVOConfig, ToggleKey,
};
use crate::{counter::next_id, merge};
use anyhow::Result;
use log::error;
use regex::Regex;
use std::path::PathBuf;
use std::{collections::HashSet, path::Path};
use thiserror::Error;
const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"];
#[derive(Debug, Clone, Default)]
pub(crate) struct ResolvedConfig {
parsed: ParsedConfig,
source_path: Option<PathBuf>,
// Generated properties
id: i32,
match_paths: Vec<String>,
filter_title: Option<Regex>,
filter_class: Option<Regex>,
filter_exec: Option<Regex>,
}
impl Config for ResolvedConfig {
fn id(&self) -> i32 {
self.id
}
fn label(&self) -> &str {
if let Some(label) = self.parsed.label.as_deref() {
return label;
}
if let Some(source_path) = self.source_path.as_ref() {
if let Some(source_path) = source_path.to_str() {
return source_path;
}
}
"none"
}
fn match_paths(&self) -> &[String] {
&self.match_paths
}
fn is_match(&self, app: &AppProperties) -> bool {
if self.parsed.filter_os.is_none()
&& self.parsed.filter_title.is_none()
&& self.parsed.filter_exec.is_none()
&& self.parsed.filter_class.is_none()
{
return false;
}
let is_os_match = if let Some(filter_os) = self.parsed.filter_os.as_deref() {
os_matches(filter_os)
} else {
true
};
let is_title_match = if let Some(title_regex) = self.filter_title.as_ref() {
if let Some(title) = app.title {
title_regex.is_match(title)
} else {
false
}
} else {
true
};
let is_exec_match = if let Some(exec_regex) = self.filter_exec.as_ref() {
if let Some(exec) = app.exec {
exec_regex.is_match(exec)
} else {
false
}
} else {
true
};
let is_class_match = if let Some(class_regex) = self.filter_class.as_ref() {
if let Some(class) = app.class {
class_regex.is_match(class)
} else {
false
}
} else {
true
};
// All the filters that have been specified must be true to define a match
is_os_match && is_exec_match && is_title_match && is_class_match
}
fn backend(&self) -> Backend {
// TODO: test
match self
.parsed
.backend
.as_deref()
.map(|b| b.to_lowercase())
.as_deref()
{
Some("clipboard") => Backend::Clipboard,
Some("inject") => Backend::Inject,
Some("auto") => Backend::Auto,
None => Backend::Auto,
err => {
error!("invalid backend specified {:?}, falling back to Auto", err);
Backend::Auto
}
}
}
fn enable(&self) -> bool {
self.parsed.enable.unwrap_or(true)
}
fn clipboard_threshold(&self) -> usize {
self
.parsed
.clipboard_threshold
.unwrap_or(DEFAULT_CLIPBOARD_THRESHOLD)
}
fn auto_restart(&self) -> bool {
self.parsed.auto_restart.unwrap_or(true)
}
fn pre_paste_delay(&self) -> usize {
self
.parsed
.pre_paste_delay
.unwrap_or(DEFAULT_PRE_PASTE_DELAY)
}
fn toggle_key(&self) -> Option<ToggleKey> {
// TODO: test
match self
.parsed
.toggle_key
.as_deref()
.map(|key| key.to_lowercase())
.as_deref()
{
Some("ctrl") => Some(ToggleKey::Ctrl),
Some("alt") => Some(ToggleKey::Alt),
Some("shift") => Some(ToggleKey::Shift),
Some("meta") | Some("cmd") => Some(ToggleKey::Meta),
Some("right_ctrl") => Some(ToggleKey::RightCtrl),
Some("right_alt") => Some(ToggleKey::RightAlt),
Some("right_shift") => Some(ToggleKey::RightShift),
Some("right_meta") | Some("right_cmd") => Some(ToggleKey::RightMeta),
Some("left_ctrl") => Some(ToggleKey::LeftCtrl),
Some("left_alt") => Some(ToggleKey::LeftAlt),
Some("left_shift") => Some(ToggleKey::LeftShift),
Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta),
Some("off") => None,
None => None,
err => {
error!("invalid toggle_key specified {:?}", err);
None
}
}
}
fn preserve_clipboard(&self) -> bool {
self.parsed.preserve_clipboard.unwrap_or(true)
}
fn restore_clipboard_delay(&self) -> usize {
self
.parsed
.restore_clipboard_delay
.unwrap_or(DEFAULT_RESTORE_CLIPBOARD_DELAY)
}
fn paste_shortcut_event_delay(&self) -> usize {
self
.parsed
.paste_shortcut_event_delay
.unwrap_or(DEFAULT_SHORTCUT_EVENT_DELAY)
}
fn paste_shortcut(&self) -> Option<String> {
self.parsed.paste_shortcut.clone()
}
fn disable_x11_fast_inject(&self) -> bool {
self.parsed.disable_x11_fast_inject.unwrap_or(false)
}
fn inject_delay(&self) -> Option<usize> {
self.parsed.inject_delay
}
fn key_delay(&self) -> Option<usize> {
self.parsed.key_delay
}
fn word_separators(&self) -> Vec<String> {
self.parsed.word_separators.clone().unwrap_or_else(|| {
vec![
" ".to_string(),
",".to_string(),
".".to_string(),
"?".to_string(),
"!".to_string(),
"\r".to_string(),
"\n".to_string(),
(22u8 as char).to_string(),
]
})
}
fn backspace_limit(&self) -> usize {
self.parsed.backspace_limit.unwrap_or(5)
}
fn apply_patch(&self) -> bool {
self.parsed.apply_patch.unwrap_or(true)
}
fn keyboard_layout(&self) -> Option<RMLVOConfig> {
self
.parsed
.keyboard_layout
.as_ref()
.map(|layout| RMLVOConfig {
rules: layout.get("rules").map(String::from),
model: layout.get("model").map(String::from),
layout: layout.get("layout").map(String::from),
variant: layout.get("variant").map(String::from),
options: layout.get("options").map(String::from),
})
}
fn search_trigger(&self) -> Option<String> {
match self.parsed.search_trigger.as_deref() {
Some("OFF") | Some("off") => None,
Some(x) => Some(x.to_string()),
None => None,
}
}
fn search_shortcut(&self) -> Option<String> {
match self.parsed.search_shortcut.as_deref() {
Some("OFF") | Some("off") => None,
Some(x) => Some(x.to_string()),
None => Some("ALT+SPACE".to_string()),
}
}
fn undo_backspace(&self) -> bool {
self.parsed.undo_backspace.unwrap_or(true)
}
fn show_icon(&self) -> bool {
self.parsed.show_icon.unwrap_or(true)
}
fn show_notifications(&self) -> bool {
self.parsed.show_notifications.unwrap_or(true)
}
fn secure_input_notification(&self) -> bool {
self.parsed.secure_input_notification.unwrap_or(true)
}
fn emulate_alt_codes(&self) -> bool {
self.parsed.emulate_alt_codes.unwrap_or(false)
}
fn post_form_delay(&self) -> usize {
self
.parsed
.post_form_delay
.unwrap_or(DEFAULT_POST_FORM_DELAY)
}
fn post_search_delay(&self) -> usize {
self
.parsed
.post_search_delay
.unwrap_or(DEFAULT_POST_SEARCH_DELAY)
}
fn win32_exclude_orphan_events(&self) -> bool {
self.parsed.win32_exclude_orphan_events.unwrap_or(true)
}
fn evdev_modifier_delay(&self) -> Option<usize> {
self.parsed.evdev_modifier_delay
}
fn win32_keyboard_layout_cache_interval(&self) -> i64 {
self
.parsed
.win32_keyboard_layout_cache_interval
.unwrap_or(2000)
}
fn x11_use_xclip_backend(&self) -> bool {
self.parsed.x11_use_xclip_backend.unwrap_or(false)
}
fn x11_use_xdotool_backend(&self) -> bool {
self.parsed.x11_use_xdotool_backend.unwrap_or(false)
}
}
impl ResolvedConfig {
pub fn load(path: &Path, parent: Option<&Self>) -> Result<Self> {
let mut config = ParsedConfig::load(path)?;
// Merge with parent config if present
if let Some(parent) = parent {
Self::merge_parsed(&mut config, &parent.parsed);
}
// Extract the base directory
let base_dir = path
.parent()
.ok_or_else(ResolveError::ParentResolveFailed)?;
let match_paths = Self::generate_match_paths(&config, base_dir)
.into_iter()
.collect();
let filter_title = if let Some(filter_title) = config.filter_title.as_deref() {
Some(Regex::new(filter_title)?)
} else {
None
};
let filter_class = if let Some(filter_class) = config.filter_class.as_deref() {
Some(Regex::new(filter_class)?)
} else {
None
};
let filter_exec = if let Some(filter_exec) = config.filter_exec.as_deref() {
Some(Regex::new(filter_exec)?)
} else {
None
};
Ok(Self {
parsed: config,
source_path: Some(path.to_owned()),
id: next_id(),
match_paths,
filter_title,
filter_class,
filter_exec,
})
}
fn merge_parsed(child: &mut ParsedConfig, parent: &ParsedConfig) {
// Override the None fields with the parent's value
merge!(
ParsedConfig,
child,
parent,
// Fields
label,
backend,
enable,
clipboard_threshold,
auto_restart,
pre_paste_delay,
preserve_clipboard,
restore_clipboard_delay,
paste_shortcut,
apply_patch,
paste_shortcut_event_delay,
disable_x11_fast_inject,
toggle_key,
inject_delay,
key_delay,
evdev_modifier_delay,
word_separators,
backspace_limit,
keyboard_layout,
search_trigger,
search_shortcut,
undo_backspace,
show_icon,
show_notifications,
secure_input_notification,
emulate_alt_codes,
post_form_delay,
post_search_delay,
win32_exclude_orphan_events,
win32_keyboard_layout_cache_interval,
x11_use_xclip_backend,
x11_use_xdotool_backend,
includes,
excludes,
extra_includes,
extra_excludes,
use_standard_includes,
filter_title,
filter_class,
filter_exec,
filter_os
);
}
fn aggregate_includes(config: &ParsedConfig) -> HashSet<String> {
let mut includes = HashSet::new();
if config.use_standard_includes.is_none() || config.use_standard_includes.unwrap() {
STANDARD_INCLUDES.iter().for_each(|include| {
includes.insert(include.to_string());
})
}
if let Some(yaml_includes) = config.includes.as_ref() {
yaml_includes.iter().for_each(|include| {
includes.insert(include.to_string());
})
}
if let Some(extra_includes) = config.extra_includes.as_ref() {
extra_includes.iter().for_each(|include| {
includes.insert(include.to_string());
})
}
includes
}
fn aggregate_excludes(config: &ParsedConfig) -> HashSet<String> {
let mut excludes = HashSet::new();
if let Some(yaml_excludes) = config.excludes.as_ref() {
yaml_excludes.iter().for_each(|exclude| {
excludes.insert(exclude.to_string());
})
}
if let Some(extra_excludes) = config.extra_excludes.as_ref() {
extra_excludes.iter().for_each(|exclude| {
excludes.insert(exclude.to_string());
})
}
excludes
}
fn generate_match_paths(config: &ParsedConfig, base_dir: &Path) -> HashSet<String> {
let includes = Self::aggregate_includes(config);
let excludes = Self::aggregate_excludes(config);
// Extract the paths
let exclude_paths = calculate_paths(base_dir, excludes.iter());
let include_paths = calculate_paths(base_dir, includes.iter());
include_paths
.difference(&exclude_paths)
.cloned()
.collect::<HashSet<_>>()
}
}
#[derive(Error, Debug)]
pub enum ResolveError {
#[error("unable to resolve parent path")]
ParentResolveFailed(),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::tests::use_test_directory;
use std::fs::create_dir_all;
#[test]
fn aggregate_includes_empty_config() {
assert_eq!(
ResolvedConfig::aggregate_includes(&ParsedConfig {
..Default::default()
}),
vec!["../match/**/[!_]*.yml".to_string(),]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn aggregate_includes_no_standard() {
assert_eq!(
ResolvedConfig::aggregate_includes(&ParsedConfig {
use_standard_includes: Some(false),
..Default::default()
}),
HashSet::new()
);
}
#[test]
fn aggregate_includes_custom_includes() {
assert_eq!(
ResolvedConfig::aggregate_includes(&ParsedConfig {
includes: Some(vec!["custom/*.yml".to_string()]),
..Default::default()
}),
vec![
"../match/**/[!_]*.yml".to_string(),
"custom/*.yml".to_string()
]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn aggregate_includes_extra_includes() {
assert_eq!(
ResolvedConfig::aggregate_includes(&ParsedConfig {
extra_includes: Some(vec!["custom/*.yml".to_string()]),
..Default::default()
}),
vec![
"../match/**/[!_]*.yml".to_string(),
"custom/*.yml".to_string()
]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn aggregate_includes_includes_and_extra_includes() {
assert_eq!(
ResolvedConfig::aggregate_includes(&ParsedConfig {
includes: Some(vec!["sub/*.yml".to_string()]),
extra_includes: Some(vec!["custom/*.yml".to_string()]),
..Default::default()
}),
vec![
"../match/**/[!_]*.yml".to_string(),
"custom/*.yml".to_string(),
"sub/*.yml".to_string()
]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn aggregate_excludes_empty_config() {
assert_eq!(
ResolvedConfig::aggregate_excludes(&ParsedConfig {
..Default::default()
})
.len(),
0
);
}
#[test]
fn aggregate_excludes_no_standard() {
assert_eq!(
ResolvedConfig::aggregate_excludes(&ParsedConfig {
use_standard_includes: Some(false),
..Default::default()
}),
HashSet::new()
);
}
#[test]
fn aggregate_excludes_custom_excludes() {
assert_eq!(
ResolvedConfig::aggregate_excludes(&ParsedConfig {
excludes: Some(vec!["custom/*.yml".to_string()]),
..Default::default()
}),
vec!["custom/*.yml".to_string()]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn aggregate_excludes_extra_excludes() {
assert_eq!(
ResolvedConfig::aggregate_excludes(&ParsedConfig {
extra_excludes: Some(vec!["custom/*.yml".to_string()]),
..Default::default()
}),
vec!["custom/*.yml".to_string()]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn aggregate_excludes_excludes_and_extra_excludes() {
assert_eq!(
ResolvedConfig::aggregate_excludes(&ParsedConfig {
excludes: Some(vec!["sub/*.yml".to_string()]),
extra_excludes: Some(vec!["custom/*.yml".to_string()]),
..Default::default()
}),
vec!["custom/*.yml".to_string(), "sub/*.yml".to_string()]
.iter()
.cloned()
.collect::<HashSet<_>>()
);
}
#[test]
fn merge_parent_field_parent_fallback() {
let parent = ParsedConfig {
use_standard_includes: Some(false),
..Default::default()
};
let mut child = ParsedConfig {
..Default::default()
};
assert_eq!(child.use_standard_includes, None);
ResolvedConfig::merge_parsed(&mut child, &parent);
assert_eq!(child.use_standard_includes, Some(false));
}
#[test]
fn merge_parent_field_child_overwrite_parent() {
let parent = ParsedConfig {
use_standard_includes: Some(true),
..Default::default()
};
let mut child = ParsedConfig {
use_standard_includes: Some(false),
..Default::default()
};
assert_eq!(child.use_standard_includes, Some(false));
ResolvedConfig::merge_parsed(&mut child, &parent);
assert_eq!(child.use_standard_includes, Some(false));
}
#[test]
fn match_paths_generated_correctly() {
use_test_directory(|_, match_dir, config_dir| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, "").unwrap();
let config = ResolvedConfig::load(&config_file, None).unwrap();
let mut expected = vec![
base_file.to_string_lossy().to_string(),
another_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
];
expected.sort();
let mut result = config.match_paths().to_vec();
result.sort();
assert_eq!(result, expected.as_slice());
});
}
#[test]
fn match_paths_generated_correctly_with_child_config() {
use_test_directory(|_, match_dir, config_dir| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, "test").unwrap();
let sub_file = sub_dir.join("another.yml");
std::fs::write(&sub_file, "test").unwrap();
let sub_under_file = sub_dir.join("_sub.yml");
std::fs::write(&sub_under_file, "test").unwrap();
// Configs
let parent_file = config_dir.join("parent.yml");
std::fs::write(
&parent_file,
r#"
excludes: ['../**/another.yml']
"#,
)
.unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(
&config_file,
r#"
use_standard_includes: false
excludes: []
includes: ["../match/sub/*.yml"]
"#,
)
.unwrap();
let parent = ResolvedConfig::load(&parent_file, None).unwrap();
let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap();
let mut expected = vec![
sub_file.to_string_lossy().to_string(),
sub_under_file.to_string_lossy().to_string(),
];
expected.sort();
let mut result = child.match_paths().to_vec();
result.sort();
assert_eq!(result, expected.as_slice());
let expected = vec![base_file.to_string_lossy().to_string()];
assert_eq!(parent.match_paths(), expected.as_slice());
});
}
#[test]
fn match_paths_generated_correctly_with_underscore_files() {
use_test_directory(|_, match_dir, config_dir| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, "extra_includes: ['../match/_sub.yml']").unwrap();
let config = ResolvedConfig::load(&config_file, None).unwrap();
let mut expected = vec![
base_file.to_string_lossy().to_string(),
another_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
under_file.to_string_lossy().to_string(),
];
expected.sort();
let mut result = config.match_paths().to_vec();
result.sort();
assert_eq!(result, expected.as_slice());
});
}
fn test_filter_is_match(config: &str, app: &AppProperties) -> bool {
let mut result = false;
let result_ref = &mut result;
use_test_directory(move |_, _, config_dir| {
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, config).unwrap();
let config = ResolvedConfig::load(&config_file, None).unwrap();
*result_ref = config.is_match(app)
});
result
}
#[test]
fn is_match_no_filters() {
assert!(!test_filter_is_match(
"",
&AppProperties {
title: Some("Google"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
}
#[test]
fn is_match_filter_title() {
assert!(test_filter_is_match(
"filter_title: Google",
&AppProperties {
title: Some("Google Mail"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
"filter_title: Google",
&AppProperties {
title: Some("Yahoo"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
"filter_title: Google",
&AppProperties {
title: None,
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
}
#[test]
fn is_match_filter_class() {
assert!(test_filter_is_match(
"filter_class: Chrome",
&AppProperties {
title: Some("Google Mail"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
"filter_class: Chrome",
&AppProperties {
title: Some("Yahoo"),
class: Some("Another"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
"filter_class: Chrome",
&AppProperties {
title: Some("google"),
class: None,
exec: Some("chrome.exe"),
},
));
}
#[test]
fn is_match_filter_exec() {
assert!(test_filter_is_match(
"filter_exec: chrome.exe",
&AppProperties {
title: Some("Google Mail"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
"filter_exec: chrome.exe",
&AppProperties {
title: Some("Yahoo"),
class: Some("Another"),
exec: Some("zoom.exe"),
},
));
assert!(!test_filter_is_match(
"filter_exec: chrome.exe",
&AppProperties {
title: Some("google"),
class: Some("Chrome"),
exec: None,
},
));
}
#[test]
fn is_match_filter_os() {
let (current, another) = if cfg!(target_os = "windows") {
("windows", "macos")
} else if cfg!(target_os = "macos") {
("macos", "windows")
} else if cfg!(target_os = "linux") {
("linux", "macos")
} else {
("invalid", "invalid")
};
assert!(test_filter_is_match(
&format!("filter_os: {}", current),
&AppProperties {
title: Some("Google Mail"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
&format!("filter_os: {}", another),
&AppProperties {
title: Some("Google Mail"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
}
#[test]
fn is_match_multiple_filters() {
assert!(test_filter_is_match(
r#"
filter_exec: chrome.exe
filter_title: "Youtube"
"#,
&AppProperties {
title: Some("Youtube - Broadcast Yourself"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
assert!(!test_filter_is_match(
r#"
filter_exec: chrome.exe
filter_title: "Youtube"
"#,
&AppProperties {
title: Some("Gmail"),
class: Some("Chrome"),
exec: Some("chrome.exe"),
},
));
}
}

View File

@ -0,0 +1,196 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::error::NonFatalErrorSet;
use super::{resolve::ResolvedConfig, Config, ConfigStore, ConfigStoreError};
use anyhow::{Context, Result};
use log::{debug, error};
use std::sync::Arc;
use std::{collections::HashSet, path::Path};
pub(crate) struct DefaultConfigStore {
default: Arc<dyn Config>,
customs: Vec<Arc<dyn Config>>,
}
impl ConfigStore for DefaultConfigStore {
fn default(&self) -> Arc<dyn super::Config> {
Arc::clone(&self.default)
}
fn active<'a>(&'a self, app: &super::AppProperties) -> Arc<dyn super::Config> {
// Find a custom config that matches or fallback to the default one
for custom in self.customs.iter() {
if custom.is_match(app) {
return Arc::clone(custom);
}
}
Arc::clone(&self.default)
}
fn configs(&self) -> Vec<Arc<dyn Config>> {
let mut configs = vec![Arc::clone(&self.default)];
for custom in self.customs.iter() {
configs.push(Arc::clone(custom));
}
configs
}
// TODO: test
fn get_all_match_paths(&self) -> HashSet<String> {
let mut paths = HashSet::new();
paths.extend(self.default().match_paths().iter().cloned());
for custom in self.customs.iter() {
paths.extend(custom.match_paths().iter().cloned());
}
paths
}
}
impl DefaultConfigStore {
pub fn load(config_dir: &Path) -> Result<(Self, Vec<NonFatalErrorSet>)> {
if !config_dir.is_dir() {
return Err(ConfigStoreError::InvalidConfigDir().into());
}
// First get the default.yml file
let default_file = config_dir.join("default.yml");
if !default_file.exists() || !default_file.is_file() {
return Err(ConfigStoreError::MissingDefault().into());
}
let mut non_fatal_errors = Vec::new();
let default = ResolvedConfig::load(&default_file, None)
.context("failed to load default.yml configuration")?;
debug!("loaded default config at path: {:?}", default_file);
// Then the others
let mut customs: Vec<Arc<dyn Config>> = Vec::new();
for entry in std::fs::read_dir(config_dir).map_err(ConfigStoreError::IOError)? {
let entry = entry?;
let config_file = entry.path();
let extension = config_file
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase();
// Additional config files are loaded best-effort
if config_file.is_file()
&& config_file != default_file
&& (extension == "yml" || extension == "yaml")
{
match ResolvedConfig::load(&config_file, Some(&default)) {
Ok(config) => {
customs.push(Arc::new(config));
debug!("loaded config at path: {:?}", config_file);
}
Err(err) => {
error!(
"unable to load config at path: {:?}, with error: {}",
config_file, err
);
non_fatal_errors.push(NonFatalErrorSet::single_error(&config_file, err));
}
}
}
}
Ok((
Self {
default: Arc::new(default),
customs,
},
non_fatal_errors,
))
}
pub fn from_configs(default: Arc<dyn Config>, customs: Vec<Arc<dyn Config>>) -> Result<Self> {
Ok(Self { default, customs })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::MockConfig;
pub fn new_mock(label: &'static str, is_match: bool) -> MockConfig {
let label = label.to_owned();
let mut mock = MockConfig::new();
mock.expect_id().return_const(0);
mock.expect_label().return_const(label);
mock.expect_is_match().return_const(is_match);
mock
}
#[test]
fn config_store_selects_correctly() {
let default = new_mock("default", false);
let custom1 = new_mock("custom1", false);
let custom2 = new_mock("custom2", true);
let store = DefaultConfigStore {
default: Arc::new(default),
customs: vec![Arc::new(custom1), Arc::new(custom2)],
};
assert_eq!(store.default().label(), "default");
assert_eq!(
store
.active(&crate::config::AppProperties {
title: None,
class: None,
exec: None,
})
.label(),
"custom2"
);
}
#[test]
fn config_store_active_fallback_to_default_if_no_match() {
let default = new_mock("default", false);
let custom1 = new_mock("custom1", false);
let custom2 = new_mock("custom2", false);
let store = DefaultConfigStore {
default: Arc::new(default),
customs: vec![Arc::new(custom1), Arc::new(custom2)],
};
assert_eq!(store.default().label(), "default");
assert_eq!(
store
.active(&crate::config::AppProperties {
title: None,
class: None,
exec: None,
})
.label(),
"default"
);
}
}

View File

@ -0,0 +1,80 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#[macro_export]
macro_rules! merge {
( $t:ident, $child:expr, $parent:expr, $( $x:ident ),* ) => {
{
$(
if $child.$x.is_none() {
$child.$x = $parent.$x.clone();
}
)*
// Build a temporary object to verify that all fields
// are being used at compile time
$t {
$(
$x: None,
)*
};
}
};
}
pub fn os_matches(os: &str) -> bool {
match os {
"macos" => cfg!(target_os = "macos"),
"windows" => cfg!(target_os = "windows"),
"linux" => cfg!(target_os = "linux"),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_os = "linux")]
fn os_matches_linux() {
assert!(os_matches("linux"));
assert!(!os_matches("windows"));
assert!(!os_matches("macos"));
assert!(!os_matches("invalid"));
}
#[test]
#[cfg(target_os = "macos")]
fn os_matches_macos() {
assert!(os_matches("macos"));
assert!(!os_matches("windows"));
assert!(!os_matches("linux"));
assert!(!os_matches("invalid"));
}
#[test]
#[cfg(target_os = "windows")]
fn os_matches_windows() {
assert!(os_matches("windows"));
assert!(!os_matches("macos"));
assert!(!os_matches("linux"));
assert!(!os_matches("invalid"));
}
}

View File

@ -0,0 +1,34 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::sync::atomic::{AtomicI32, Ordering};
thread_local! {
// TODO: if thread local, we probably don't need an atomic
static STRUCT_COUNTER: AtomicI32 = AtomicI32::new(0);
}
pub type StructId = i32;
/// Some structs need a unique id.
/// In order to generate it, we use an atomic static variable
/// that is incremented for each struct.
pub fn next_id() -> StructId {
STRUCT_COUNTER.with(|count| count.fetch_add(1, Ordering::SeqCst))
}

View File

@ -0,0 +1,71 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Error;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct NonFatalErrorSet {
pub file: PathBuf,
pub errors: Vec<ErrorRecord>,
}
impl NonFatalErrorSet {
pub fn new(file: &Path, non_fatal_errors: Vec<ErrorRecord>) -> Self {
Self {
file: file.to_owned(),
errors: non_fatal_errors,
}
}
pub fn single_error(file: &Path, error: Error) -> Self {
Self {
file: file.to_owned(),
errors: vec![ErrorRecord::error(error)],
}
}
}
#[derive(Debug)]
pub struct ErrorRecord {
pub level: ErrorLevel,
pub error: Error,
}
impl ErrorRecord {
pub fn error(error: Error) -> Self {
Self {
level: ErrorLevel::Error,
error,
}
}
pub fn warn(error: Error) -> Self {
Self {
level: ErrorLevel::Warning,
error,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ErrorLevel {
Error,
Warning,
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,714 @@
/*
* This file is part of espanso.
*
* C title: (), class: (), exec: ()opyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use log::warn;
use regex::Regex;
use std::{collections::HashMap, path::Path, sync::Arc};
use self::config::LegacyConfig;
use crate::matches::{
group::loader::yaml::{
parse::{YAMLMatch, YAMLVariable},
try_convert_into_match, try_convert_into_variable,
},
MatchEffect,
};
use crate::{config::store::DefaultConfigStore, counter::StructId};
use crate::{
config::Config,
config::{AppProperties, ConfigStore},
counter::next_id,
matches::{
store::{MatchSet, MatchStore},
Match, Variable,
},
};
use std::convert::TryInto;
mod config;
mod model;
pub fn load(
base_dir: &Path,
package_dir: &Path,
) -> Result<(Box<dyn ConfigStore>, Box<dyn MatchStore>)> {
let config_set = config::LegacyConfigSet::load(base_dir, package_dir)?;
let mut match_deduplicate_map = HashMap::new();
let mut var_deduplicate_map = HashMap::new();
let (default_config, mut default_match_group) = split_config(config_set.default);
deduplicate_ids(
&mut default_match_group,
&mut match_deduplicate_map,
&mut var_deduplicate_map,
);
let mut match_groups = HashMap::new();
match_groups.insert("default".to_string(), default_match_group);
let mut custom_configs: Vec<Arc<dyn Config>> = Vec::new();
for custom in config_set.specific {
let (custom_config, mut custom_match_group) = split_config(custom);
deduplicate_ids(
&mut custom_match_group,
&mut match_deduplicate_map,
&mut var_deduplicate_map,
);
match_groups.insert(custom_config.name.clone(), custom_match_group);
custom_configs.push(Arc::new(custom_config));
}
let config_store = DefaultConfigStore::from_configs(Arc::new(default_config), custom_configs)?;
let match_store = LegacyMatchStore::new(match_groups);
Ok((Box::new(config_store), Box::new(match_store)))
}
fn split_config(config: LegacyConfig) -> (LegacyInteropConfig, LegacyMatchGroup) {
let global_vars = config
.global_vars
.iter()
.filter_map(|var| {
let var: YAMLVariable = serde_yaml::from_value(var.clone()).ok()?;
let (var, warnings) = try_convert_into_variable(var, true).ok()?;
warnings.into_iter().for_each(|warning| {
warn!("{}", warning);
});
Some(var)
})
.collect();
let matches = config
.matches
.iter()
.filter_map(|var| {
let m: YAMLMatch = serde_yaml::from_value(var.clone()).ok()?;
let (m, warnings) = try_convert_into_match(m, true).ok()?;
warnings.into_iter().for_each(|warning| {
warn!("{}", warning);
});
Some(m)
})
.collect();
let match_group = LegacyMatchGroup {
matches,
global_vars,
};
let config: LegacyInteropConfig = config.into();
(config, match_group)
}
/// Due to the way the legacy configs are loaded (matches are copied multiple times in the various configs)
/// we need to deduplicate the ids of those matches (and global vars).
fn deduplicate_ids(
match_group: &mut LegacyMatchGroup,
match_map: &mut HashMap<Match, StructId>,
var_map: &mut HashMap<Variable, StructId>,
) {
deduplicate_vars(&mut match_group.global_vars, var_map);
deduplicate_matches(&mut match_group.matches, match_map, var_map);
}
fn deduplicate_matches(
matches: &mut [Match],
match_map: &mut HashMap<Match, StructId>,
var_map: &mut HashMap<Variable, StructId>,
) {
for m in matches.iter_mut() {
// Deduplicate variables first
if let MatchEffect::Text(effect) = &mut m.effect {
deduplicate_vars(&mut effect.vars, var_map);
}
let mut m_without_id = m.clone();
m_without_id.id = 0;
if let Some(id) = match_map.get(&m_without_id) {
m.id = *id;
} else {
match_map.insert(m_without_id, m.id);
}
}
}
// TODO: test case of matches with inner variables
fn deduplicate_vars(vars: &mut [Variable], var_map: &mut HashMap<Variable, StructId>) {
for v in vars.iter_mut() {
let mut v_without_id = v.clone();
v_without_id.id = 0;
if let Some(id) = var_map.get(&v_without_id) {
v.id = *id;
} else {
var_map.insert(v_without_id, v.id);
}
}
}
struct LegacyInteropConfig {
pub name: String,
match_paths: Vec<String>,
id: i32,
config: LegacyConfig,
filter_title: Option<Regex>,
filter_class: Option<Regex>,
filter_exec: Option<Regex>,
}
impl From<config::LegacyConfig> for LegacyInteropConfig {
fn from(config: config::LegacyConfig) -> Self {
Self {
id: next_id(),
config: config.clone(),
name: config.name.clone(),
match_paths: vec![config.name],
filter_title: if !config.filter_title.is_empty() {
Regex::new(&config.filter_title).ok()
} else {
None
},
filter_class: if !config.filter_class.is_empty() {
Regex::new(&config.filter_class).ok()
} else {
None
},
filter_exec: if !config.filter_exec.is_empty() {
Regex::new(&config.filter_exec).ok()
} else {
None
},
}
}
}
impl Config for LegacyInteropConfig {
fn id(&self) -> i32 {
self.id
}
fn label(&self) -> &str {
&self.config.name
}
fn backend(&self) -> crate::config::Backend {
match self.config.backend {
config::BackendType::Inject => crate::config::Backend::Inject,
config::BackendType::Clipboard => crate::config::Backend::Clipboard,
config::BackendType::Auto => crate::config::Backend::Auto,
}
}
fn auto_restart(&self) -> bool {
self.config.auto_restart
}
fn match_paths(&self) -> &[String] {
&self.match_paths
}
fn is_match(&self, app: &AppProperties) -> bool {
if self.filter_title.is_none() && self.filter_exec.is_none() && self.filter_class.is_none() {
return false;
}
let is_title_match = if let Some(title_regex) = self.filter_title.as_ref() {
if let Some(title) = app.title {
title_regex.is_match(title)
} else {
false
}
} else {
true
};
let is_exec_match = if let Some(exec_regex) = self.filter_exec.as_ref() {
if let Some(exec) = app.exec {
exec_regex.is_match(exec)
} else {
false
}
} else {
true
};
let is_class_match = if let Some(class_regex) = self.filter_class.as_ref() {
if let Some(class) = app.class {
class_regex.is_match(class)
} else {
false
}
} else {
true
};
// All the filters that have been specified must be true to define a match
is_exec_match && is_title_match && is_class_match
}
fn clipboard_threshold(&self) -> usize {
crate::config::default::DEFAULT_CLIPBOARD_THRESHOLD
}
fn pre_paste_delay(&self) -> usize {
crate::config::default::DEFAULT_PRE_PASTE_DELAY
}
fn toggle_key(&self) -> Option<crate::config::ToggleKey> {
match self.config.toggle_key {
model::KeyModifier::CTRL => Some(crate::config::ToggleKey::Ctrl),
model::KeyModifier::SHIFT => Some(crate::config::ToggleKey::Shift),
model::KeyModifier::ALT => Some(crate::config::ToggleKey::Alt),
model::KeyModifier::META => Some(crate::config::ToggleKey::Meta),
model::KeyModifier::BACKSPACE => None,
model::KeyModifier::OFF => None,
model::KeyModifier::LEFT_CTRL => Some(crate::config::ToggleKey::LeftCtrl),
model::KeyModifier::RIGHT_CTRL => Some(crate::config::ToggleKey::RightCtrl),
model::KeyModifier::LEFT_ALT => Some(crate::config::ToggleKey::LeftAlt),
model::KeyModifier::RIGHT_ALT => Some(crate::config::ToggleKey::RightAlt),
model::KeyModifier::LEFT_META => Some(crate::config::ToggleKey::LeftMeta),
model::KeyModifier::RIGHT_META => Some(crate::config::ToggleKey::RightMeta),
model::KeyModifier::LEFT_SHIFT => Some(crate::config::ToggleKey::LeftShift),
model::KeyModifier::RIGHT_SHIFT => Some(crate::config::ToggleKey::RightShift),
model::KeyModifier::CAPS_LOCK => None,
}
}
fn preserve_clipboard(&self) -> bool {
self.config.preserve_clipboard
}
fn restore_clipboard_delay(&self) -> usize {
self.config.restore_clipboard_delay.try_into().unwrap()
}
fn paste_shortcut_event_delay(&self) -> usize {
crate::config::default::DEFAULT_SHORTCUT_EVENT_DELAY
}
fn paste_shortcut(&self) -> Option<String> {
match self.config.paste_shortcut {
model::PasteShortcut::Default => None,
model::PasteShortcut::CtrlV => Some("CTRL+V".to_string()),
model::PasteShortcut::CtrlShiftV => Some("CTRL+SHIFT+V".to_string()),
model::PasteShortcut::ShiftInsert => Some("SHIFT+INSERT".to_string()),
model::PasteShortcut::CtrlAltV => Some("CTRL+ALT+V".to_string()),
model::PasteShortcut::MetaV => Some("META+V".to_string()),
}
}
fn disable_x11_fast_inject(&self) -> bool {
!self.config.fast_inject
}
fn inject_delay(&self) -> Option<usize> {
if self.config.inject_delay == 0 {
None
} else {
Some(self.config.inject_delay.try_into().unwrap())
}
}
fn key_delay(&self) -> Option<usize> {
if self.config.backspace_delay == 0 {
None
} else {
Some(self.config.backspace_delay.try_into().unwrap())
}
}
fn word_separators(&self) -> Vec<String> {
self
.config
.word_separators
.iter()
.map(|c| String::from(*c))
.collect()
}
fn backspace_limit(&self) -> usize {
self.config.backspace_limit.try_into().unwrap()
}
fn apply_patch(&self) -> bool {
true
}
fn keyboard_layout(&self) -> Option<crate::config::RMLVOConfig> {
None
}
fn search_trigger(&self) -> Option<String> {
self.config.search_trigger.clone()
}
fn search_shortcut(&self) -> Option<String> {
self.config.search_shortcut.clone()
}
fn undo_backspace(&self) -> bool {
self.config.undo_backspace
}
fn show_icon(&self) -> bool {
self.config.show_icon
}
fn show_notifications(&self) -> bool {
self.config.show_notifications
}
fn secure_input_notification(&self) -> bool {
self.config.secure_input_notification
}
fn enable(&self) -> bool {
self.config.enable_active
}
fn post_form_delay(&self) -> usize {
crate::config::default::DEFAULT_POST_FORM_DELAY
}
fn post_search_delay(&self) -> usize {
crate::config::default::DEFAULT_POST_SEARCH_DELAY
}
fn emulate_alt_codes(&self) -> bool {
false
}
fn win32_exclude_orphan_events(&self) -> bool {
true
}
fn evdev_modifier_delay(&self) -> Option<usize> {
Some(10)
}
fn win32_keyboard_layout_cache_interval(&self) -> i64 {
2000
}
fn x11_use_xclip_backend(&self) -> bool {
false
}
fn x11_use_xdotool_backend(&self) -> bool {
false
}
}
struct LegacyMatchGroup {
matches: Vec<Match>,
global_vars: Vec<Variable>,
}
struct LegacyMatchStore {
groups: HashMap<String, LegacyMatchGroup>,
}
impl LegacyMatchStore {
pub fn new(groups: HashMap<String, LegacyMatchGroup>) -> Self {
Self { groups }
}
}
impl MatchStore for LegacyMatchStore {
fn query(&self, paths: &[String]) -> MatchSet {
let group = if !paths.is_empty() {
self.groups.get(&paths[0])
} else {
None
};
if let Some(group) = group {
MatchSet {
matches: group.matches.iter().collect(),
global_vars: group.global_vars.iter().collect(),
}
} else {
MatchSet {
matches: Vec::new(),
global_vars: Vec::new(),
}
}
}
fn loaded_paths(&self) -> Vec<String> {
self.groups.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs::create_dir_all, path::Path};
use tempdir::TempDir;
pub fn use_test_directory(callback: impl FnOnce(&Path, &Path, &Path)) {
let dir = TempDir::new("tempconfig").unwrap();
let user_dir = dir.path().join("user");
create_dir_all(&user_dir).unwrap();
let package_dir = TempDir::new("tempconfig").unwrap();
callback(
&dunce::canonicalize(&dir.path()).unwrap(),
&dunce::canonicalize(&user_dir).unwrap(),
&dunce::canonicalize(&package_dir.path()).unwrap(),
);
}
#[test]
fn load_legacy_works_correctly() {
use_test_directory(|base, user, packages| {
std::fs::write(
base.join("default.yml"),
r#"
backend: Clipboard
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
std::fs::write(
user.join("specific.yml"),
r#"
name: specific
parent: default
matches:
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
std::fs::write(
user.join("separate.yml"),
r#"
name: separate
filter_title: "Google"
matches:
- trigger: "eren"
replace: "mikasa"
"#,
)
.unwrap();
let (config_store, match_store) = load(base, packages).unwrap();
let default_config = config_store.default();
assert_eq!(default_config.match_paths().len(), 1);
let active_config = config_store.active(&AppProperties {
title: Some("Google"),
class: None,
exec: None,
});
assert_eq!(active_config.match_paths().len(), 1);
let default_fallback = config_store.active(&AppProperties {
title: Some("Yahoo"),
class: None,
exec: None,
});
assert_eq!(default_fallback.match_paths().len(), 1);
assert_eq!(
match_store
.query(default_config.match_paths())
.matches
.len(),
2
);
assert_eq!(
match_store
.query(default_config.match_paths())
.global_vars
.len(),
1
);
assert_eq!(
match_store.query(active_config.match_paths()).matches.len(),
3
);
assert_eq!(
match_store
.query(active_config.match_paths())
.global_vars
.len(),
1
);
assert_eq!(
match_store
.query(default_fallback.match_paths())
.matches
.len(),
2
);
assert_eq!(
match_store
.query(default_fallback.match_paths())
.global_vars
.len(),
1
);
});
}
#[test]
fn load_legacy_deduplicates_ids_correctly() {
use_test_directory(|base, user, packages| {
std::fs::write(
base.join("default.yml"),
r#"
backend: Clipboard
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
- trigger: "withvars"
replace: "{{output}}"
vars:
- name: "output"
type: "echo"
params:
echo: "test"
"#,
)
.unwrap();
std::fs::write(
user.join("specific.yml"),
r#"
name: specific
filter_title: "Google"
"#,
)
.unwrap();
let (config_store, match_store) = load(base, packages).unwrap();
let default_config = config_store.default();
let active_config = config_store.active(&AppProperties {
title: Some("Google"),
class: None,
exec: None,
});
for (i, m) in match_store
.query(default_config.match_paths())
.matches
.into_iter()
.enumerate()
{
assert_eq!(
m.id,
match_store
.query(active_config.match_paths())
.matches
.get(i)
.unwrap()
.id
);
}
assert_eq!(
match_store
.query(default_config.match_paths())
.global_vars
.first()
.unwrap()
.id,
match_store
.query(active_config.match_paths())
.global_vars
.first()
.unwrap()
.id,
);
});
}
#[test]
fn load_legacy_with_packages() {
use_test_directory(|base, _, packages| {
std::fs::write(
base.join("default.yml"),
r#"
backend: Clipboard
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
create_dir_all(packages.join("test-package")).unwrap();
std::fs::write(
packages.join("test-package").join("package.yml"),
r#"
name: test-package
parent: default
matches:
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let (config_store, match_store) = load(base, packages).unwrap();
let default_config = config_store.default();
assert_eq!(default_config.match_paths().len(), 1);
assert_eq!(
match_store
.query(default_config.match_paths())
.matches
.len(),
2
);
});
}
}

View File

@ -0,0 +1,62 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use serde::{Deserialize, Serialize};
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq)]
pub enum KeyModifier {
CTRL,
SHIFT,
ALT,
META,
BACKSPACE,
OFF,
// These are specific variants of the ones above. See issue: #117
// https://github.com/federico-terzi/espanso/issues/117
LEFT_CTRL,
RIGHT_CTRL,
LEFT_ALT,
RIGHT_ALT,
LEFT_META,
RIGHT_META,
LEFT_SHIFT,
RIGHT_SHIFT,
// Special cases, should not be used in config
CAPS_LOCK,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum PasteShortcut {
Default, // Default one for the current system
CtrlV, // Classic Ctrl+V shortcut
CtrlShiftV, // Could be used to paste without formatting in many applications
ShiftInsert, // Often used in Linux systems
CtrlAltV, // Used in some Linux terminals (urxvt)
MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
}
impl Default for PasteShortcut {
fn default() -> Self {
PasteShortcut::Default
}
}

319
espanso-config/src/lib.rs Normal file
View File

@ -0,0 +1,319 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use config::ConfigStore;
use matches::store::MatchStore;
use std::path::Path;
use thiserror::Error;
#[macro_use]
extern crate lazy_static;
pub mod config;
mod counter;
pub mod error;
mod legacy;
pub mod matches;
mod util;
#[allow(clippy::type_complexity)]
pub fn load(
base_path: &Path,
) -> Result<(
Box<dyn ConfigStore>,
Box<dyn MatchStore>,
Vec<error::NonFatalErrorSet>,
)> {
let config_dir = base_path.join("config");
if !config_dir.exists() || !config_dir.is_dir() {
return Err(ConfigError::MissingConfigDir().into());
}
let (config_store, non_fatal_config_errors) = config::load_store(&config_dir)?;
let root_paths = config_store.get_all_match_paths();
let (match_store, non_fatal_match_errors) =
matches::store::load(&root_paths.into_iter().collect::<Vec<String>>());
let mut non_fatal_errors = Vec::new();
non_fatal_errors.extend(non_fatal_config_errors.into_iter());
non_fatal_errors.extend(non_fatal_match_errors.into_iter());
Ok((
Box::new(config_store),
Box::new(match_store),
non_fatal_errors,
))
}
pub fn load_legacy(
config_dir: &Path,
package_dir: &Path,
) -> Result<(Box<dyn ConfigStore>, Box<dyn MatchStore>)> {
legacy::load(config_dir, package_dir)
}
pub fn is_legacy_config(base_dir: &Path) -> bool {
base_dir.join("user").is_dir() && base_dir.join("default.yml").is_file()
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("missing config directory")]
MissingConfigDir(),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::tests::use_test_directory;
use config::AppProperties;
#[test]
fn load_works_correctly() {
use_test_directory(|base, match_dir, config_dir| {
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "_sub.yml"
matches:
- trigger: "hello2"
replace: "world2"
"#,
)
.unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(
&under_file,
r#"
matches:
- trigger: "hello3"
replace: "world3"
"#,
)
.unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, "").unwrap();
let custom_config_file = config_dir.join("custom.yml");
std::fs::write(
&custom_config_file,
r#"
filter_title: "Chrome"
use_standard_includes: false
includes: ["../match/another.yml"]
"#,
)
.unwrap();
let (config_store, match_store, errors) = load(base).unwrap();
assert_eq!(errors.len(), 0);
assert_eq!(config_store.default().match_paths().len(), 2);
assert_eq!(
config_store
.active(&AppProperties {
title: Some("Google Chrome"),
class: None,
exec: None,
})
.match_paths()
.len(),
1
);
assert_eq!(
match_store
.query(config_store.default().match_paths())
.matches
.len(),
3
);
assert_eq!(
match_store
.query(
config_store
.active(&AppProperties {
title: Some("Chrome"),
class: None,
exec: None,
})
.match_paths()
)
.matches
.len(),
2
);
});
}
#[test]
fn load_non_fatal_errors() {
use_test_directory(|base, match_dir, config_dir| {
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
matches:
- "invalid"invalid
"#,
)
.unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "_sub.yml"
matches:
- trigger: "hello2"
replace: "world2"
"#,
)
.unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(
&under_file,
r#"
matches:
- trigger: "hello3"
replace: "world3"invalid
"#,
)
.unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, r#""#).unwrap();
let custom_config_file = config_dir.join("custom.yml");
std::fs::write(
&custom_config_file,
r#"
filter_title: "Chrome"
"
use_standard_includes: false
includes: ["../match/another.yml"]
"#,
)
.unwrap();
let (config_store, match_store, errors) = load(base).unwrap();
assert_eq!(errors.len(), 3);
// It shouldn't have loaded the "config.yml" one because of the YAML error
assert_eq!(config_store.configs().len(), 1);
// It shouldn't load "base.yml" and "_sub.yml" due to YAML errors
assert_eq!(match_store.loaded_paths().len(), 1);
});
}
#[test]
fn load_non_fatal_match_errors() {
use_test_directory(|base, match_dir, config_dir| {
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
matches:
- trigger: "hello"
replace: "world"
- trigger: "invalid because there is no action field"
"#,
)
.unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, r#""#).unwrap();
let (config_store, match_store, errors) = load(base).unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].file, base_file);
assert_eq!(errors[0].errors.len(), 1);
assert_eq!(
match_store
.query(config_store.default().match_paths())
.matches
.len(),
1
);
});
}
#[test]
fn load_fatal_errors() {
use_test_directory(|base, match_dir, config_dir| {
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
matches:
- trigger: hello
replace: world
"#,
)
.unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(
&config_file,
r#"
invalid
"
"#,
)
.unwrap();
// A syntax error in the default.yml file cannot be handled gracefully
assert!(load(base).is_err());
});
}
#[test]
fn load_without_valid_config_dir() {
use_test_directory(|_, match_dir, _| {
// To correcly load the configs, the "load" method looks for the "config" directory
assert!(load(match_dir).is_err());
});
}
}

View File

@ -0,0 +1,179 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use std::path::Path;
use thiserror::Error;
use crate::error::NonFatalErrorSet;
use self::yaml::YAMLImporter;
use super::MatchGroup;
pub(crate) mod yaml;
trait Importer {
fn is_supported(&self, extension: &str) -> bool;
fn load_group(&self, path: &Path) -> Result<(MatchGroup, Option<NonFatalErrorSet>)>;
}
lazy_static! {
static ref IMPORTERS: Vec<Box<dyn Importer + Sync + Send>> = vec![Box::new(YAMLImporter::new()),];
}
pub(crate) fn load_match_group(path: &Path) -> Result<(MatchGroup, Option<NonFatalErrorSet>)> {
if let Some(extension) = path.extension() {
let extension = extension.to_string_lossy().to_lowercase();
let importer = IMPORTERS
.iter()
.find(|importer| importer.is_supported(&extension));
match importer {
Some(importer) => match importer.load_group(path) {
Ok((group, non_fatal_error_set)) => Ok((group, non_fatal_error_set)),
Err(err) => Err(LoadError::ParsingError(err).into()),
},
None => Err(LoadError::InvalidFormat.into()),
}
} else {
Err(LoadError::MissingExtension.into())
}
}
#[derive(Error, Debug)]
pub enum LoadError {
#[error("missing extension in match group file")]
MissingExtension,
#[error("invalid match group format")]
InvalidFormat,
#[error(transparent)]
ParsingError(anyhow::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::tests::use_test_directory;
#[test]
fn load_group_invalid_format() {
use_test_directory(|_, match_dir, _| {
let file = match_dir.join("base.invalid");
std::fs::write(&file, "test").unwrap();
assert!(matches!(
load_match_group(&file)
.unwrap_err()
.downcast::<LoadError>()
.unwrap(),
LoadError::InvalidFormat
));
});
}
#[test]
fn load_group_missing_extension() {
use_test_directory(|_, match_dir, _| {
let file = match_dir.join("base");
std::fs::write(&file, "test").unwrap();
assert!(matches!(
load_match_group(&file)
.unwrap_err()
.downcast::<LoadError>()
.unwrap(),
LoadError::MissingExtension
));
});
}
#[test]
fn load_group_parsing_error() {
use_test_directory(|_, match_dir, _| {
let file = match_dir.join("base.yml");
std::fs::write(&file, "test").unwrap();
assert!(matches!(
load_match_group(&file)
.unwrap_err()
.downcast::<LoadError>()
.unwrap(),
LoadError::ParsingError(_)
));
});
}
#[test]
fn load_group_yaml_format() {
use_test_directory(|_, match_dir, _| {
let file = match_dir.join("base.yml");
std::fs::write(
&file,
r#"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
assert_eq!(load_match_group(&file).unwrap().0.matches.len(), 1);
});
}
#[test]
fn load_group_yaml_format_2() {
use_test_directory(|_, match_dir, _| {
let file = match_dir.join("base.yaml");
std::fs::write(
&file,
r#"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
assert_eq!(load_match_group(&file).unwrap().0.matches.len(), 1);
});
}
#[test]
fn load_group_yaml_format_casing() {
use_test_directory(|_, match_dir, _| {
let file = match_dir.join("base.YML");
std::fs::write(
&file,
r#"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
assert_eq!(load_match_group(&file).unwrap().0.matches.len(), 1);
});
}
}

View File

@ -0,0 +1,921 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::{
counter::next_id,
error::{ErrorRecord, NonFatalErrorSet},
matches::{
group::{path::resolve_imports, MatchGroup},
ImageEffect, Match, Params, RegexCause, TextFormat, TextInjectMode, UpperCasingStyle, Value,
Variable,
},
};
use anyhow::{anyhow, bail, Context, Result};
use parse::YAMLMatchGroup;
use regex::{Captures, Regex};
use self::{
parse::{YAMLMatch, YAMLVariable},
util::convert_params,
};
use crate::matches::{MatchCause, MatchEffect, TextEffect, TriggerCause};
use super::Importer;
pub(crate) mod parse;
mod util;
lazy_static! {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
static ref FORM_CONTROL_REGEX: Regex =
Regex::new("\\[\\[\\s*(\\w+)(\\.\\w+)?\\s*\\]\\]").unwrap();
}
// Create an alias to make the meaning more explicit
type Warning = anyhow::Error;
pub(crate) struct YAMLImporter {}
impl YAMLImporter {
pub fn new() -> Self {
Self {}
}
}
impl Importer for YAMLImporter {
fn is_supported(&self, extension: &str) -> bool {
extension == "yaml" || extension == "yml"
}
fn load_group(
&self,
path: &std::path::Path,
) -> anyhow::Result<(crate::matches::group::MatchGroup, Option<NonFatalErrorSet>)> {
let yaml_group =
YAMLMatchGroup::parse_from_file(path).context("failed to parse YAML match group")?;
let mut non_fatal_errors = Vec::new();
let mut global_vars = Vec::new();
for yaml_global_var in yaml_group.global_vars.as_ref().cloned().unwrap_or_default() {
match try_convert_into_variable(yaml_global_var, false) {
Ok((var, warnings)) => {
global_vars.push(var);
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
}
Err(err) => {
non_fatal_errors.push(ErrorRecord::error(err));
}
}
}
let mut matches = Vec::new();
for yaml_match in yaml_group.matches.as_ref().cloned().unwrap_or_default() {
match try_convert_into_match(yaml_match, false) {
Ok((m, warnings)) => {
matches.push(m);
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
}
Err(err) => {
non_fatal_errors.push(ErrorRecord::error(err));
}
}
}
// Resolve imports
let (resolved_imports, import_errors) =
resolve_imports(path, &yaml_group.imports.unwrap_or_default())
.context("failed to resolve YAML match group imports")?;
non_fatal_errors.extend(import_errors);
let non_fatal_error_set = if !non_fatal_errors.is_empty() {
Some(NonFatalErrorSet::new(path, non_fatal_errors))
} else {
None
};
Ok((
MatchGroup {
imports: resolved_imports,
global_vars,
matches,
},
non_fatal_error_set,
))
}
}
pub fn try_convert_into_match(
yaml_match: YAMLMatch,
use_compatibility_mode: bool,
) -> Result<(Match, Vec<Warning>)> {
let mut warnings = Vec::new();
if yaml_match.uppercase_style.is_some() && yaml_match.propagate_case.is_none() {
warnings.push(anyhow!(
"specifying the 'uppercase_style' option without 'propagate_case' has no effect"
));
}
let triggers = if let Some(trigger) = yaml_match.trigger {
Some(vec![trigger])
} else {
yaml_match.triggers
};
let uppercase_style = match yaml_match
.uppercase_style
.map(|s| s.to_lowercase())
.as_deref()
{
Some("uppercase") => UpperCasingStyle::Uppercase,
Some("capitalize") => UpperCasingStyle::Capitalize,
Some("capitalize_words") => UpperCasingStyle::CapitalizeWords,
Some(style) => {
warnings.push(anyhow!(
"unrecognized uppercase_style: {:?}, falling back to the default",
style
));
TriggerCause::default().uppercase_style
}
_ => TriggerCause::default().uppercase_style,
};
let cause = if let Some(triggers) = triggers {
MatchCause::Trigger(TriggerCause {
triggers,
left_word: yaml_match
.left_word
.or(yaml_match.word)
.unwrap_or(TriggerCause::default().left_word),
right_word: yaml_match
.right_word
.or(yaml_match.word)
.unwrap_or(TriggerCause::default().right_word),
propagate_case: yaml_match
.propagate_case
.unwrap_or(TriggerCause::default().propagate_case),
uppercase_style,
})
} else if let Some(regex) = yaml_match.regex {
// TODO: add test case
MatchCause::Regex(RegexCause { regex })
} else {
MatchCause::None
};
// TODO: test force_mode/force_clipboard
let force_mode = if let Some(true) = yaml_match.force_clipboard {
Some(TextInjectMode::Clipboard)
} else if let Some(mode) = yaml_match.force_mode {
match mode.to_lowercase().as_str() {
"clipboard" => Some(TextInjectMode::Clipboard),
"keys" => Some(TextInjectMode::Keys),
_ => None,
}
} else {
None
};
let effect =
if yaml_match.replace.is_some() || yaml_match.markdown.is_some() || yaml_match.html.is_some() {
// TODO: test markdown and html cases
let (replace, format) = if let Some(plain) = yaml_match.replace {
(plain, TextFormat::Plain)
} else if let Some(markdown) = yaml_match.markdown {
(markdown, TextFormat::Markdown)
} else if let Some(html) = yaml_match.html {
(html, TextFormat::Html)
} else {
unreachable!();
};
let mut vars: Vec<Variable> = Vec::new();
for yaml_var in yaml_match.vars.unwrap_or_default() {
let (var, var_warnings) =
try_convert_into_variable(yaml_var.clone(), use_compatibility_mode)
.with_context(|| format!("failed to load variable: {:?}", yaml_var))?;
warnings.extend(var_warnings);
vars.push(var);
}
MatchEffect::Text(TextEffect {
replace,
vars,
format,
force_mode,
})
} else if let Some(form_layout) = yaml_match.form {
// Replace all the form fields with actual variables
// In v2.1.0-alpha the form control syntax was replaced with [[control]]
// instead of {{control}}, so we check if compatibility mode is being used.
// TODO: remove once compatibility mode is removed
let (resolved_replace, resolved_layout) = if use_compatibility_mode {
(
VAR_REGEX
.replace_all(&form_layout, |caps: &Captures| {
let var_name = caps.get(1).unwrap().as_str();
format!("{{{{form1.{}}}}}", var_name)
})
.to_string(),
VAR_REGEX
.replace_all(&form_layout, |caps: &Captures| {
let var_name = caps.get(1).unwrap().as_str();
format!("[[{}]]", var_name)
})
.to_string(),
)
} else {
(
FORM_CONTROL_REGEX
.replace_all(&form_layout, |caps: &Captures| {
let var_name = caps.get(1).unwrap().as_str();
format!("{{{{form1.{}}}}}", var_name)
})
.to_string(),
form_layout,
)
};
// Convert escaped brakets in forms
let resolved_replace = resolved_replace.replace("\\{", "{ ").replace("\\}", " }");
// Convert the form data to valid variables
let mut params = Params::new();
params.insert("layout".to_string(), Value::String(resolved_layout));
if let Some(fields) = yaml_match.form_fields {
params.insert("fields".to_string(), Value::Object(convert_params(fields)?));
}
let vars = vec![Variable {
id: next_id(),
name: "form1".to_owned(),
var_type: "form".to_owned(),
params,
..Default::default()
}];
MatchEffect::Text(TextEffect {
replace: resolved_replace,
vars,
format: TextFormat::Plain,
force_mode,
})
} else if let Some(image_path) = yaml_match.image_path {
// TODO: test image case
MatchEffect::Image(ImageEffect { path: image_path })
} else {
MatchEffect::None
};
if let MatchEffect::None = effect {
bail!(
"match triggered by {:?} does not produce any effect. Did you forget the 'replace' field?",
cause.long_description()
);
}
Ok((
Match {
cause,
effect,
label: yaml_match.label,
id: next_id(),
search_terms: yaml_match.search_terms.unwrap_or_default(),
},
warnings,
))
}
pub fn try_convert_into_variable(
yaml_var: YAMLVariable,
use_compatibility_mode: bool,
) -> Result<(Variable, Vec<Warning>)> {
Ok((
Variable {
name: yaml_var.name,
var_type: yaml_var.var_type,
params: convert_params(yaml_var.params)?,
id: next_id(),
inject_vars: !use_compatibility_mode && yaml_var.inject_vars.unwrap_or(true),
depends_on: yaml_var.depends_on,
},
Vec::new(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
matches::{Match, Params, Value},
util::tests::use_test_directory,
};
use std::fs::create_dir_all;
fn create_match_with_warnings(
yaml: &str,
use_compatibility_mode: bool,
) -> Result<(Match, Vec<Warning>)> {
let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?;
let (mut m, warnings) = try_convert_into_match(yaml_match, use_compatibility_mode)?;
// Reset the IDs to correctly compare them
m.id = 0;
if let MatchEffect::Text(e) = &mut m.effect {
e.vars.iter_mut().for_each(|v| v.id = 0);
}
Ok((m, warnings))
}
fn create_match(yaml: &str) -> Result<Match> {
let (m, warnings) = create_match_with_warnings(yaml, false)?;
if !warnings.is_empty() {
panic!("warnings were detected but not handled: {:?}", warnings);
}
Ok(m)
}
#[test]
fn basic_match_maps_correctly() {
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn multiple_triggers_maps_correctly() {
assert_eq!(
create_match(
r#"
triggers: ["Hello", "john"]
replace: "world"
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string(), "john".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn word_maps_correctly() {
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
word: true
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
left_word: true,
right_word: true,
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn left_word_maps_correctly() {
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
left_word: true
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
left_word: true,
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn right_word_maps_correctly() {
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
right_word: true
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
right_word: true,
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn propagate_case_maps_correctly() {
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
propagate_case: true
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
propagate_case: true,
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn uppercase_style_maps_correctly() {
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
uppercase_style: "capitalize"
propagate_case: true
"#
)
.unwrap()
.cause
.into_trigger()
.unwrap()
.uppercase_style,
UpperCasingStyle::Capitalize,
);
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
uppercase_style: "capitalize_words"
propagate_case: true
"#
)
.unwrap()
.cause
.into_trigger()
.unwrap()
.uppercase_style,
UpperCasingStyle::CapitalizeWords,
);
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
uppercase_style: "uppercase"
propagate_case: true
"#
)
.unwrap()
.cause
.into_trigger()
.unwrap()
.uppercase_style,
UpperCasingStyle::Uppercase,
);
// Invalid without propagate_case
let (m, warnings) = create_match_with_warnings(
r#"
trigger: "Hello"
replace: "world"
uppercase_style: "capitalize"
"#,
false,
)
.unwrap();
assert_eq!(
m.cause.into_trigger().unwrap().uppercase_style,
UpperCasingStyle::Capitalize,
);
assert_eq!(warnings.len(), 1);
// Invalid style
let (m, warnings) = create_match_with_warnings(
r#"
trigger: "Hello"
replace: "world"
uppercase_style: "invalid"
propagate_case: true
"#,
false,
)
.unwrap();
assert_eq!(
m.cause.into_trigger().unwrap().uppercase_style,
UpperCasingStyle::Uppercase,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn form_maps_correctly() {
let mut params = Params::new();
params.insert(
"layout".to_string(),
Value::String("Hi [[name]]!".to_string()),
);
assert_eq!(
create_match(
r#"
trigger: "Hello"
form: "Hi [[name]]!"
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "Hi {{form1.name}}!".to_string(),
vars: vec![Variable {
id: 0,
name: "form1".to_string(),
var_type: "form".to_string(),
params,
..Default::default()
}],
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn form_maps_correctly_with_variable_injection() {
let mut params = Params::new();
params.insert(
"layout".to_string(),
Value::String("Hi [[name]]! {{signature}}".to_string()),
);
assert_eq!(
create_match(
r#"
trigger: "Hello"
form: "Hi [[name]]! {{signature}}"
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "Hi {{form1.name}}! {{signature}}".to_string(),
vars: vec![Variable {
id: 0,
name: "form1".to_string(),
var_type: "form".to_string(),
params,
..Default::default()
}],
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn form_maps_correctly_legacy_format() {
let mut params = Params::new();
params.insert(
"layout".to_string(),
Value::String("Hi [[name]]!".to_string()),
);
assert_eq!(
create_match_with_warnings(
r#"
trigger: "Hello"
form: "Hi {{name}}!"
"#,
true
)
.unwrap()
.0,
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "Hi {{form1.name}}!".to_string(),
vars: vec![Variable {
id: 0,
name: "form1".to_string(),
var_type: "form".to_string(),
params,
..Default::default()
}],
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn vars_maps_correctly() {
let mut params = Params::new();
params.insert("param1".to_string(), Value::Bool(true));
let vars = vec![Variable {
name: "var1".to_string(),
var_type: "test".to_string(),
params,
..Default::default()
}];
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
vars:
- name: var1
type: test
params:
param1: true
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
vars,
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn vars_inject_vars_and_depends_on() {
let vars = vec![
Variable {
name: "var1".to_string(),
var_type: "test".to_string(),
depends_on: vec!["test".to_owned()],
..Default::default()
},
Variable {
name: "var2".to_string(),
var_type: "test".to_string(),
inject_vars: false,
..Default::default()
},
];
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
vars:
- name: var1
type: test
depends_on: ["test"]
- name: var2
type: "test"
inject_vars: false
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
vars,
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn vars_no_params_maps_correctly() {
let vars = vec![Variable {
name: "var1".to_string(),
var_type: "test".to_string(),
params: Params::new(),
..Default::default()
}];
assert_eq!(
create_match(
r#"
trigger: "Hello"
replace: "world"
vars:
- name: var1
type: test
"#
)
.unwrap(),
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["Hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
vars,
..Default::default()
}),
..Default::default()
}
)
}
#[test]
fn importer_is_supported() {
let importer = YAMLImporter::new();
assert!(importer.is_supported("yaml"));
assert!(importer.is_supported("yml"));
assert!(!importer.is_supported("invalid"));
}
#[test]
fn importer_works_correctly() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "sub/sub.yml"
- "invalid/import.yml" # This should be discarded
global_vars:
- name: "var1"
type: "test"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "").unwrap();
let importer = YAMLImporter::new();
let (mut group, non_fatal_error_set) = importer.load_group(&base_file).unwrap();
// The invalid import path should be reported as error
assert_eq!(non_fatal_error_set.unwrap().errors.len(), 1);
// Reset the ids to compare them correctly
group.matches.iter_mut().for_each(|mut m| m.id = 0);
group.global_vars.iter_mut().for_each(|mut v| v.id = 0);
let vars = vec![Variable {
name: "var1".to_string(),
var_type: "test".to_string(),
params: Params::new(),
..Default::default()
}];
assert_eq!(
group,
MatchGroup {
imports: vec![sub_file.to_string_lossy().to_string(),],
global_vars: vars,
matches: vec![Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}],
}
)
});
}
#[test]
fn importer_invalid_syntax() {
use_test_directory(|_, match_dir, _| {
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- invalid
- indentation
"#,
)
.unwrap();
let importer = YAMLImporter::new();
assert!(importer.load_group(&base_file).is_err());
})
}
}

View File

@ -0,0 +1,141 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use crate::util::is_yaml_empty;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YAMLMatchGroup {
#[serde(default)]
pub imports: Option<Vec<String>>,
#[serde(default)]
pub global_vars: Option<Vec<YAMLVariable>>,
#[serde(default)]
pub matches: Option<Vec<YAMLMatch>>,
}
impl YAMLMatchGroup {
pub fn parse_from_str(yaml: &str) -> Result<Self> {
// Because an empty string is not valid YAML but we want to support it anyway
if is_yaml_empty(yaml) {
return Ok(serde_yaml::from_str(
"arbitrary_field_that_will_not_block_the_parser: true",
)?);
}
Ok(serde_yaml::from_str(yaml)?)
}
// TODO: test
pub fn parse_from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
Self::parse_from_str(&content)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YAMLMatch {
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub trigger: Option<String>,
#[serde(default)]
pub triggers: Option<Vec<String>>,
#[serde(default)]
pub regex: Option<String>,
#[serde(default)]
pub replace: Option<String>,
#[serde(default)]
pub image_path: Option<String>,
#[serde(default)]
pub form: Option<String>,
#[serde(default)]
pub form_fields: Option<Mapping>,
#[serde(default)]
pub vars: Option<Vec<YAMLVariable>>,
#[serde(default)]
pub word: Option<bool>,
#[serde(default)]
pub left_word: Option<bool>,
#[serde(default)]
pub right_word: Option<bool>,
#[serde(default)]
pub propagate_case: Option<bool>,
#[serde(default)]
pub uppercase_style: Option<String>,
#[serde(default)]
pub force_clipboard: Option<bool>,
#[serde(default)]
pub force_mode: Option<String>,
#[serde(default)]
pub markdown: Option<String>,
#[serde(default)]
pub paragraph: Option<bool>,
#[serde(default)]
pub html: Option<String>,
#[serde(default)]
pub search_terms: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct YAMLVariable {
pub name: String,
#[serde(rename = "type")]
pub var_type: String,
#[serde(default = "default_params")]
pub params: Mapping,
#[serde(default)]
pub inject_vars: Option<bool>,
#[serde(default)]
pub depends_on: Vec<String>,
}
fn default_params() -> Mapping {
Mapping::new()
}

View File

@ -0,0 +1,176 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::convert::TryInto;
use anyhow::Result;
use serde_yaml::Mapping;
use thiserror::Error;
use crate::matches::{Number, Params, Value};
pub(crate) fn convert_params(m: Mapping) -> Result<Params> {
let mut params = Params::new();
for (key, value) in m {
let key = key.as_str().ok_or(ConversionError::InvalidKeyFormat)?;
let value = convert_value(value)?;
params.insert(key.to_owned(), value);
}
Ok(params)
}
fn convert_value(value: serde_yaml::Value) -> Result<Value> {
Ok(match value {
serde_yaml::Value::Null => Value::Null,
serde_yaml::Value::Bool(val) => Value::Bool(val),
serde_yaml::Value::Number(n) => {
if n.is_i64() {
Value::Number(Number::Integer(
n.as_i64().ok_or(ConversionError::InvalidNumberFormat)?,
))
} else if n.is_u64() {
Value::Number(Number::Integer(
n.as_u64()
.ok_or(ConversionError::InvalidNumberFormat)?
.try_into()?,
))
} else if n.is_f64() {
Value::Number(Number::Float(
n.as_f64()
.ok_or(ConversionError::InvalidNumberFormat)?
.into(),
))
} else {
return Err(ConversionError::InvalidNumberFormat.into());
}
}
serde_yaml::Value::String(s) => Value::String(s),
serde_yaml::Value::Sequence(arr) => Value::Array(
arr
.into_iter()
.map(convert_value)
.collect::<Result<Vec<Value>>>()?,
),
serde_yaml::Value::Mapping(m) => Value::Object(convert_params(m)?),
})
}
#[derive(Error, Debug)]
pub enum ConversionError {
#[error("invalid key format")]
InvalidKeyFormat,
#[error("invalid number format")]
InvalidNumberFormat,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_value_null() {
assert_eq!(convert_value(serde_yaml::Value::Null).unwrap(), Value::Null);
}
#[test]
fn convert_value_bool() {
assert_eq!(
convert_value(serde_yaml::Value::Bool(true)).unwrap(),
Value::Bool(true)
);
assert_eq!(
convert_value(serde_yaml::Value::Bool(false)).unwrap(),
Value::Bool(false)
);
}
#[test]
fn convert_value_number() {
assert_eq!(
convert_value(serde_yaml::Value::Number(0.into())).unwrap(),
Value::Number(Number::Integer(0))
);
assert_eq!(
convert_value(serde_yaml::Value::Number((-100).into())).unwrap(),
Value::Number(Number::Integer(-100))
);
assert_eq!(
convert_value(serde_yaml::Value::Number(1.5.into())).unwrap(),
Value::Number(Number::Float(1.5.into()))
);
}
#[test]
fn convert_value_string() {
assert_eq!(
convert_value(serde_yaml::Value::String("hello".to_string())).unwrap(),
Value::String("hello".to_string())
);
}
#[test]
fn convert_value_array() {
assert_eq!(
convert_value(serde_yaml::Value::Sequence(vec![
serde_yaml::Value::Bool(true),
serde_yaml::Value::Null,
]))
.unwrap(),
Value::Array(vec![Value::Bool(true), Value::Null,])
);
}
#[test]
fn convert_value_params() {
let mut mapping = serde_yaml::Mapping::new();
mapping.insert(
serde_yaml::Value::String("test".to_string()),
serde_yaml::Value::Null,
);
let mut expected = Params::new();
expected.insert("test".to_string(), Value::Null);
assert_eq!(
convert_value(serde_yaml::Value::Mapping(mapping)).unwrap(),
Value::Object(expected)
);
}
#[test]
fn convert_params_works_correctly() {
let mut mapping = serde_yaml::Mapping::new();
mapping.insert(
serde_yaml::Value::String("test".to_string()),
serde_yaml::Value::Null,
);
let mut expected = Params::new();
expected.insert("test".to_string(), Value::Null);
assert_eq!(convert_params(mapping).unwrap(), expected);
}
#[test]
fn convert_params_invalid_key_type() {
let mut mapping = serde_yaml::Mapping::new();
mapping.insert(serde_yaml::Value::Null, serde_yaml::Value::Null);
assert!(convert_params(mapping).is_err());
}
}

View File

@ -0,0 +1,42 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use std::path::Path;
use crate::error::NonFatalErrorSet;
use super::{Match, Variable};
pub(crate) mod loader;
mod path;
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct MatchGroup {
pub imports: Vec<String>,
pub global_vars: Vec<Variable>,
pub matches: Vec<Match>,
}
impl MatchGroup {
// TODO: test
pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> {
loader::load_match_group(group_path)
}
}

View File

@ -0,0 +1,164 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::error::ErrorRecord;
pub fn resolve_imports(
group_path: &Path,
imports: &[String],
) -> Result<(Vec<String>, Vec<ErrorRecord>)> {
let mut paths = Vec::new();
// Get the containing directory
let current_dir = if group_path.is_file() {
if let Some(parent) = group_path.parent() {
parent
} else {
return Err(
ResolveImportError::Failed(format!(
"unable to resolve imports for match group starting from current path: {:?}",
group_path
))
.into(),
);
}
} else {
group_path
};
let mut non_fatal_errors = Vec::new();
for import in imports.iter() {
let import_path = PathBuf::from(import);
// Absolute or relative import
let full_path = if import_path.is_relative() {
current_dir.join(import_path)
} else {
import_path
};
match dunce::canonicalize(&full_path)
.with_context(|| format!("unable to canonicalize import path: {:?}", full_path))
{
Ok(canonical_path) => {
if canonical_path.exists() && canonical_path.is_file() {
paths.push(canonical_path)
} else {
// Best effort imports
non_fatal_errors.push(ErrorRecord::error(anyhow!(
"unable to resolve import at path: {:?}",
canonical_path
)))
}
}
Err(error) => non_fatal_errors.push(ErrorRecord::error(error)),
}
}
let string_paths = paths
.into_iter()
.map(|path| path.to_string_lossy().to_string())
.collect();
Ok((string_paths, non_fatal_errors))
}
#[derive(Error, Debug)]
pub enum ResolveImportError {
#[error("resolve import failed: `{0}`")]
Failed(String),
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::util::tests::use_test_directory;
use std::fs::create_dir_all;
#[test]
fn resolve_imports_works_correctly() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let absolute_file = sub_dir.join("absolute.yml");
std::fs::write(&absolute_file, "test").unwrap();
let imports = vec![
"another.yml".to_string(),
"sub/sub.yml".to_string(),
absolute_file.to_string_lossy().to_string(),
"sub/invalid.yml".to_string(), // Should be skipped
];
let (resolved_imports, errors) = resolve_imports(&base_file, &imports).unwrap();
assert_eq!(
resolved_imports,
vec![
another_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
absolute_file.to_string_lossy().to_string(),
]
);
// The "sub/invalid.yml" should generate an error
assert_eq!(errors.len(), 1);
});
}
#[test]
fn resolve_imports_parent_relative_path() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, "test").unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let imports = vec!["../base.yml".to_string()];
let (resolved_imports, errors) = resolve_imports(&sub_file, &imports).unwrap();
assert_eq!(
resolved_imports,
vec![base_file.to_string_lossy().to_string(),]
);
assert_eq!(errors.len(), 0);
});
}
}

View File

@ -0,0 +1,244 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use enum_as_inner::EnumAsInner;
use ordered_float::OrderedFloat;
use std::collections::BTreeMap;
use crate::counter::StructId;
pub(crate) mod group;
pub mod store;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Match {
pub id: StructId,
pub cause: MatchCause,
pub effect: MatchEffect,
// Metadata
pub label: Option<String>,
pub search_terms: Vec<String>,
}
impl Default for Match {
fn default() -> Self {
Self {
cause: MatchCause::None,
effect: MatchEffect::None,
label: None,
id: 0,
search_terms: vec![],
}
}
}
impl Match {
// TODO: test
pub fn description(&self) -> &str {
if let Some(label) = &self.label {
label
} else if let MatchEffect::Text(text_effect) = &self.effect {
&text_effect.replace
} else if let MatchEffect::Image(_) = &self.effect {
"Image content"
} else {
"No description available for this match"
}
}
// TODO: test
pub fn cause_description(&self) -> Option<&str> {
self.cause.description()
}
pub fn search_terms(&self) -> Vec<&str> {
self
.search_terms
.iter()
.map(|term| term.as_str())
.chain(self.cause.search_terms())
.collect()
}
}
// Causes
#[derive(Debug, Clone, Eq, Hash, PartialEq, EnumAsInner)]
pub enum MatchCause {
None,
Trigger(TriggerCause),
Regex(RegexCause),
// TODO: shortcut
}
impl MatchCause {
// TODO: test
pub fn description(&self) -> Option<&str> {
if let MatchCause::Trigger(trigger_cause) = &self {
trigger_cause.triggers.first().map(|s| s.as_str())
} else {
None
}
// TODO: insert rendering for hotkey/shortcut
// TODO: insert rendering for regex? I'm worried it might be too long
}
// TODO: test
pub fn long_description(&self) -> String {
if let MatchCause::Trigger(trigger_cause) = &self {
format!("triggers: {:?}", trigger_cause.triggers)
} else {
"No description available".to_owned()
}
// TODO: insert rendering for hotkey/shortcut
// TODO: insert rendering for regex? I'm worried it might be too long
}
pub fn search_terms(&self) -> Vec<&str> {
if let MatchCause::Trigger(trigger_cause) = &self {
trigger_cause.triggers.iter().map(|s| s.as_str()).collect()
} else {
vec![]
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TriggerCause {
pub triggers: Vec<String>,
pub left_word: bool,
pub right_word: bool,
pub propagate_case: bool,
pub uppercase_style: UpperCasingStyle,
}
impl Default for TriggerCause {
fn default() -> Self {
Self {
triggers: Vec::new(),
left_word: false,
right_word: false,
propagate_case: false,
uppercase_style: UpperCasingStyle::Uppercase,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum UpperCasingStyle {
Uppercase,
Capitalize,
CapitalizeWords,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct RegexCause {
pub regex: String,
}
// Effects
#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
pub enum MatchEffect {
None,
Text(TextEffect),
Image(ImageEffect),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TextEffect {
pub replace: String,
pub vars: Vec<Variable>,
pub format: TextFormat,
pub force_mode: Option<TextInjectMode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TextFormat {
Plain,
Markdown,
Html,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TextInjectMode {
Keys,
Clipboard,
}
impl Default for TextEffect {
fn default() -> Self {
Self {
replace: String::new(),
vars: Vec::new(),
format: TextFormat::Plain,
force_mode: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct ImageEffect {
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Variable {
pub id: StructId,
pub name: String,
pub var_type: String,
pub params: Params,
pub inject_vars: bool,
pub depends_on: Vec<String>,
}
impl Default for Variable {
fn default() -> Self {
Self {
id: 0,
name: String::new(),
var_type: String::new(),
params: Params::new(),
inject_vars: true,
depends_on: Vec::new(),
}
}
}
pub type Params = BTreeMap<String, Value>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
pub enum Value {
Null,
Bool(bool),
Number(Number),
String(String),
Array(Vec<Value>),
Object(Params),
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub enum Number {
Integer(i64),
Float(OrderedFloat<f64>),
}

View File

@ -0,0 +1,738 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use super::{MatchSet, MatchStore};
use crate::{
counter::StructId,
error::NonFatalErrorSet,
matches::{group::MatchGroup, Match, Variable},
};
use anyhow::Context;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
};
pub(crate) struct DefaultMatchStore {
pub groups: HashMap<String, MatchGroup>,
}
impl DefaultMatchStore {
pub fn load(paths: &[String]) -> (Self, Vec<NonFatalErrorSet>) {
let mut groups = HashMap::new();
let mut non_fatal_error_sets = Vec::new();
// Because match groups can imports other match groups,
// we have to load them recursively starting from the
// top-level ones.
load_match_groups_recursively(&mut groups, paths, &mut non_fatal_error_sets);
(Self { groups }, non_fatal_error_sets)
}
}
impl MatchStore for DefaultMatchStore {
fn query(&self, paths: &[String]) -> MatchSet {
let mut matches: Vec<&Match> = Vec::new();
let mut global_vars: Vec<&Variable> = Vec::new();
let mut visited_paths = HashSet::new();
let mut visited_matches = HashSet::new();
let mut visited_global_vars = HashSet::new();
query_matches_for_paths(
&self.groups,
&mut visited_paths,
&mut visited_matches,
&mut visited_global_vars,
&mut matches,
&mut global_vars,
paths,
);
MatchSet {
matches,
global_vars,
}
}
fn loaded_paths(&self) -> Vec<String> {
self.groups.keys().cloned().collect()
}
}
fn load_match_groups_recursively(
groups: &mut HashMap<String, MatchGroup>,
paths: &[String],
non_fatal_error_sets: &mut Vec<NonFatalErrorSet>,
) {
for path in paths.iter() {
if !groups.contains_key(path) {
let group_path = PathBuf::from(path);
match MatchGroup::load(&group_path)
.with_context(|| format!("unable to load match group {:?}", group_path))
{
Ok((group, non_fatal_error_set)) => {
let imports = group.imports.clone();
groups.insert(path.clone(), group);
if let Some(non_fatal_error_set) = non_fatal_error_set {
non_fatal_error_sets.push(non_fatal_error_set);
}
load_match_groups_recursively(groups, &imports, non_fatal_error_sets);
}
Err(err) => {
non_fatal_error_sets.push(NonFatalErrorSet::single_error(&group_path, err));
}
}
}
}
}
fn query_matches_for_paths<'a>(
groups: &'a HashMap<String, MatchGroup>,
visited_paths: &mut HashSet<String>,
visited_matches: &mut HashSet<StructId>,
visited_global_vars: &mut HashSet<StructId>,
matches: &mut Vec<&'a Match>,
global_vars: &mut Vec<&'a Variable>,
paths: &[String],
) {
for path in paths.iter() {
if !visited_paths.contains(path) {
visited_paths.insert(path.clone());
if let Some(group) = groups.get(path) {
query_matches_for_paths(
groups,
visited_paths,
visited_matches,
visited_global_vars,
matches,
global_vars,
&group.imports,
);
for m in group.matches.iter() {
if !visited_matches.contains(&m.id) {
matches.push(m);
visited_matches.insert(m.id);
}
}
for var in group.global_vars.iter() {
if !visited_global_vars.contains(&var.id) {
global_vars.push(var);
visited_global_vars.insert(var.id);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
matches::{MatchCause, MatchEffect, TextEffect, TriggerCause},
util::tests::use_test_directory,
};
use std::fs::create_dir_all;
fn create_match(trigger: &str, replace: &str) -> Match {
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec![trigger.to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: replace.to_string(),
..Default::default()
}),
..Default::default()
}
}
fn create_matches(matches: &[(&str, &str)]) -> Vec<Match> {
matches
.iter()
.map(|(trigger, replace)| create_match(trigger, replace))
.collect()
}
fn create_test_var(name: &str) -> Variable {
Variable {
name: name.to_string(),
var_type: "test".to_string(),
..Default::default()
}
}
fn create_vars(vars: &[&str]) -> Vec<Variable> {
vars.iter().map(|var| create_test_var(var)).collect()
}
#[test]
fn match_store_loads_correctly() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "_another.yml"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let (match_store, non_fatal_error_sets) =
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(non_fatal_error_sets.len(), 0);
assert_eq!(match_store.groups.len(), 3);
let base_group = &match_store
.groups
.get(&base_file.to_string_lossy().to_string())
.unwrap()
.matches;
let base_group: Vec<Match> = base_group
.iter()
.map(|m| {
let mut copy = m.clone();
copy.id = 0;
copy
})
.collect();
assert_eq!(base_group, create_matches(&[("hello", "world")]));
let another_group = &match_store
.groups
.get(&another_file.to_string_lossy().to_string())
.unwrap()
.matches;
let another_group: Vec<Match> = another_group
.iter()
.map(|m| {
let mut copy = m.clone();
copy.id = 0;
copy
})
.collect();
assert_eq!(
another_group,
create_matches(&[("hello", "world2"), ("foo", "bar")])
);
let sub_group = &match_store
.groups
.get(&sub_file.to_string_lossy().to_string())
.unwrap()
.matches;
let sub_group: Vec<Match> = sub_group
.iter()
.map(|m| {
let mut copy = m.clone();
copy.id = 0;
copy
})
.collect();
assert_eq!(sub_group, create_matches(&[("hello", "world3")]));
});
}
#[test]
fn match_store_handles_circular_dependency() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "_another.yml"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
imports:
- "../_another.yml"
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let (match_store, non_fatal_error_sets) =
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(match_store.groups.len(), 3);
assert_eq!(non_fatal_error_sets.len(), 0);
});
}
#[test]
fn match_store_query_single_path_with_imports() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let (match_store, non_fatal_error_sets) =
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(non_fatal_error_sets.len(), 0);
let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.map(|mut m| {
m.id = 0;
m
})
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world3"),
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.map(|mut v| {
v.id = 0;
v
})
.collect::<Vec<Variable>>(),
create_vars(&["var2", "var1"])
);
});
}
#[test]
fn match_store_query_handles_circular_depencencies() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
imports:
- "../_another.yml" # Circular import
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let (match_store, non_fatal_error_sets) =
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(non_fatal_error_sets.len(), 0);
let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.map(|mut m| {
m.id = 0;
m
})
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world3"),
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.map(|mut v| {
v.id = 0;
v
})
.collect::<Vec<Variable>>(),
create_vars(&["var2", "var1"])
);
});
}
#[test]
fn match_store_query_multiple_paths() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let (match_store, non_fatal_error_sets) = DefaultMatchStore::load(&[
base_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
]);
assert_eq!(non_fatal_error_sets.len(), 0);
let match_set = match_store.query(&[
base_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.map(|mut m| {
m.id = 0;
m
})
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
("hello", "world3"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.map(|mut v| {
v.id = 0;
v
})
.collect::<Vec<Variable>>(),
create_vars(&["var1", "var2"])
);
});
}
#[test]
fn match_store_query_handle_duplicates_when_imports_and_paths_overlap() {
use_test_directory(|_, match_dir, _| {
let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap();
let base_file = match_dir.join("base.yml");
std::fs::write(
&base_file,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let (match_store, non_fatal_error_sets) =
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(non_fatal_error_sets.len(), 0);
let match_set = match_store.query(&[
base_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.map(|mut m| {
m.id = 0;
m
})
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world3"), // This appears only once, though it appears 2 times
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.map(|mut v| {
v.id = 0;
v
})
.collect::<Vec<Variable>>(),
create_vars(&["var2", "var1"])
);
});
}
// TODO: add fatal and non-fatal error cases
}

View File

@ -0,0 +1,41 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::error::NonFatalErrorSet;
use super::{Match, Variable};
mod default;
pub trait MatchStore: Send {
fn query(&self, paths: &[String]) -> MatchSet;
fn loaded_paths(&self) -> Vec<String>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchSet<'a> {
pub matches: Vec<&'a Match>,
pub global_vars: Vec<&'a Variable>,
}
pub fn load(paths: &[String]) -> (impl MatchStore, Vec<NonFatalErrorSet>) {
// TODO: here we can replace the DefaultMatchStore with a caching wrapper
// that returns the same response for the given "paths" query
default::DefaultMatchStore::load(paths)
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
/// Check if the given string represents an empty YAML.
/// In other words, it checks if the document is only composed
/// of spaces and/or comments
pub fn is_yaml_empty(yaml: &str) -> bool {
for line in yaml.lines() {
let trimmed_line = line.trim();
if !trimmed_line.starts_with('#') && !trimmed_line.is_empty() {
return false;
}
}
true
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::{fs::create_dir_all, path::Path};
use tempdir::TempDir;
pub fn use_test_directory(callback: impl FnOnce(&Path, &Path, &Path)) {
let dir = TempDir::new("tempconfig").unwrap();
let match_dir = dir.path().join("match");
create_dir_all(&match_dir).unwrap();
let config_dir = dir.path().join("config");
create_dir_all(&config_dir).unwrap();
callback(
&dunce::canonicalize(&dir.path()).unwrap(),
&dunce::canonicalize(match_dir).unwrap(),
&dunce::canonicalize(config_dir).unwrap(),
);
}
#[test]
fn is_yaml_empty_document_empty() {
assert!(is_yaml_empty(""));
}
#[test]
fn is_yaml_empty_document_with_comments() {
assert!(is_yaml_empty("\n#comment \n \n"));
}
#[test]
fn is_yaml_empty_document_with_comments_and_content() {
assert!(!is_yaml_empty("\n#comment \n field: true\n"));
}
#[test]
fn is_yaml_empty_document_with_content() {
assert!(!is_yaml_empty("\nfield: true\n"));
}
}

33
espanso-detect/Cargo.toml Normal file
View File

@ -0,0 +1,33 @@
[package]
name = "espanso-detect"
version = "0.1.0"
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
edition = "2018"
build="build.rs"
[features]
# If the wayland feature is enabled, all X11 dependencies will be dropped
# and only EVDEV-based methods will be supported.
wayland = ["sctk"]
[dependencies]
log = "0.4.14"
lazycell = "1.3.0"
anyhow = "1.0.38"
thiserror = "1.0.23"
regex = "1.4.3"
lazy_static = "1.4.0"
[target.'cfg(windows)'.dependencies]
widestring = "0.4.3"
[target.'cfg(target_os="linux")'.dependencies]
libc = "0.2.85"
scopeguard = "1.1.0"
sctk = { package = "smithay-client-toolkit", version = "0.14.0", optional = true }
[build-dependencies]
cc = "1.0.73"
[dev-dependencies]
enum-as-inner = "0.3.3"

83
espanso-detect/build.rs Normal file
View File

@ -0,0 +1,83 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#[cfg(target_os = "windows")]
fn cc_config() {
println!("cargo:rerun-if-changed=src/win32/native.cpp");
println!("cargo:rerun-if-changed=src/win32/native.h");
cc::Build::new()
.cpp(true)
.include("src/win32/native.h")
.file("src/win32/native.cpp")
.compile("espansodetect");
println!("cargo:rustc-link-lib=static=espansodetect");
println!("cargo:rustc-link-lib=dylib=user32");
#[cfg(target_env = "gnu")]
println!("cargo:rustc-link-lib=dylib=stdc++");
}
#[cfg(target_os = "linux")]
fn cc_config() {
println!("cargo:rerun-if-changed=src/x11/native.cpp");
println!("cargo:rerun-if-changed=src/x11/native.h");
println!("cargo:rerun-if-changed=src/evdev/native.cpp");
println!("cargo:rerun-if-changed=src/evdev/native.h");
if cfg!(not(feature = "wayland")) {
cc::Build::new()
.cpp(true)
.include("src/x11")
.file("src/x11/native.cpp")
.compile("espansodetect");
println!("cargo:rustc-link-lib=static=espansodetect");
println!("cargo:rustc-link-lib=dylib=X11");
println!("cargo:rustc-link-lib=dylib=Xtst");
}
cc::Build::new()
.cpp(true)
.include("src/evdev")
.file("src/evdev/native.cpp")
.compile("espansodetectevdev");
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
println!("cargo:rustc-link-lib=static=espansodetectevdev");
println!("cargo:rustc-link-lib=dylib=xkbcommon");
}
#[cfg(target_os = "macos")]
fn cc_config() {
println!("cargo:rerun-if-changed=src/mac/native.mm");
println!("cargo:rerun-if-changed=src/mac/native.h");
cc::Build::new()
.cpp(true)
.include("src/mac/native.h")
.file("src/mac/native.mm")
.compile("espansodetect");
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=espansodetect");
println!("cargo:rustc-link-lib=framework=Cocoa");
println!("cargo:rustc-link-lib=framework=Carbon");
}
fn main() {
cc_config();
}

View File

@ -0,0 +1,3 @@
This module is used to detect keyboard and mouse input using EVDEV layer, which is necessary on Wayland.
The module started as a port of this xkbcommon example
https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c

View File

@ -0,0 +1,47 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use scopeguard::ScopeGuard;
use super::ffi::{xkb_context, xkb_context_new, xkb_context_unref, XKB_CONTEXT_NO_FLAGS};
use anyhow::Result;
use thiserror::Error;
pub struct Context {
context: *mut xkb_context,
}
impl Context {
pub fn new() -> Result<Context> {
let raw_context = unsafe { xkb_context_new(XKB_CONTEXT_NO_FLAGS) };
let context = scopeguard::guard(raw_context, |raw_context| unsafe {
xkb_context_unref(raw_context);
});
if raw_context.is_null() {
return Err(ContextError::FailedCreation().into());
}
Ok(Self {
context: ScopeGuard::into_inner(context),
})
}
pub fn get_handle(&self) -> *mut xkb_context {
self.context
}
}
impl Drop for Context {
fn drop(&mut self) {
unsafe {
xkb_context_unref(self.context);
}
}
}
#[derive(Error, Debug)]
pub enum ContextError {
#[error("could not create xkb context")]
FailedCreation(),
}

View File

@ -0,0 +1,334 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use anyhow::Result;
use libc::{input_event, size_t, ssize_t, ENODEV, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_RDONLY};
use log::trace;
use scopeguard::ScopeGuard;
use std::collections::HashMap;
use std::os::raw::c_char;
use std::os::unix::io::AsRawFd;
use std::{
ffi::{c_void, CStr},
fs::OpenOptions,
};
use std::{fs::File, os::unix::fs::OpenOptionsExt};
use thiserror::Error;
use super::sync::ModifiersState;
use super::{
ffi::{
is_keyboard_or_mouse, xkb_key_direction, xkb_keycode_t, xkb_keymap_key_repeats, xkb_state,
xkb_state_get_keymap, xkb_state_key_get_one_sym, xkb_state_key_get_utf8, xkb_state_new,
xkb_state_unref, xkb_state_update_key, EV_KEY,
},
keymap::Keymap,
};
const EVDEV_OFFSET: i32 = 8;
pub const KEY_STATE_RELEASE: i32 = 0;
pub const KEY_STATE_PRESS: i32 = 1;
pub const KEY_STATE_REPEAT: i32 = 2;
#[derive(Debug)]
pub enum RawInputEvent {
Keyboard(RawKeyboardEvent),
Mouse(RawMouseEvent),
}
#[derive(Debug)]
pub struct RawKeyboardEvent {
pub sym: u32,
pub code: u32,
pub value: String,
pub state: i32,
}
#[derive(Debug)]
pub struct RawMouseEvent {
pub code: u16,
pub is_down: bool,
}
pub struct Device {
path: String,
file: File,
state: *mut xkb_state,
}
impl Device {
pub fn from(path: &str, keymap: &Keymap) -> Result<Device> {
let file = OpenOptions::new()
.read(true)
.custom_flags(O_NONBLOCK | O_CLOEXEC | O_RDONLY)
.open(&path)?;
if unsafe { is_keyboard_or_mouse(file.as_raw_fd()) == 0 } {
return Err(DeviceError::InvalidDevice(path.to_string()).into());
}
let raw_state = unsafe { xkb_state_new(keymap.get_handle()) };
// Automatically close the state if the function does not return correctly
let state = scopeguard::guard(raw_state, |raw_state| unsafe {
xkb_state_unref(raw_state);
});
if raw_state.is_null() {
return Err(DeviceError::InvalidState(path.to_string()).into());
}
Ok(Self {
path: path.to_string(),
file,
// Release the state without freeing it
state: ScopeGuard::into_inner(state),
})
}
pub fn get_state(&self) -> *mut xkb_state {
self.state
}
pub fn get_raw_fd(&self) -> i32 {
self.file.as_raw_fd()
}
pub fn get_path(&self) -> String {
self.path.to_string()
}
pub fn read(&self) -> Result<Vec<RawInputEvent>> {
let errno_ptr = unsafe { libc::__errno_location() };
let mut len: ssize_t;
let mut evs: [input_event; 16] = unsafe { std::mem::zeroed() };
let mut events = Vec::new();
loop {
len = unsafe {
libc::read(
self.file.as_raw_fd(),
evs.as_mut_ptr() as *mut c_void,
std::mem::size_of_val(&evs),
)
};
if len <= 0 {
break;
}
let nevs: size_t = len as usize / std::mem::size_of::<input_event>();
#[allow(clippy::needless_range_loop)]
for i in 0..nevs {
let event = self.process_event(evs[i].type_, evs[i].code, evs[i].value);
if let Some(event) = event {
events.push(event);
}
}
}
if len < 0 && unsafe { *errno_ptr } != EWOULDBLOCK {
if unsafe { *errno_ptr } == ENODEV {
return Err(DeviceError::FailedReadNoSuchDevice.into());
}
return Err(DeviceError::FailedRead(unsafe { *errno_ptr }).into());
}
Ok(events)
}
fn process_event(&self, _type: u16, code: u16, value: i32) -> Option<RawInputEvent> {
if _type != EV_KEY {
return None;
}
let is_down = value == KEY_STATE_PRESS;
// Check if the current event originated from a mouse
if (0x110..=0x117).contains(&code) {
// Mouse event
return Some(RawInputEvent::Mouse(RawMouseEvent { code, is_down }));
}
// Keyboard event
let keycode: xkb_keycode_t = EVDEV_OFFSET as u32 + code as u32;
let keymap = unsafe { xkb_state_get_keymap(self.get_state()) };
if value == KEY_STATE_REPEAT && unsafe { xkb_keymap_key_repeats(keymap, keycode) } == 0 {
return None;
}
let sym = unsafe { xkb_state_key_get_one_sym(self.get_state(), keycode) };
if sym == 0 {
return None;
}
// Extract the utf8 char
let mut buffer: [c_char; 16] = [0; 16];
unsafe {
xkb_state_key_get_utf8(
self.get_state(),
keycode,
buffer.as_mut_ptr(),
std::mem::size_of_val(&buffer),
)
};
let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr()) };
let content = content_raw.to_string_lossy().to_string();
let event = RawKeyboardEvent {
state: value,
code: keycode,
sym,
value: content,
};
if value == KEY_STATE_RELEASE {
unsafe { xkb_state_update_key(self.get_state(), keycode, xkb_key_direction::UP) };
} else {
unsafe { xkb_state_update_key(self.get_state(), keycode, xkb_key_direction::DOWN) };
}
Some(RawInputEvent::Keyboard(event))
}
pub fn update_key(&mut self, code: u32, pressed: bool) {
let direction = if pressed {
super::ffi::xkb_key_direction::DOWN
} else {
super::ffi::xkb_key_direction::UP
};
unsafe {
xkb_state_update_key(self.get_state(), code, direction);
}
}
pub fn update_modifier_state(
&mut self,
modifiers_state: &ModifiersState,
modifiers_map: &HashMap<String, u32>,
) {
if modifiers_state.alt {
self.update_key(
*modifiers_map
.get("alt")
.expect("unable to find modifiers key in map"),
true,
);
}
if modifiers_state.ctrl {
self.update_key(
*modifiers_map
.get("ctrl")
.expect("unable to find modifiers key in map"),
true,
);
}
if modifiers_state.meta {
self.update_key(
*modifiers_map
.get("meta")
.expect("unable to find modifiers key in map"),
true,
);
}
if modifiers_state.num_lock {
self.update_key(
*modifiers_map
.get("num_lock")
.expect("unable to find modifiers key in map"),
true,
);
self.update_key(
*modifiers_map
.get("num_lock")
.expect("unable to find modifiers key in map"),
false,
);
}
if modifiers_state.shift {
self.update_key(
*modifiers_map
.get("shift")
.expect("unable to find modifiers key in map"),
true,
);
}
if modifiers_state.caps_lock {
self.update_key(
*modifiers_map
.get("caps_lock")
.expect("unable to find modifiers key in map"),
true,
);
self.update_key(
*modifiers_map
.get("caps_lock")
.expect("unable to find modifiers key in map"),
false,
);
}
}
}
impl Drop for Device {
fn drop(&mut self) {
unsafe {
xkb_state_unref(self.state);
}
}
}
pub fn get_devices(keymap: &Keymap) -> Result<Vec<Device>> {
let mut keyboards = Vec::new();
let dirs = std::fs::read_dir("/dev/input/")?;
for entry in dirs {
match entry {
Ok(device) => {
// Skip non-eventX devices
if !device.file_name().to_string_lossy().starts_with("event") {
continue;
}
let path = device.path().to_string_lossy().to_string();
let keyboard = Device::from(&path, keymap);
match keyboard {
Ok(keyboard) => {
keyboards.push(keyboard);
}
Err(error) => {
trace!("error opening keyboard: {}", error);
}
}
}
Err(error) => {
trace!("could not read keyboard device: {}", error);
}
}
}
if keyboards.is_empty() {
return Err(DeviceError::NoDevicesFound().into());
}
Ok(keyboards)
}
#[derive(Error, Debug)]
pub enum DeviceError {
#[error("could not create xkb state for `{0}`")]
InvalidState(String),
#[error("`{0}` is not a valid device")]
InvalidDevice(String),
#[error("no devices found")]
NoDevicesFound(),
#[error("read operation failed with code: `{0}`")]
FailedRead(i32),
#[error("read operation failed: ENODEV No such device")]
FailedReadNoSuchDevice,
}

View File

@ -0,0 +1,78 @@
// Bindings taken from: https://github.com/rtbo/xkbcommon-rs/blob/master/src/xkb/ffi.rs
use std::os::raw::c_int;
use libc::c_char;
#[allow(non_camel_case_types)]
pub enum xkb_context {}
#[allow(non_camel_case_types)]
pub enum xkb_state {}
#[allow(non_camel_case_types)]
pub enum xkb_keymap {}
#[allow(non_camel_case_types)]
pub type xkb_keycode_t = u32;
#[allow(non_camel_case_types)]
pub type xkb_keysym_t = u32;
#[repr(C)]
pub struct xkb_rule_names {
pub rules: *const c_char,
pub model: *const c_char,
pub layout: *const c_char,
pub variant: *const c_char,
pub options: *const c_char,
}
#[repr(C)]
#[allow(clippy::upper_case_acronyms)]
pub enum xkb_key_direction {
UP,
DOWN,
}
#[allow(non_camel_case_types)]
pub type xkb_keymap_compile_flags = u32;
pub const XKB_KEYMAP_COMPILE_NO_FLAGS: u32 = 0;
#[allow(non_camel_case_types)]
pub type xkb_context_flags = u32;
pub const XKB_CONTEXT_NO_FLAGS: u32 = 0;
#[allow(non_camel_case_types)]
pub type xkb_state_component = u32;
pub const EV_KEY: u16 = 0x01;
#[link(name = "xkbcommon")]
extern "C" {
pub fn xkb_state_unref(state: *mut xkb_state);
pub fn xkb_state_new(keymap: *mut xkb_keymap) -> *mut xkb_state;
pub fn xkb_keymap_new_from_names(
context: *mut xkb_context,
names: *const xkb_rule_names,
flags: xkb_keymap_compile_flags,
) -> *mut xkb_keymap;
pub fn xkb_keymap_unref(keymap: *mut xkb_keymap);
pub fn xkb_context_new(flags: xkb_context_flags) -> *mut xkb_context;
pub fn xkb_context_unref(context: *mut xkb_context);
pub fn xkb_state_get_keymap(state: *mut xkb_state) -> *mut xkb_keymap;
pub fn xkb_keymap_key_repeats(keymap: *mut xkb_keymap, key: xkb_keycode_t) -> c_int;
pub fn xkb_state_update_key(
state: *mut xkb_state,
key: xkb_keycode_t,
direction: xkb_key_direction,
) -> xkb_state_component;
pub fn xkb_state_key_get_utf8(
state: *mut xkb_state,
key: xkb_keycode_t,
buffer: *mut c_char,
size: usize,
) -> c_int;
pub fn xkb_state_key_get_one_sym(state: *mut xkb_state, key: xkb_keycode_t) -> xkb_keysym_t;
}
#[link(name = "espansodetectevdev", kind = "static")]
extern "C" {
pub fn is_keyboard_or_mouse(fd: i32) -> i32;
}

View File

@ -0,0 +1,149 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use log::error;
use crate::{
event::{KeyboardEvent, Status},
hotkey::HotKey,
};
use std::{collections::HashMap, time::Instant};
use super::state::State;
// Number of milliseconds that define how long the hotkey memory
// should retain pressed keys
const HOTKEY_WINDOW_TIMEOUT: u128 = 5000;
pub type KeySym = u32;
pub type KeyCode = u32;
pub type HotkeyMemoryMap = Vec<(KeyCode, Instant)>;
pub struct HotKeyFilter {
map: HashMap<KeySym, KeyCode>,
memory: HotkeyMemoryMap,
hotkey_raw_map: HashMap<i32, Vec<KeyCode>>,
}
impl HotKeyFilter {
pub fn new() -> Self {
Self {
map: HashMap::new(),
memory: HotkeyMemoryMap::new(),
hotkey_raw_map: HashMap::new(),
}
}
pub fn initialize(&mut self, state: &State, hotkeys: &[HotKey]) {
// First load the map
self.map = HashMap::new();
for code in 0..256 {
if let Some(sym) = state.get_sym(code) {
self.map.insert(sym, code);
}
}
// Then the actual hotkeys
self.hotkey_raw_map = hotkeys
.iter()
.filter_map(|hk| {
let codes = Self::convert_hotkey_to_codes(self, hk);
if codes.is_none() {
error!("unable to register hotkey {:?}", hk);
}
Some((hk.id, codes?))
})
.collect();
}
pub fn process_event(&mut self, event: &KeyboardEvent) -> Option<i32> {
let mut hotkey = None;
let mut key_code = None;
let mut to_be_removed = Vec::new();
if event.status == Status::Released {
// Remove from the memory all the key occurrences
to_be_removed.extend(self.memory.iter().enumerate().filter_map(|(i, (code, _))| {
if *code == event.code {
Some(i)
} else {
None
}
}));
} else {
key_code = Some(event.code)
}
// Remove the old entries
to_be_removed.extend(
self
.memory
.iter()
.enumerate()
.filter_map(|(i, (_, instant))| {
if instant.elapsed().as_millis() > HOTKEY_WINDOW_TIMEOUT {
Some(i)
} else {
None
}
}),
);
// Remove duplicates and revert
if !to_be_removed.is_empty() {
#[allow(clippy::stable_sort_primitive)]
to_be_removed.sort();
to_be_removed.dedup();
to_be_removed.reverse();
to_be_removed.into_iter().for_each(|index| {
self.memory.remove(index);
})
}
if let Some(code) = key_code {
self.memory.push((code, Instant::now()));
for (id, codes) in self.hotkey_raw_map.iter() {
if codes
.iter()
.all(|hk_code| self.memory.iter().any(|(m_code, _)| m_code == hk_code))
{
hotkey = Some(*id);
break;
}
}
}
hotkey
}
fn convert_hotkey_to_codes(&self, hk: &HotKey) -> Option<Vec<KeyCode>> {
let mut codes = Vec::new();
let key_code = self.map.get(&hk.key.to_code()?)?;
codes.push(*key_code);
for modifier in hk.modifiers.iter() {
let code = self.map.get(&modifier.to_code()?)?;
codes.push(*code);
}
Some(codes)
}
}

View File

@ -0,0 +1,112 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use std::ffi::CString;
use scopeguard::ScopeGuard;
use anyhow::Result;
use thiserror::Error;
use crate::KeyboardConfig;
use super::{
context::Context,
ffi::{
xkb_keymap, xkb_keymap_new_from_names, xkb_keymap_unref, xkb_rule_names,
XKB_KEYMAP_COMPILE_NO_FLAGS,
},
};
pub struct Keymap {
keymap: *mut xkb_keymap,
}
impl Keymap {
pub fn new(context: &Context, rmlvo: Option<KeyboardConfig>) -> Result<Keymap> {
let owned_rmlvo = Self::generate_owned_rmlvo(rmlvo);
let names = Self::generate_names(&owned_rmlvo);
let raw_keymap = unsafe {
xkb_keymap_new_from_names(context.get_handle(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS)
};
let keymap = scopeguard::guard(raw_keymap, |raw_keymap| unsafe {
xkb_keymap_unref(raw_keymap);
});
if raw_keymap.is_null() {
return Err(KeymapError::FailedCreation().into());
}
Ok(Self {
keymap: ScopeGuard::into_inner(keymap),
})
}
pub fn get_handle(&self) -> *mut xkb_keymap {
self.keymap
}
fn generate_owned_rmlvo(rmlvo: Option<KeyboardConfig>) -> OwnedRawKeyboardConfig {
let rules = rmlvo
.as_ref()
.and_then(|config| config.rules.clone())
.unwrap_or_default();
let model = rmlvo
.as_ref()
.and_then(|config| config.model.clone())
.unwrap_or_default();
let layout = rmlvo
.as_ref()
.and_then(|config| config.layout.clone())
.unwrap_or_default();
let variant = rmlvo
.as_ref()
.and_then(|config| config.variant.clone())
.unwrap_or_default();
let options = rmlvo
.as_ref()
.and_then(|config| config.options.clone())
.unwrap_or_default();
OwnedRawKeyboardConfig {
rules: CString::new(rules).expect("unable to create CString for keymap"),
model: CString::new(model).expect("unable to create CString for keymap"),
layout: CString::new(layout).expect("unable to create CString for keymap"),
variant: CString::new(variant).expect("unable to create CString for keymap"),
options: CString::new(options).expect("unable to create CString for keymap"),
}
}
fn generate_names(owned_config: &OwnedRawKeyboardConfig) -> xkb_rule_names {
xkb_rule_names {
rules: owned_config.rules.as_ptr(),
model: owned_config.model.as_ptr(),
layout: owned_config.layout.as_ptr(),
variant: owned_config.variant.as_ptr(),
options: owned_config.options.as_ptr(),
}
}
}
impl Drop for Keymap {
fn drop(&mut self) {
unsafe {
xkb_keymap_unref(self.keymap);
}
}
}
#[derive(Error, Debug)]
pub enum KeymapError {
#[error("could not create xkb keymap")]
FailedCreation(),
}
struct OwnedRawKeyboardConfig {
rules: CString,
model: CString,
layout: CString,
variant: CString,
options: CString,
}

View File

@ -0,0 +1,460 @@
// This code is heavily inspired by the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
mod context;
mod device;
mod ffi;
mod hotkey;
mod keymap;
mod state;
mod sync;
use std::cell::RefCell;
use std::collections::HashMap;
use anyhow::{Context as AnyhowContext, Result};
use context::Context;
use device::{get_devices, Device};
use keymap::Keymap;
use lazycell::LazyCell;
use libc::{
__errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD,
EPOLL_CTL_DEL,
};
use log::{debug, error, info, trace, warn};
use thiserror::Error;
use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
use crate::event::{Key::*, MouseButton, MouseEvent};
use crate::{event::HotKeyEvent, event::Variant::*, hotkey::HotKey};
use crate::{event::Status::*, KeyboardConfig, Source, SourceCallback, SourceCreationOptions};
use self::{
device::{DeviceError, RawInputEvent, KEY_STATE_PRESS, KEY_STATE_RELEASE},
hotkey::HotKeyFilter,
state::State,
};
const BTN_LEFT: u16 = 0x110;
const BTN_RIGHT: u16 = 0x111;
const BTN_MIDDLE: u16 = 0x112;
const BTN_SIDE: u16 = 0x113;
const BTN_EXTRA: u16 = 0x114;
// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
// keycode set (where ESC is 9).
const EVDEV_OFFSET: u32 = 8;
// List of modifier keycodes, as defined in the "input-event-codes.h" header
// TODO: create an option to override them if needed
const KEY_CTRL: u32 = 29;
const KEY_SHIFT: u32 = 42;
const KEY_ALT: u32 = 56;
const KEY_META: u32 = 125;
const KEY_CAPSLOCK: u32 = 58;
const KEY_NUMLOCK: u32 = 69;
pub struct EVDEVSource {
devices: Vec<Device>,
hotkeys: Vec<HotKey>,
_keyboard_rmlvo: Option<KeyboardConfig>,
_context: LazyCell<Context>,
_keymap: LazyCell<Keymap>,
_hotkey_filter: RefCell<HotKeyFilter>,
_modifiers_map: HashMap<String, u32>,
}
#[allow(clippy::new_without_default)]
impl EVDEVSource {
pub fn new(options: SourceCreationOptions) -> EVDEVSource {
let mut modifiers_map = HashMap::new();
modifiers_map.insert("ctrl".to_string(), KEY_CTRL + EVDEV_OFFSET);
modifiers_map.insert("shift".to_string(), KEY_SHIFT + EVDEV_OFFSET);
modifiers_map.insert("alt".to_string(), KEY_ALT + EVDEV_OFFSET);
modifiers_map.insert("meta".to_string(), KEY_META + EVDEV_OFFSET);
modifiers_map.insert("caps_lock".to_string(), KEY_CAPSLOCK + EVDEV_OFFSET);
modifiers_map.insert("num_lock".to_string(), KEY_NUMLOCK + EVDEV_OFFSET);
Self {
devices: Vec::new(),
hotkeys: options.hotkeys,
_context: LazyCell::new(),
_keymap: LazyCell::new(),
_keyboard_rmlvo: options.evdev_keyboard_rmlvo,
_hotkey_filter: RefCell::new(HotKeyFilter::new()),
_modifiers_map: modifiers_map,
}
}
}
impl Source for EVDEVSource {
fn initialize(&mut self) -> Result<()> {
let context = Context::new().expect("unable to obtain xkb context");
let keymap =
Keymap::new(&context, self._keyboard_rmlvo.clone()).expect("unable to create xkb keymap");
match get_devices(&keymap) {
Ok(devices) => self.devices = devices,
Err(error) => {
if let Some(device_error) = error.downcast_ref::<DeviceError>() {
if matches!(device_error, DeviceError::NoDevicesFound()) {
error!("Unable to open EVDEV devices, this usually has to do with permissions.");
error!(
"You can either add the current user to the 'input' group or run espanso as root"
);
return Err(EVDEVSourceError::PermissionDenied().into());
}
}
return Err(error);
}
}
let state = State::new(&keymap)?;
info!("Querying modifier status...");
if let Some(modifiers_state) =
sync::get_modifiers_state().context("EVDEV modifier context state synchronization")?
{
debug!("Updating device modifier state: {:?}", modifiers_state);
for device in &mut self.devices {
device.update_modifier_state(&modifiers_state, &self._modifiers_map);
}
}
// Initialize the hotkeys
self
._hotkey_filter
.borrow_mut()
.initialize(&state, &self.hotkeys);
if self._context.fill(context).is_err() {
return Err(EVDEVSourceError::InitFailure().into());
}
if self._keymap.fill(keymap).is_err() {
return Err(EVDEVSourceError::InitFailure().into());
}
Ok(())
}
fn eventloop(&self, event_callback: SourceCallback) -> Result<()> {
if self.devices.is_empty() {
error!("can't start eventloop without evdev devices");
return Err(EVDEVSourceError::NoDevices().into());
}
let raw_epfd = unsafe { libc::epoll_create1(0) };
let epfd = scopeguard::guard(raw_epfd, |raw_epfd| unsafe {
close(raw_epfd);
});
if *epfd < 0 {
error!("could not create epoll instance");
return Err(EVDEVSourceError::Internal().into());
}
// Setup epoll for all input devices
let errno_ptr = unsafe { __errno_location() };
for (i, device) in self.devices.iter().enumerate() {
let mut ev: epoll_event = unsafe { std::mem::zeroed() };
ev.events = EPOLLIN as u32;
ev.u64 = i as u64;
if unsafe { epoll_ctl(*epfd, EPOLL_CTL_ADD, device.get_raw_fd(), &mut ev) } != 0 {
error!(
"Could not add {} to epoll, errno {}",
device.get_path(),
unsafe { *errno_ptr }
);
return Err(EVDEVSourceError::Internal().into());
}
}
let mut hotkey_filter = self._hotkey_filter.borrow_mut();
// Read events indefinitely
let mut evs: [epoll_event; 16] = unsafe { std::mem::zeroed() };
loop {
let ret = unsafe { epoll_wait(*epfd, evs.as_mut_ptr(), 16, -1) };
if ret < 0 {
if unsafe { *errno_ptr } == EINTR {
continue;
} else {
error!("Could not poll for events, {}", unsafe { *errno_ptr });
return Err(EVDEVSourceError::Internal().into());
}
}
#[allow(clippy::needless_range_loop)]
for i in 0usize..(ret as usize) {
let ev = evs[i];
let device = &self.devices[ev.u64 as usize];
match device.read() {
Ok(events) if !events.is_empty() => {
// Convert raw events to the common format and invoke the callback
events.into_iter().for_each(|raw_event| {
let event: Option<InputEvent> = raw_event.into();
if let Some(event) = event {
// On Wayland we need to detect the global shortcuts manually
if let InputEvent::Keyboard(key_event) = &event {
if let Some(hotkey) = (*hotkey_filter).process_event(key_event) {
event_callback(InputEvent::HotKey(HotKeyEvent { hotkey_id: hotkey }))
}
}
event_callback(event);
} else {
trace!("unable to convert raw event to input event");
}
});
}
Ok(_) => { /* SKIP EMPTY */ }
Err(err) => {
if let Some(DeviceError::FailedReadNoSuchDevice) = err.downcast_ref::<DeviceError>() {
warn!("Can't read from device {}, this error usually means the device has been disconnected, removing from epoll.", device.get_path());
if unsafe {
epoll_ctl(
*epfd,
EPOLL_CTL_DEL,
device.get_raw_fd(),
std::ptr::null_mut(),
)
} != 0
{
error!(
"Could not remove {} from epoll, errno {}",
device.get_path(),
unsafe { *errno_ptr }
);
return Err(EVDEVSourceError::Internal().into());
}
} else {
error!("Can't read from device {}: {}", device.get_path(), err)
}
}
}
}
}
}
}
#[derive(Error, Debug)]
pub enum EVDEVSourceError {
#[error("initialization failed")]
InitFailure(),
#[error("permission denied")]
PermissionDenied(),
#[error("no devices")]
NoDevices(),
#[error("internal error")]
Internal(),
}
impl From<RawInputEvent> for Option<InputEvent> {
fn from(raw: RawInputEvent) -> Option<InputEvent> {
match raw {
RawInputEvent::Keyboard(keyboard_event) => {
let (key, variant) = key_sym_to_key(keyboard_event.sym as i32);
let value = if keyboard_event.value.is_empty() {
None
} else {
Some(keyboard_event.value)
};
let status = if keyboard_event.state == KEY_STATE_PRESS {
Pressed
} else if keyboard_event.state == KEY_STATE_RELEASE {
Released
} else {
// Filter out the "repeated" events
return None;
};
return Some(InputEvent::Keyboard(KeyboardEvent {
key,
value,
status,
variant,
code: keyboard_event.code,
}));
}
RawInputEvent::Mouse(mouse_event) => {
let button = raw_to_mouse_button(mouse_event.code);
let status = if mouse_event.is_down {
Pressed
} else {
Released
};
if let Some(button) = button {
return Some(InputEvent::Mouse(MouseEvent { button, status }));
}
}
}
None
}
}
// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
fn key_sym_to_key(key_sym: i32) -> (Key, Option<Variant>) {
match key_sym {
// Modifiers
0xFFE9 => (Alt, Some(Left)),
0xFFEA => (Alt, Some(Right)),
0xFFE5 => (CapsLock, None),
0xFFE3 => (Control, Some(Left)),
0xFFE4 => (Control, Some(Right)),
0xFFE7 | 0xFFEB => (Meta, Some(Left)),
0xFFE8 | 0xFFEC => (Meta, Some(Right)),
0xFF7F => (NumLock, None),
0xFFE1 => (Shift, Some(Left)),
0xFFE2 => (Shift, Some(Right)),
// Whitespace
0xFF0D => (Enter, None),
0xFF09 => (Tab, None),
0x20 => (Space, None),
// Navigation
0xFF54 => (ArrowDown, None),
0xFF51 => (ArrowLeft, None),
0xFF53 => (ArrowRight, None),
0xFF52 => (ArrowUp, None),
0xFF57 => (End, None),
0xFF50 => (Home, None),
0xFF56 => (PageDown, None),
0xFF55 => (PageUp, None),
// UI
0xFF1B => (Escape, None),
// Editing keys
0xFF08 => (Backspace, None),
// Function keys
0xFFBE => (F1, None),
0xFFBF => (F2, None),
0xFFC0 => (F3, None),
0xFFC1 => (F4, None),
0xFFC2 => (F5, None),
0xFFC3 => (F6, None),
0xFFC4 => (F7, None),
0xFFC5 => (F8, None),
0xFFC6 => (F9, None),
0xFFC7 => (F10, None),
0xFFC8 => (F11, None),
0xFFC9 => (F12, None),
0xFFCA => (F13, None),
0xFFCB => (F14, None),
0xFFCC => (F15, None),
0xFFCD => (F16, None),
0xFFCE => (F17, None),
0xFFCF => (F18, None),
0xFFD0 => (F19, None),
0xFFD1 => (F20, None),
// Numpad
0xFFB0 => (Numpad0, None),
0xFFB1 => (Numpad1, None),
0xFFB2 => (Numpad2, None),
0xFFB3 => (Numpad3, None),
0xFFB4 => (Numpad4, None),
0xFFB5 => (Numpad5, None),
0xFFB6 => (Numpad6, None),
0xFFB7 => (Numpad7, None),
0xFFB8 => (Numpad8, None),
0xFFB9 => (Numpad9, None),
// Other keys, includes the raw code provided by the operating system
_ => (Other(key_sym), None),
}
}
// These codes can be found in the "input-event-codes.h" header file
fn raw_to_mouse_button(raw: u16) -> Option<MouseButton> {
match raw {
BTN_LEFT => Some(MouseButton::Left),
BTN_RIGHT => Some(MouseButton::Right),
BTN_MIDDLE => Some(MouseButton::Middle),
BTN_SIDE => Some(MouseButton::Button1),
BTN_EXTRA => Some(MouseButton::Button2),
_ => None,
}
}
#[cfg(test)]
mod tests {
use device::RawMouseEvent;
use crate::event::{InputEvent, Key::Other, KeyboardEvent};
use super::{
device::{RawInputEvent, RawKeyboardEvent},
*,
};
#[test]
fn raw_to_input_event_keyboard_works_correctly() {
let raw = RawInputEvent::Keyboard(RawKeyboardEvent {
sym: 0x4B,
value: "k".to_owned(),
state: KEY_STATE_RELEASE,
code: 0,
});
let result: Option<InputEvent> = raw.into();
assert_eq!(
result.unwrap(),
InputEvent::Keyboard(KeyboardEvent {
key: Other(0x4B),
status: Released,
value: Some("k".to_string()),
variant: None,
code: 0,
})
);
}
#[test]
fn raw_to_input_event_mouse_works_correctly() {
let raw = RawInputEvent::Mouse(RawMouseEvent {
code: BTN_RIGHT,
is_down: false,
});
let result: Option<InputEvent> = raw.into();
assert_eq!(
result.unwrap(),
InputEvent::Mouse(MouseEvent {
status: Released,
button: MouseButton::Right,
})
);
}
}

View File

@ -0,0 +1,90 @@
// A good portion of the following code has been taken by the "interactive-evdev.c"
// example of "libxkbcommon" by Ran Benita. The original license is included as follows:
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
/*
* Copyright © 2012 Ran Benita <ran234@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "native.h"
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <getopt.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string>
#include <sys/epoll.h>
#include <linux/input.h>
#include "xkbcommon/xkbcommon.h"
#define NLONGS(n) (((n) + LONG_BIT - 1) / LONG_BIT)
static bool
evdev_bit_is_set(const unsigned long *array, int bit)
{
return array[bit / LONG_BIT] & (1LL << (bit % LONG_BIT));
}
/* Some heuristics to see if the device is a keyboard. */
int32_t is_keyboard_or_mouse(int fd)
{
int i;
unsigned long evbits[NLONGS(EV_CNT)] = {0};
unsigned long keybits[NLONGS(KEY_CNT)] = {0};
errno = 0;
ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), evbits);
if (errno)
return false;
if (!evdev_bit_is_set(evbits, EV_KEY))
return false;
errno = 0;
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keybits)), keybits);
if (errno)
return false;
// Test for keyboard keys
for (i = KEY_RESERVED; i <= KEY_MIN_INTERESTING; i++)
if (evdev_bit_is_set(keybits, i))
return true;
// Test for mouse keys
for (i = BTN_MOUSE; i <= BTN_TASK; i++)
if (evdev_bit_is_set(keybits, i))
return true;
return false;
}

View File

@ -0,0 +1,27 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ESPANSO_DETECT_EVDEV_H
#define ESPANSO_DETECT_EVDEV_H
#include <stdint.h>
extern "C" int32_t is_keyboard_or_mouse(int fd);
#endif //ESPANSO_DETECT_EVDEV_H

View File

@ -0,0 +1,56 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use scopeguard::ScopeGuard;
use anyhow::Result;
use thiserror::Error;
use super::{
ffi::{xkb_state, xkb_state_key_get_one_sym, xkb_state_new, xkb_state_unref},
keymap::Keymap,
};
pub struct State {
state: *mut xkb_state,
}
impl State {
pub fn new(keymap: &Keymap) -> Result<State> {
let raw_state = unsafe { xkb_state_new(keymap.get_handle()) };
let state = scopeguard::guard(raw_state, |raw_state| unsafe {
xkb_state_unref(raw_state);
});
if raw_state.is_null() {
return Err(StateError::FailedCreation().into());
}
Ok(Self {
state: ScopeGuard::into_inner(state),
})
}
pub fn get_sym(&self, code: u32) -> Option<u32> {
let sym = unsafe { xkb_state_key_get_one_sym(self.state, code) };
if sym == 0 {
None
} else {
Some(sym)
}
}
}
impl Drop for State {
fn drop(&mut self) {
unsafe {
xkb_state_unref(self.state);
}
}
}
#[derive(Error, Debug)]
pub enum StateError {
#[error("could not create xkb state")]
FailedCreation(),
}

View File

@ -0,0 +1,39 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#[derive(Debug, Clone, Copy)]
pub struct ModifiersState {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub caps_lock: bool,
pub meta: bool,
pub num_lock: bool,
}
#[cfg(feature = "wayland")]
mod wayland;
#[cfg(feature = "wayland")]
pub use wayland::get_modifiers_state;
#[cfg(not(feature = "wayland"))]
pub fn get_modifiers_state() -> anyhow::Result<Option<ModifiersState>> {
// Fallback for non-wayland systems
Ok(None)
}

View File

@ -0,0 +1,248 @@
// This module was implemented starting from this wonderful example:
// https://github.com/Smithay/client-toolkit/blob/master/examples/kbd_input.rs
use std::cell::RefCell;
use std::cmp::min;
use std::rc::Rc;
use anyhow::{Context, Result};
use log::error;
use sctk::reexports::calloop;
use sctk::reexports::client::protocol::{wl_keyboard, wl_shm, wl_surface};
use sctk::seat::keyboard::{map_keyboard_repeat, Event as KbEvent, RepeatKind};
use sctk::shm::AutoMemPool;
use sctk::window::{Event as WEvent, FallbackFrame};
sctk::default_environment!(EspansoModifiersSync, desktop);
pub fn get_modifiers_state() -> Result<Option<super::ModifiersState>> {
let (env, display, queue) = sctk::new_default_environment!(EspansoModifiersSync, desktop)
.context("Unable to connect to a Wayland compositor")?;
let result = Rc::new(RefCell::new(None));
/*
* Prepare a calloop event loop to handle key repetion
*/
// Here `Option<WEvent>` is the type of a global value that will be shared by
// all callbacks invoked by the event loop.
let mut event_loop = calloop::EventLoop::<Option<WEvent>>::try_new().unwrap();
/*
* Create a buffer with window contents
*/
let mut dimensions = (1u32, 1u32);
/*
* Init wayland objects
*/
let surface = env.create_surface().detach();
let mut window = env
.create_window::<FallbackFrame, _>(surface, None, dimensions, move |evt, mut dispatch_data| {
let next_action = dispatch_data.get::<Option<WEvent>>().unwrap();
// Keep last event in priority order : Close > Configure > Refresh
let replace = matches!(
(&evt, &*next_action),
(_, &None)
| (_, &Some(WEvent::Refresh))
| (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. }))
| (&WEvent::Close, _)
);
if replace {
*next_action = Some(evt);
}
})
.context("Failed to create a window !")?;
window.set_title("Espanso Sync Tool".to_string());
let mut pool = env
.create_auto_pool()
.context("Failed to create a memory pool !")?;
/*
* Keyboard initialization
*/
let mut seats = Vec::<(
String,
Option<(wl_keyboard::WlKeyboard, calloop::RegistrationToken)>,
)>::new();
// first process already existing seats
for seat in env.get_all_seats() {
if let Some((has_kbd, name)) = sctk::seat::with_seat_data(&seat, |seat_data| {
(
seat_data.has_keyboard && !seat_data.defunct,
seat_data.name.clone(),
)
}) {
if has_kbd {
let result_clone = result.clone();
match map_keyboard_repeat(
event_loop.handle(),
&seat,
None,
RepeatKind::System,
move |event, _, _| keyboard_event_handler(event, &result_clone),
) {
Ok((kbd, repeat_source)) => {
seats.push((name, Some((kbd, repeat_source))));
}
Err(e) => {
error!("Failed to map keyboard on seat {} : {:?}.", name, e);
seats.push((name, None));
}
}
} else {
seats.push((name, None));
}
}
}
// then setup a listener for changes
let loop_handle = event_loop.handle();
let result_clone = result.clone();
let _seat_listener = env.listen_for_seats(move |seat, seat_data, _| {
let result_clone = result_clone.clone();
// find the seat in the vec of seats, or insert it if it is unknown
let idx = seats.iter().position(|(name, _)| name == &seat_data.name);
let idx = idx.unwrap_or_else(|| {
seats.push((seat_data.name.clone(), None));
seats.len() - 1
});
let (_, ref mut opt_kbd) = &mut seats[idx];
// we should map a keyboard if the seat has the capability & is not defunct
if seat_data.has_keyboard && !seat_data.defunct {
if opt_kbd.is_none() {
// we should initalize a keyboard
match map_keyboard_repeat(
loop_handle.clone(),
&seat,
None,
RepeatKind::System,
move |event, _, _| keyboard_event_handler(event, &result_clone),
) {
Ok((kbd, repeat_source)) => {
*opt_kbd = Some((kbd, repeat_source));
}
Err(e) => {
eprintln!(
"Failed to map keyboard on seat {} : {:?}.",
seat_data.name, e
)
}
}
}
} else if let Some((kbd, source)) = opt_kbd.take() {
// the keyboard has been removed, cleanup
kbd.release();
loop_handle.remove(source);
}
});
if !env.get_shell().unwrap().needs_configure() {
// initial draw to bootstrap on wl_shell
redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
window.refresh();
}
sctk::WaylandSource::new(queue)
.quick_insert(event_loop.handle())
.unwrap();
let mut next_action = None;
loop {
match next_action.take() {
Some(WEvent::Close) => break,
Some(WEvent::Refresh) => {
window.refresh();
window.surface().commit();
}
Some(WEvent::Configure {
new_size,
states: _,
}) => {
if let Some((w, h)) = new_size {
window.resize(w, h);
dimensions = (w, h)
}
window.refresh();
redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
}
None => {
let result_clone = result.clone();
let result_ref = result_clone.borrow();
if let Some(result) = &*result_ref {
return Ok(Some(*result));
}
}
}
// always flush the connection before going to sleep waiting for events
display.flush().unwrap();
event_loop
.dispatch(Some(std::time::Duration::from_millis(10)), &mut next_action)
.unwrap();
}
Ok(None)
}
fn keyboard_event_handler(
event: KbEvent,
result_clone: &Rc<RefCell<Option<super::ModifiersState>>>,
) {
if let KbEvent::Modifiers { modifiers } = event {
let mut result_mut = (**result_clone).borrow_mut();
*result_mut = Some(super::ModifiersState {
ctrl: modifiers.ctrl,
alt: modifiers.alt,
shift: modifiers.shift,
caps_lock: modifiers.caps_lock,
meta: modifiers.logo,
num_lock: modifiers.num_lock,
})
}
}
#[allow(clippy::many_single_char_names)]
fn redraw(
pool: &mut AutoMemPool,
surface: &wl_surface::WlSurface,
(buf_x, buf_y): (u32, u32),
) -> Result<(), ::std::io::Error> {
let (canvas, new_buffer) = pool.buffer(
buf_x as i32,
buf_y as i32,
4 * buf_x as i32,
wl_shm::Format::Argb8888,
)?;
for (i, dst_pixel) in canvas.chunks_exact_mut(4).enumerate() {
let x = i as u32 % buf_x;
let y = i as u32 / buf_x;
let r: u32 = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
let g: u32 = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
let b: u32 = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y);
let pixel: [u8; 4] = ((0xFF << 24) + (r << 16) + (g << 8) + b).to_ne_bytes();
dst_pixel[0] = pixel[0];
dst_pixel[1] = pixel[1];
dst_pixel[2] = pixel[2];
dst_pixel[3] = pixel[3];
}
surface.attach(Some(&new_buffer), 0, 0);
if surface.as_ref().version() >= 4 {
surface.damage_buffer(0, 0, buf_x as i32, buf_y as i32);
} else {
surface.damage(0, 0, buf_x as i32, buf_y as i32);
}
surface.commit();
Ok(())
}

147
espanso-detect/src/event.rs Normal file
View File

@ -0,0 +1,147 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#[cfg(test)]
use enum_as_inner::EnumAsInner;
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(test, derive(EnumAsInner))]
pub enum InputEvent {
Mouse(MouseEvent),
Keyboard(KeyboardEvent),
HotKey(HotKeyEvent),
// Special event type only used on macOS
// This is sent after a global keyboard shortcut is released
// See https://github.com/federico-terzi/espanso/issues/791
AllModifiersReleased,
}
#[derive(Debug, PartialEq, Eq)]
pub enum MouseButton {
Left,
Right,
Middle,
Button1,
Button2,
Button3,
Button4,
Button5,
}
#[derive(Debug, PartialEq, Eq)]
pub struct MouseEvent {
pub button: MouseButton,
pub status: Status,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Status {
Pressed,
Released,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Variant {
Left,
Right,
}
#[derive(Debug, PartialEq, Eq)]
pub struct KeyboardEvent {
pub key: Key,
pub value: Option<String>,
pub status: Status,
pub variant: Option<Variant>,
pub code: u32,
}
// A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
#[derive(Debug, PartialEq, Eq)]
pub enum Key {
// Modifiers
Alt,
CapsLock,
Control,
Meta,
NumLock,
Shift,
// Whitespace
Enter,
Tab,
Space,
// Navigation
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
End,
Home,
PageDown,
PageUp,
// UI
Escape,
// Editing keys
Backspace,
// Function keys
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
// Numpad keys
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
// Other keys, includes the raw code provided by the operating system
Other(i32),
}
#[derive(Debug, PartialEq, Eq)]
pub struct HotKeyEvent {
pub hotkey_id: i32,
}

View File

@ -0,0 +1,628 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::fmt::Display;
use regex::Regex;
lazy_static! {
static ref RAW_PARSER: Regex = Regex::new(r"^RAW\((\d+)\)$").unwrap();
}
#[derive(Debug, PartialEq, Clone, Eq)]
pub enum ShortcutKey {
Alt,
Control,
Meta,
Shift,
Enter,
Tab,
Space,
Insert,
// Navigation
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
End,
Home,
PageDown,
PageUp,
// Function ShortcutKeys
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
// Alphabet
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
// Numbers
N0,
N1,
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
// Numpad
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
// Specify the raw platform-specific virtual key code.
Raw(u32),
}
impl Display for ShortcutKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
ShortcutKey::Alt => write!(f, "ALT"),
ShortcutKey::Control => write!(f, "CTRL"),
ShortcutKey::Meta => write!(f, "META"),
ShortcutKey::Shift => write!(f, "SHIFT"),
ShortcutKey::Enter => write!(f, "ENTER"),
ShortcutKey::Tab => write!(f, "TAB"),
ShortcutKey::Space => write!(f, "SPACE"),
ShortcutKey::Insert => write!(f, "INSERT"),
ShortcutKey::ArrowDown => write!(f, "DOWN"),
ShortcutKey::ArrowLeft => write!(f, "LEFT"),
ShortcutKey::ArrowRight => write!(f, "RIGHT"),
ShortcutKey::ArrowUp => write!(f, "UP"),
ShortcutKey::End => write!(f, "END"),
ShortcutKey::Home => write!(f, "HOME"),
ShortcutKey::PageDown => write!(f, "PAGEDOWN"),
ShortcutKey::PageUp => write!(f, "PAGEUP"),
ShortcutKey::F1 => write!(f, "F1"),
ShortcutKey::F2 => write!(f, "F2"),
ShortcutKey::F3 => write!(f, "F3"),
ShortcutKey::F4 => write!(f, "F4"),
ShortcutKey::F5 => write!(f, "F5"),
ShortcutKey::F6 => write!(f, "F6"),
ShortcutKey::F7 => write!(f, "F7"),
ShortcutKey::F8 => write!(f, "F8"),
ShortcutKey::F9 => write!(f, "F9"),
ShortcutKey::F10 => write!(f, "F10"),
ShortcutKey::F11 => write!(f, "F11"),
ShortcutKey::F12 => write!(f, "F12"),
ShortcutKey::F13 => write!(f, "F13"),
ShortcutKey::F14 => write!(f, "F14"),
ShortcutKey::F15 => write!(f, "F15"),
ShortcutKey::F16 => write!(f, "F16"),
ShortcutKey::F17 => write!(f, "F17"),
ShortcutKey::F18 => write!(f, "F18"),
ShortcutKey::F19 => write!(f, "F19"),
ShortcutKey::F20 => write!(f, "F20"),
ShortcutKey::A => write!(f, "A"),
ShortcutKey::B => write!(f, "B"),
ShortcutKey::C => write!(f, "C"),
ShortcutKey::D => write!(f, "D"),
ShortcutKey::E => write!(f, "E"),
ShortcutKey::F => write!(f, "F"),
ShortcutKey::G => write!(f, "G"),
ShortcutKey::H => write!(f, "H"),
ShortcutKey::I => write!(f, "I"),
ShortcutKey::J => write!(f, "J"),
ShortcutKey::K => write!(f, "K"),
ShortcutKey::L => write!(f, "L"),
ShortcutKey::M => write!(f, "M"),
ShortcutKey::N => write!(f, "N"),
ShortcutKey::O => write!(f, "O"),
ShortcutKey::P => write!(f, "P"),
ShortcutKey::Q => write!(f, "Q"),
ShortcutKey::R => write!(f, "R"),
ShortcutKey::S => write!(f, "S"),
ShortcutKey::T => write!(f, "T"),
ShortcutKey::U => write!(f, "U"),
ShortcutKey::V => write!(f, "V"),
ShortcutKey::W => write!(f, "W"),
ShortcutKey::X => write!(f, "X"),
ShortcutKey::Y => write!(f, "Y"),
ShortcutKey::Z => write!(f, "Z"),
ShortcutKey::N0 => write!(f, "0"),
ShortcutKey::N1 => write!(f, "1"),
ShortcutKey::N2 => write!(f, "2"),
ShortcutKey::N3 => write!(f, "3"),
ShortcutKey::N4 => write!(f, "4"),
ShortcutKey::N5 => write!(f, "5"),
ShortcutKey::N6 => write!(f, "6"),
ShortcutKey::N7 => write!(f, "7"),
ShortcutKey::N8 => write!(f, "8"),
ShortcutKey::N9 => write!(f, "9"),
ShortcutKey::Numpad0 => write!(f, "NUMPAD0"),
ShortcutKey::Numpad1 => write!(f, "NUMPAD1"),
ShortcutKey::Numpad2 => write!(f, "NUMPAD2"),
ShortcutKey::Numpad3 => write!(f, "NUMPAD3"),
ShortcutKey::Numpad4 => write!(f, "NUMPAD4"),
ShortcutKey::Numpad5 => write!(f, "NUMPAD5"),
ShortcutKey::Numpad6 => write!(f, "NUMPAD6"),
ShortcutKey::Numpad7 => write!(f, "NUMPAD7"),
ShortcutKey::Numpad8 => write!(f, "NUMPAD8"),
ShortcutKey::Numpad9 => write!(f, "NUMPAD9"),
ShortcutKey::Raw(code) => write!(f, "RAW({})", code),
}
}
}
impl ShortcutKey {
pub fn parse(key: &str) -> Option<ShortcutKey> {
let parsed = match key {
"ALT" | "OPTION" => Some(ShortcutKey::Alt),
"CTRL" => Some(ShortcutKey::Control),
"META" | "CMD" => Some(ShortcutKey::Meta),
"SHIFT" => Some(ShortcutKey::Shift),
"ENTER" => Some(ShortcutKey::Enter),
"TAB" => Some(ShortcutKey::Tab),
"SPACE" => Some(ShortcutKey::Space),
"INSERT" => Some(ShortcutKey::Insert),
"DOWN" => Some(ShortcutKey::ArrowDown),
"LEFT" => Some(ShortcutKey::ArrowLeft),
"RIGHT" => Some(ShortcutKey::ArrowRight),
"UP" => Some(ShortcutKey::ArrowUp),
"END" => Some(ShortcutKey::End),
"HOME" => Some(ShortcutKey::Home),
"PAGEDOWN" => Some(ShortcutKey::PageDown),
"PAGEUP" => Some(ShortcutKey::PageUp),
"F1" => Some(ShortcutKey::F1),
"F2" => Some(ShortcutKey::F2),
"F3" => Some(ShortcutKey::F3),
"F4" => Some(ShortcutKey::F4),
"F5" => Some(ShortcutKey::F5),
"F6" => Some(ShortcutKey::F6),
"F7" => Some(ShortcutKey::F7),
"F8" => Some(ShortcutKey::F8),
"F9" => Some(ShortcutKey::F9),
"F10" => Some(ShortcutKey::F10),
"F11" => Some(ShortcutKey::F11),
"F12" => Some(ShortcutKey::F12),
"F13" => Some(ShortcutKey::F13),
"F14" => Some(ShortcutKey::F14),
"F15" => Some(ShortcutKey::F15),
"F16" => Some(ShortcutKey::F16),
"F17" => Some(ShortcutKey::F17),
"F18" => Some(ShortcutKey::F18),
"F19" => Some(ShortcutKey::F19),
"F20" => Some(ShortcutKey::F20),
"A" => Some(ShortcutKey::A),
"B" => Some(ShortcutKey::B),
"C" => Some(ShortcutKey::C),
"D" => Some(ShortcutKey::D),
"E" => Some(ShortcutKey::E),
"F" => Some(ShortcutKey::F),
"G" => Some(ShortcutKey::G),
"H" => Some(ShortcutKey::H),
"I" => Some(ShortcutKey::I),
"J" => Some(ShortcutKey::J),
"K" => Some(ShortcutKey::K),
"L" => Some(ShortcutKey::L),
"M" => Some(ShortcutKey::M),
"N" => Some(ShortcutKey::N),
"O" => Some(ShortcutKey::O),
"P" => Some(ShortcutKey::P),
"Q" => Some(ShortcutKey::Q),
"R" => Some(ShortcutKey::R),
"S" => Some(ShortcutKey::S),
"T" => Some(ShortcutKey::T),
"U" => Some(ShortcutKey::U),
"V" => Some(ShortcutKey::V),
"W" => Some(ShortcutKey::W),
"X" => Some(ShortcutKey::X),
"Y" => Some(ShortcutKey::Y),
"Z" => Some(ShortcutKey::Z),
"0" => Some(ShortcutKey::N0),
"1" => Some(ShortcutKey::N1),
"2" => Some(ShortcutKey::N2),
"3" => Some(ShortcutKey::N3),
"4" => Some(ShortcutKey::N4),
"5" => Some(ShortcutKey::N5),
"6" => Some(ShortcutKey::N6),
"7" => Some(ShortcutKey::N7),
"8" => Some(ShortcutKey::N8),
"9" => Some(ShortcutKey::N9),
"NUMPAD0" => Some(ShortcutKey::Numpad0),
"NUMPAD1" => Some(ShortcutKey::Numpad1),
"NUMPAD2" => Some(ShortcutKey::Numpad2),
"NUMPAD3" => Some(ShortcutKey::Numpad3),
"NUMPAD4" => Some(ShortcutKey::Numpad4),
"NUMPAD5" => Some(ShortcutKey::Numpad5),
"NUMPAD6" => Some(ShortcutKey::Numpad6),
"NUMPAD7" => Some(ShortcutKey::Numpad7),
"NUMPAD8" => Some(ShortcutKey::Numpad8),
"NUMPAD9" => Some(ShortcutKey::Numpad9),
_ => None,
};
if parsed.is_none() {
// Attempt to parse raw ShortcutKeys
if RAW_PARSER.is_match(key) {
if let Some(caps) = RAW_PARSER.captures(key) {
let code_str = caps.get(1).map_or("", |m| m.as_str());
let code = code_str.parse::<u32>();
if let Ok(code) = code {
return Some(ShortcutKey::Raw(code));
}
}
}
}
parsed
}
// macOS keycodes
#[cfg(target_os = "macos")]
pub fn to_code(&self) -> Option<u32> {
match self {
ShortcutKey::Alt => Some(0x3A),
ShortcutKey::Control => Some(0x3B),
ShortcutKey::Meta => Some(0x37),
ShortcutKey::Shift => Some(0x38),
ShortcutKey::Enter => Some(0x24),
ShortcutKey::Tab => Some(0x30),
ShortcutKey::Space => Some(0x31),
ShortcutKey::ArrowDown => Some(0x7D),
ShortcutKey::ArrowLeft => Some(0x7B),
ShortcutKey::ArrowRight => Some(0x7C),
ShortcutKey::ArrowUp => Some(0x7E),
ShortcutKey::End => Some(0x77),
ShortcutKey::Home => Some(0x73),
ShortcutKey::PageDown => Some(0x79),
ShortcutKey::PageUp => Some(0x74),
ShortcutKey::Insert => None,
ShortcutKey::F1 => Some(0x7A),
ShortcutKey::F2 => Some(0x78),
ShortcutKey::F3 => Some(0x63),
ShortcutKey::F4 => Some(0x76),
ShortcutKey::F5 => Some(0x60),
ShortcutKey::F6 => Some(0x61),
ShortcutKey::F7 => Some(0x62),
ShortcutKey::F8 => Some(0x64),
ShortcutKey::F9 => Some(0x65),
ShortcutKey::F10 => Some(0x6D),
ShortcutKey::F11 => Some(0x67),
ShortcutKey::F12 => Some(0x6F),
ShortcutKey::F13 => Some(0x69),
ShortcutKey::F14 => Some(0x6B),
ShortcutKey::F15 => Some(0x71),
ShortcutKey::F16 => Some(0x6A),
ShortcutKey::F17 => Some(0x40),
ShortcutKey::F18 => Some(0x4F),
ShortcutKey::F19 => Some(0x50),
ShortcutKey::F20 => Some(0x5A),
ShortcutKey::A => Some(0x00),
ShortcutKey::B => Some(0x0B),
ShortcutKey::C => Some(0x08),
ShortcutKey::D => Some(0x02),
ShortcutKey::E => Some(0x0E),
ShortcutKey::F => Some(0x03),
ShortcutKey::G => Some(0x05),
ShortcutKey::H => Some(0x04),
ShortcutKey::I => Some(0x22),
ShortcutKey::J => Some(0x26),
ShortcutKey::K => Some(0x28),
ShortcutKey::L => Some(0x25),
ShortcutKey::M => Some(0x2E),
ShortcutKey::N => Some(0x2D),
ShortcutKey::O => Some(0x1F),
ShortcutKey::P => Some(0x23),
ShortcutKey::Q => Some(0x0C),
ShortcutKey::R => Some(0x0F),
ShortcutKey::S => Some(0x01),
ShortcutKey::T => Some(0x11),
ShortcutKey::U => Some(0x20),
ShortcutKey::V => Some(0x09),
ShortcutKey::W => Some(0x0D),
ShortcutKey::X => Some(0x07),
ShortcutKey::Y => Some(0x10),
ShortcutKey::Z => Some(0x06),
ShortcutKey::N0 => Some(0x1D),
ShortcutKey::N1 => Some(0x12),
ShortcutKey::N2 => Some(0x13),
ShortcutKey::N3 => Some(0x14),
ShortcutKey::N4 => Some(0x15),
ShortcutKey::N5 => Some(0x17),
ShortcutKey::N6 => Some(0x16),
ShortcutKey::N7 => Some(0x1A),
ShortcutKey::N8 => Some(0x1C),
ShortcutKey::N9 => Some(0x19),
ShortcutKey::Numpad0 => Some(0x52),
ShortcutKey::Numpad1 => Some(0x53),
ShortcutKey::Numpad2 => Some(0x54),
ShortcutKey::Numpad3 => Some(0x55),
ShortcutKey::Numpad4 => Some(0x56),
ShortcutKey::Numpad5 => Some(0x57),
ShortcutKey::Numpad6 => Some(0x58),
ShortcutKey::Numpad7 => Some(0x59),
ShortcutKey::Numpad8 => Some(0x5B),
ShortcutKey::Numpad9 => Some(0x5C),
ShortcutKey::Raw(code) => Some(*code),
}
}
// Windows key codes
#[cfg(target_os = "windows")]
pub fn to_code(&self) -> Option<u32> {
let vkey = match self {
ShortcutKey::Alt => 0x12,
ShortcutKey::Control => 0x11,
ShortcutKey::Meta => 0x5B,
ShortcutKey::Shift => 0xA0,
ShortcutKey::Enter => 0x0D,
ShortcutKey::Tab => 0x09,
ShortcutKey::Space => 0x20,
ShortcutKey::ArrowDown => 0x28,
ShortcutKey::ArrowLeft => 0x25,
ShortcutKey::ArrowRight => 0x27,
ShortcutKey::ArrowUp => 0x26,
ShortcutKey::End => 0x23,
ShortcutKey::Home => 0x24,
ShortcutKey::PageDown => 0x22,
ShortcutKey::PageUp => 0x21,
ShortcutKey::Insert => 0x2D,
ShortcutKey::F1 => 0x70,
ShortcutKey::F2 => 0x71,
ShortcutKey::F3 => 0x72,
ShortcutKey::F4 => 0x73,
ShortcutKey::F5 => 0x74,
ShortcutKey::F6 => 0x75,
ShortcutKey::F7 => 0x76,
ShortcutKey::F8 => 0x77,
ShortcutKey::F9 => 0x78,
ShortcutKey::F10 => 0x79,
ShortcutKey::F11 => 0x7A,
ShortcutKey::F12 => 0x7B,
ShortcutKey::F13 => 0x7C,
ShortcutKey::F14 => 0x7D,
ShortcutKey::F15 => 0x7E,
ShortcutKey::F16 => 0x7F,
ShortcutKey::F17 => 0x80,
ShortcutKey::F18 => 0x81,
ShortcutKey::F19 => 0x82,
ShortcutKey::F20 => 0x83,
ShortcutKey::A => 0x41,
ShortcutKey::B => 0x42,
ShortcutKey::C => 0x43,
ShortcutKey::D => 0x44,
ShortcutKey::E => 0x45,
ShortcutKey::F => 0x46,
ShortcutKey::G => 0x47,
ShortcutKey::H => 0x48,
ShortcutKey::I => 0x49,
ShortcutKey::J => 0x4A,
ShortcutKey::K => 0x4B,
ShortcutKey::L => 0x4C,
ShortcutKey::M => 0x4D,
ShortcutKey::N => 0x4E,
ShortcutKey::O => 0x4F,
ShortcutKey::P => 0x50,
ShortcutKey::Q => 0x51,
ShortcutKey::R => 0x52,
ShortcutKey::S => 0x53,
ShortcutKey::T => 0x54,
ShortcutKey::U => 0x55,
ShortcutKey::V => 0x56,
ShortcutKey::W => 0x57,
ShortcutKey::X => 0x58,
ShortcutKey::Y => 0x59,
ShortcutKey::Z => 0x5A,
ShortcutKey::N0 => 0x30,
ShortcutKey::N1 => 0x31,
ShortcutKey::N2 => 0x32,
ShortcutKey::N3 => 0x33,
ShortcutKey::N4 => 0x34,
ShortcutKey::N5 => 0x35,
ShortcutKey::N6 => 0x36,
ShortcutKey::N7 => 0x37,
ShortcutKey::N8 => 0x38,
ShortcutKey::N9 => 0x39,
ShortcutKey::Numpad0 => 0x60,
ShortcutKey::Numpad1 => 0x61,
ShortcutKey::Numpad2 => 0x62,
ShortcutKey::Numpad3 => 0x63,
ShortcutKey::Numpad4 => 0x64,
ShortcutKey::Numpad5 => 0x65,
ShortcutKey::Numpad6 => 0x66,
ShortcutKey::Numpad7 => 0x67,
ShortcutKey::Numpad8 => 0x68,
ShortcutKey::Numpad9 => 0x69,
ShortcutKey::Raw(code) => *code,
};
Some(vkey)
}
// Linux mappings
// NOTE: on linux, this method returns the KeySym and not the KeyCode
// which should be obtained in other ways depending on the backend.
// (X11 or Wayland)
#[cfg(target_os = "linux")]
pub fn to_code(&self) -> Option<u32> {
match self {
ShortcutKey::Alt => Some(0xFFE9),
ShortcutKey::Control => Some(0xFFE3),
ShortcutKey::Meta => Some(0xFFEB),
ShortcutKey::Shift => Some(0xFFE1),
ShortcutKey::Enter => Some(0xFF0D),
ShortcutKey::Tab => Some(0xFF09),
ShortcutKey::Space => Some(0x20),
ShortcutKey::ArrowDown => Some(0xFF54),
ShortcutKey::ArrowLeft => Some(0xFF51),
ShortcutKey::ArrowRight => Some(0xFF53),
ShortcutKey::ArrowUp => Some(0xFF52),
ShortcutKey::End => Some(0xFF57),
ShortcutKey::Home => Some(0xFF50),
ShortcutKey::PageDown => Some(0xFF56),
ShortcutKey::PageUp => Some(0xFF55),
ShortcutKey::Insert => Some(0xff63),
ShortcutKey::F1 => Some(0xFFBE),
ShortcutKey::F2 => Some(0xFFBF),
ShortcutKey::F3 => Some(0xFFC0),
ShortcutKey::F4 => Some(0xFFC1),
ShortcutKey::F5 => Some(0xFFC2),
ShortcutKey::F6 => Some(0xFFC3),
ShortcutKey::F7 => Some(0xFFC4),
ShortcutKey::F8 => Some(0xFFC5),
ShortcutKey::F9 => Some(0xFFC6),
ShortcutKey::F10 => Some(0xFFC7),
ShortcutKey::F11 => Some(0xFFC8),
ShortcutKey::F12 => Some(0xFFC9),
ShortcutKey::F13 => Some(0xFFCA),
ShortcutKey::F14 => Some(0xFFCB),
ShortcutKey::F15 => Some(0xFFCC),
ShortcutKey::F16 => Some(0xFFCD),
ShortcutKey::F17 => Some(0xFFCE),
ShortcutKey::F18 => Some(0xFFCF),
ShortcutKey::F19 => Some(0xFFD0),
ShortcutKey::F20 => Some(0xFFD1),
ShortcutKey::A => Some(0x0061),
ShortcutKey::B => Some(0x0062),
ShortcutKey::C => Some(0x0063),
ShortcutKey::D => Some(0x0064),
ShortcutKey::E => Some(0x0065),
ShortcutKey::F => Some(0x0066),
ShortcutKey::G => Some(0x0067),
ShortcutKey::H => Some(0x0068),
ShortcutKey::I => Some(0x0069),
ShortcutKey::J => Some(0x006a),
ShortcutKey::K => Some(0x006b),
ShortcutKey::L => Some(0x006c),
ShortcutKey::M => Some(0x006d),
ShortcutKey::N => Some(0x006e),
ShortcutKey::O => Some(0x006f),
ShortcutKey::P => Some(0x0070),
ShortcutKey::Q => Some(0x0071),
ShortcutKey::R => Some(0x0072),
ShortcutKey::S => Some(0x0073),
ShortcutKey::T => Some(0x0074),
ShortcutKey::U => Some(0x0075),
ShortcutKey::V => Some(0x0076),
ShortcutKey::W => Some(0x0077),
ShortcutKey::X => Some(0x0078),
ShortcutKey::Y => Some(0x0079),
ShortcutKey::Z => Some(0x007a),
ShortcutKey::N0 => Some(0x0030),
ShortcutKey::N1 => Some(0x0031),
ShortcutKey::N2 => Some(0x0032),
ShortcutKey::N3 => Some(0x0033),
ShortcutKey::N4 => Some(0x0034),
ShortcutKey::N5 => Some(0x0035),
ShortcutKey::N6 => Some(0x0036),
ShortcutKey::N7 => Some(0x0037),
ShortcutKey::N8 => Some(0x0038),
ShortcutKey::N9 => Some(0x0039),
ShortcutKey::Numpad0 => Some(0xffb0),
ShortcutKey::Numpad1 => Some(0xffb1),
ShortcutKey::Numpad2 => Some(0xffb2),
ShortcutKey::Numpad3 => Some(0xffb3),
ShortcutKey::Numpad4 => Some(0xffb4),
ShortcutKey::Numpad5 => Some(0xffb5),
ShortcutKey::Numpad6 => Some(0xffb6),
ShortcutKey::Numpad7 => Some(0xffb7),
ShortcutKey::Numpad8 => Some(0xffb8),
ShortcutKey::Numpad9 => Some(0xffb9),
ShortcutKey::Raw(code) => Some(*code as u32),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_works_correctly() {
assert!(matches!(
ShortcutKey::parse("ALT").unwrap(),
ShortcutKey::Alt
));
assert!(matches!(
ShortcutKey::parse("META").unwrap(),
ShortcutKey::Meta
));
assert!(matches!(
ShortcutKey::parse("CMD").unwrap(),
ShortcutKey::Meta
));
assert!(matches!(
ShortcutKey::parse("RAW(1234)").unwrap(),
ShortcutKey::Raw(1234)
));
}
#[test]
fn parse_invalid_keys() {
assert!(ShortcutKey::parse("INVALID").is_none());
assert!(ShortcutKey::parse("RAW(a)").is_none());
}
}

View File

@ -0,0 +1,150 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod keys;
use std::fmt::Display;
use anyhow::Result;
use keys::ShortcutKey;
use thiserror::Error;
static MODIFIERS: &[ShortcutKey; 4] = &[
ShortcutKey::Control,
ShortcutKey::Alt,
ShortcutKey::Shift,
ShortcutKey::Meta,
];
#[derive(Debug, PartialEq, Clone, Eq)]
pub struct HotKey {
pub id: i32,
pub key: ShortcutKey,
pub modifiers: Vec<ShortcutKey>,
}
impl HotKey {
pub fn new(id: i32, shortcut: &str) -> Result<Self> {
let tokens: Vec<String> = shortcut
.split('+')
.map(|token| token.trim().to_uppercase())
.collect();
let mut modifiers = Vec::new();
let mut main_key = None;
for token in tokens {
let key = ShortcutKey::parse(&token);
match key {
Some(key) => {
if MODIFIERS.contains(&key) {
modifiers.push(key)
} else {
main_key = Some(key)
}
}
None => return Err(HotKeyError::InvalidKey(token).into()),
};
}
if modifiers.is_empty() || main_key.is_none() {
return Err(HotKeyError::InvalidShortcut(shortcut.to_string()).into());
}
Ok(Self {
id,
modifiers,
key: main_key.unwrap(),
})
}
#[allow(dead_code)]
pub(crate) fn has_ctrl(&self) -> bool {
self.modifiers.contains(&ShortcutKey::Control)
}
#[allow(dead_code)]
pub(crate) fn has_meta(&self) -> bool {
self.modifiers.contains(&ShortcutKey::Meta)
}
#[allow(dead_code)]
pub(crate) fn has_alt(&self) -> bool {
self.modifiers.contains(&ShortcutKey::Alt)
}
#[allow(dead_code)]
pub(crate) fn has_shift(&self) -> bool {
self.modifiers.contains(&ShortcutKey::Shift)
}
}
impl Display for HotKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str_modifiers: Vec<String> = self.modifiers.iter().map(|m| m.to_string()).collect();
let modifiers = str_modifiers.join("+");
write!(f, "{}+{}", &modifiers, &self.key)
}
}
#[derive(Error, Debug)]
pub enum HotKeyError {
#[error("invalid hotkey shortcut, `{0}` is not a valid key")]
InvalidKey(String),
#[error("invalid hotkey shortcut `{0}`")]
InvalidShortcut(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_correctly() {
assert_eq!(
HotKey::new(1, "CTRL+V").unwrap(),
HotKey {
id: 1,
key: ShortcutKey::V,
modifiers: vec![ShortcutKey::Control],
}
);
assert_eq!(
HotKey::new(2, "SHIFT + Ctrl + v").unwrap(),
HotKey {
id: 2,
key: ShortcutKey::V,
modifiers: vec![ShortcutKey::Shift, ShortcutKey::Control],
}
);
assert!(HotKey::new(3, "invalid").is_err());
}
#[test]
fn modifiers_detected_correcty() {
assert!(HotKey::new(1, "CTRL+V").unwrap().has_ctrl());
assert!(HotKey::new(1, "ALT + V").unwrap().has_alt());
assert!(HotKey::new(1, "CMD + V").unwrap().has_meta());
assert!(HotKey::new(1, "SHIFT+ V").unwrap().has_shift());
assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_ctrl());
assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_alt());
assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_meta());
}
}

Some files were not shown because too many files have changed in this diff Show More