Compare commits

..

No commits in common. "dev" and "v0.5.5" have entirely different histories.
dev ... v0.5.5

664 changed files with 14812 additions and 95945 deletions

View File

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

1
.github/FUNDING.yml vendored
View File

@ -1,3 +1,4 @@
# 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']

View File

@ -1,28 +0,0 @@
---
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

@ -1,12 +0,0 @@
---
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

@ -1,18 +0,0 @@
#!/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

View File

@ -1,24 +0,0 @@
#!/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

View File

@ -1,159 +0,0 @@
# 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

View File

@ -1,321 +0,0 @@
# 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: "

2
.gitignore vendored
View File

@ -36,5 +36,3 @@ DerivedData
*.snap
venv/
.vscode/

3622
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,44 @@
[workspace]
[package]
name = "espanso"
version = "0.5.5"
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"
members = [
"espanso",
"espanso-detect",
"espanso-ui",
"espanso-inject",
"espanso-ipc",
"espanso-config",
"espanso-match",
"espanso-clipboard",
"espanso-render",
"espanso-info",
"espanso-path",
"espanso-modulo",
"espanso-migrate",
"espanso-mac-utils",
"espanso-kvs",
"espanso-engine",
"espanso-package",
]
[dependencies]
widestring = "0.4.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
dirs = "2.0.2"
clap = "2.33.0"
regex = "1.3.1"
log = "0.4.8"
simplelog = "0.7.1"
fs2 = "0.4.3"
serde_json = "1.0.40"
log-panics = {version = "2.0.0", features = ["with-backtrace"]}
backtrace = "0.3.37"
chrono = "0.4.9"
lazy_static = "1.4.0"
walkdir = "2.2.9"
reqwest = "0.9.20"
tempfile = "3.1.0"
dialoguer = "0.4.0"
rand = "0.7.2"
zip = "0.5.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2.62"
[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"]

View File

@ -1,67 +0,0 @@
# 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
the "copyright" line and a pointer to where the full notice is found.
Espanso
Copyright (C) 2019-2022 Federico Terzi
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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
@ -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
notice like this when it starts in an interactive mode:
Espanso Copyright (C) 2019-2022 Federico Terzi
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View File

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

View File

@ -1,8 +1,3 @@
> 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
Espanso has always been designed with a strong focus on security.

47
azure-pipelines.yml Normal file
View File

@ -0,0 +1,47 @@
# 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

56
build.rs Normal file
View File

@ -0,0 +1,56 @@
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();
}

11
ci/build-linux.yml Normal file
View File

@ -0,0 +1,11 @@
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"

19
ci/build-macos.yml Normal file
View File

@ -0,0 +1,19 @@
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"

18
ci/build-win.yml Normal file
View File

@ -0,0 +1,18 @@
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"

36
ci/deploy.yml Normal file
View File

@ -0,0 +1,36 @@
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'))

35
ci/install-rust.yml Normal file
View File

@ -0,0 +1,35 @@
# 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

22
ci/publish-homebrew.yml Normal file
View File

@ -0,0 +1,22 @@
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'))

21
ci/test.yml Normal file
View File

@ -0,0 +1,21 @@
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

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

16
ci/ubuntu/build_deb.sh Executable file
View File

@ -0,0 +1,16 @@
#!/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

@ -1,31 +0,0 @@
[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

@ -1,82 +0,0 @@
/*
* 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

@ -1,28 +0,0 @@
/*
* 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

@ -1,112 +0,0 @@
/*
* 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

@ -1,30 +0,0 @@
/*
* 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

@ -1,89 +0,0 @@
/*
* 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

@ -1,108 +0,0 @@
/*
* 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

@ -1,33 +0,0 @@
# 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

@ -1,218 +0,0 @@
/*
* 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,20 +0,0 @@
/*
* 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) mod fallback;

View File

@ -1,28 +0,0 @@
/*
* 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

@ -1,156 +0,0 @@
/*
* 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

@ -1,184 +0,0 @@
/*
* 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

@ -1,30 +0,0 @@
/*
* 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

@ -1,82 +0,0 @@
/*
* 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

@ -1,4 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,174 +0,0 @@
// 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

@ -1,178 +0,0 @@
// 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

@ -1,76 +0,0 @@
// 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

@ -1,33 +0,0 @@
// 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

@ -1,225 +0,0 @@
// 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

@ -1,83 +0,0 @@
// 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

@ -1,28 +0,0 @@
/*
* 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

@ -1,117 +0,0 @@
/*
* 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

@ -1,76 +0,0 @@
/*
* 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

@ -1,31 +0,0 @@
/*
* 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

@ -1,139 +0,0 @@
/*
* 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(())
}
}

View File

@ -1,25 +0,0 @@
[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

@ -1,25 +0,0 @@
/*
* 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

@ -1,345 +0,0 @@
/*
* 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

@ -1,91 +0,0 @@
/*
* 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

@ -1,365 +0,0 @@
/*
* 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

@ -1,182 +0,0 @@
/*
* 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

@ -1,984 +0,0 @@
/*
* 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

@ -1,196 +0,0 @@
/*
* 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

@ -1,80 +0,0 @@
/*
* 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

@ -1,34 +0,0 @@
/*
* 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

@ -1,71 +0,0 @@
/*
* 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

@ -1,714 +0,0 @@
/*
* 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

@ -1,62 +0,0 @@
/*
* 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
}
}

View File

@ -1,319 +0,0 @@
/*
* 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

@ -1,179 +0,0 @@
/*
* 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

@ -1,921 +0,0 @@
/*
* 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

@ -1,141 +0,0 @@
/*
* 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

@ -1,176 +0,0 @@
/*
* 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

@ -1,42 +0,0 @@
/*
* 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

@ -1,164 +0,0 @@
/*
* 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

@ -1,244 +0,0 @@
/*
* 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

@ -1,738 +0,0 @@
/*
* 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

@ -1,41 +0,0 @@
/*
* 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

@ -1,74 +0,0 @@
/*
* 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"));
}
}

View File

@ -1,33 +0,0 @@
[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"

View File

@ -1,83 +0,0 @@
/*
* 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

@ -1,3 +0,0 @@
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

@ -1,47 +0,0 @@
// 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

@ -1,334 +0,0 @@
// 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

@ -1,78 +0,0 @@
// 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

@ -1,149 +0,0 @@
/*
* 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

@ -1,112 +0,0 @@
// 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

@ -1,460 +0,0 @@
// 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

@ -1,90 +0,0 @@
// 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

@ -1,27 +0,0 @@
/*
* 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

@ -1,56 +0,0 @@
// 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

@ -1,39 +0,0 @@
/*
* 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

@ -1,248 +0,0 @@
// 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(())
}

View File

@ -1,147 +0,0 @@
/*
* 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

@ -1,628 +0,0 @@
/*
* 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

@ -1,150 +0,0 @@
/*
* 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());
}
}

View File

@ -1,56 +0,0 @@
/*
* 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::process::Command;
use log::error;
use regex::Regex;
use std::path::PathBuf;
lazy_static! {
static ref LAYOUT_EXTRACT_REGEX: Regex = Regex::new(r"^\[\('.*?', '(.*?)'\)").unwrap();
}
pub fn get_active_layout() -> Option<String> {
match Command::new("gsettings")
.arg("get")
.arg("org.gnome.desktop.input-sources")
.arg("mru-sources")
.output()
{
Ok(output) => {
let output_str = String::from_utf8_lossy(&output.stdout);
let captures = LAYOUT_EXTRACT_REGEX.captures(&output_str)?;
let layout = captures.get(1)?.as_str();
Some(layout.to_string())
}
Err(err) => {
error!(
"unable to retrieve current keyboard layout with 'gsettings': {}",
err
);
None
}
}
}
pub fn is_gnome() -> bool {
let target_session_file = PathBuf::from("/usr/bin/gnome-session");
target_session_file.exists()
}

View File

@ -1,136 +0,0 @@
/*
* 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 hotkey::HotKey;
use log::info;
pub mod event;
pub mod hotkey;
pub mod layout;
#[cfg(target_os = "windows")]
pub mod win32;
#[cfg(target_os = "linux")]
#[cfg(not(feature = "wayland"))]
pub mod x11;
#[cfg(target_os = "linux")]
pub mod evdev;
#[cfg(target_os = "macos")]
pub mod mac;
#[macro_use]
extern crate lazy_static;
pub type SourceCallback = Box<dyn Fn(event::InputEvent)>;
pub trait Source {
fn initialize(&mut self) -> Result<()>;
fn eventloop(&self, event_callback: SourceCallback) -> Result<()>;
}
#[allow(dead_code)]
pub struct SourceCreationOptions {
// Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
pub use_evdev: bool,
// Can be used to overwrite the keymap configuration
// used by espanso to inject key presses.
pub evdev_keyboard_rmlvo: Option<KeyboardConfig>,
// List of global hotkeys the detection module has to register
// NOTE: Hotkeys don't work under the EVDEV backend yet (Wayland)
pub hotkeys: Vec<HotKey>,
// 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.
pub win32_exclude_orphan_events: 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
pub win32_keyboard_layout_cache_interval: i64,
}
// This struct identifies the keyboard layout that
// should be used by EVDEV when loading the keymap.
// For more information: https://xkbcommon.org/doc/current/structxkb__rule__names.html
#[derive(Debug, Clone)]
pub struct KeyboardConfig {
pub rules: Option<String>,
pub model: Option<String>,
pub layout: Option<String>,
pub variant: Option<String>,
pub options: Option<String>,
}
impl Default for SourceCreationOptions {
fn default() -> Self {
Self {
use_evdev: false,
evdev_keyboard_rmlvo: None,
hotkeys: Vec::new(),
win32_exclude_orphan_events: true,
win32_keyboard_layout_cache_interval: 2000,
}
}
}
#[cfg(target_os = "windows")]
pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
info!("using Win32Source");
Ok(Box::new(win32::Win32Source::new(
&options.hotkeys,
options.win32_exclude_orphan_events,
options.win32_keyboard_layout_cache_interval,
)))
}
#[cfg(target_os = "macos")]
pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
info!("using CocoaSource");
Ok(Box::new(mac::CocoaSource::new(&options.hotkeys)))
}
#[cfg(target_os = "linux")]
#[cfg(not(feature = "wayland"))]
pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
if options.use_evdev {
info!("using EVDEVSource");
Ok(Box::new(evdev::EVDEVSource::new(options)))
} else {
info!("using X11Source");
Ok(Box::new(x11::X11Source::new(&options.hotkeys)))
}
}
#[cfg(target_os = "linux")]
#[cfg(feature = "wayland")]
pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
info!("using EVDEVSource");
Ok(Box::new(evdev::EVDEVSource::new(options)))
}
pub use layout::get_active_layout;

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