Fresh start
2231
Cargo.lock
generated
59
Cargo.toml
|
@ -1,55 +1,6 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "espanso"
|
|
||||||
version = "0.7.3"
|
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
|
||||||
license = "GPL-3.0"
|
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
|
||||||
readme = "README.md"
|
|
||||||
homepage = "https://github.com/federico-terzi/espanso"
|
|
||||||
edition = "2018"
|
|
||||||
build="build.rs"
|
|
||||||
|
|
||||||
[modulo]
|
members = [
|
||||||
version = "0.1.1"
|
"espanso",
|
||||||
|
"espanso-detect",
|
||||||
[dependencies]
|
]
|
||||||
widestring = "0.4.0"
|
|
||||||
serde = { version = "1.0.117", features = ["derive"] }
|
|
||||||
serde_yaml = "0.8"
|
|
||||||
dirs = "2.0.2"
|
|
||||||
clap = "2.33.0"
|
|
||||||
regex = "1.3.1"
|
|
||||||
log = "0.4.8"
|
|
||||||
simplelog = "0.7.1"
|
|
||||||
fs2 = "0.4.3"
|
|
||||||
serde_json = "1.0.60"
|
|
||||||
log-panics = {version = "2.0.0", features = ["with-backtrace"]}
|
|
||||||
backtrace = "0.3.37"
|
|
||||||
chrono = "0.4.9"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
walkdir = "2.2.9"
|
|
||||||
reqwest = "0.9.20"
|
|
||||||
tempfile = "3.1.0"
|
|
||||||
dialoguer = "0.4.0"
|
|
||||||
rand = "0.7.2"
|
|
||||||
zip = "0.5.3"
|
|
||||||
notify = "4.0.13"
|
|
||||||
markdown = "0.3.0"
|
|
||||||
html2text = "0.2.1"
|
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
|
||||||
libc = "0.2.62"
|
|
||||||
signal-hook = "0.1.15"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
|
||||||
named_pipe = "0.4.1"
|
|
||||||
winapi = { version = "0.3.9", features = ["wincon"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
cmake = "0.1.31"
|
|
||||||
|
|
||||||
[package.metadata.deb]
|
|
||||||
maintainer = "Federico Terzi <federicoterzi96@gmail.com>"
|
|
||||||
depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin"
|
|
||||||
section = "utility"
|
|
||||||
license-file = ["LICENSE", "1"]
|
|
59
build.rs
|
@ -1,59 +0,0 @@
|
||||||
extern crate cmake;
|
|
||||||
use cmake::Config;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/* OS SPECIFIC CONFIGS */
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_config() -> PathBuf {
|
|
||||||
Config::new("native/libwinbridge").build()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn get_config() -> PathBuf {
|
|
||||||
Config::new("native/liblinuxbridge").build()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn get_config() -> PathBuf {
|
|
||||||
Config::new("native/libmacbridge").build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
OS CUSTOM CARGO CONFIG LINES
|
|
||||||
Note: this is where linked libraries should be specified.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn print_config() {
|
|
||||||
println!("cargo:rustc-link-lib=static=winbridge");
|
|
||||||
println!("cargo:rustc-link-lib=dylib=user32");
|
|
||||||
#[cfg(target_env = "gnu")]
|
|
||||||
println!("cargo:rustc-link-lib=dylib=gdiplus");
|
|
||||||
#[cfg(target_env = "gnu")]
|
|
||||||
println!("cargo:rustc-link-lib=dylib=stdc++");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn print_config() {
|
|
||||||
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
|
|
||||||
println!("cargo:rustc-link-lib=static=linuxbridge");
|
|
||||||
println!("cargo:rustc-link-lib=dylib=X11");
|
|
||||||
println!("cargo:rustc-link-lib=dylib=Xtst");
|
|
||||||
println!("cargo:rustc-link-lib=dylib=xdo");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn print_config() {
|
|
||||||
println!("cargo:rustc-link-lib=dylib=c++");
|
|
||||||
println!("cargo:rustc-link-lib=static=macbridge");
|
|
||||||
println!("cargo:rustc-link-lib=framework=Cocoa");
|
|
||||||
println!("cargo:rustc-link-lib=framework=IOKit");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let dst = get_config();
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-search=native={}", dst.display());
|
|
||||||
print_config();
|
|
||||||
}
|
|
15
espanso-detect/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "espanso-detect"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
build="build.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4.14"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
widestring = "0.4.3"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1.0.66"
|
55
espanso-detect/build.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <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:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
|
||||||
|
println!("cargo:rustc-link-lib=static=linuxbridge");
|
||||||
|
println!("cargo:rustc-link-lib=dylib=X11");
|
||||||
|
println!("cargo:rustc-link-lib=dylib=Xtst");
|
||||||
|
println!("cargo:rustc-link-lib=dylib=xdo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn cc_config() {
|
||||||
|
println!("cargo:rustc-link-lib=dylib=c++");
|
||||||
|
println!("cargo:rustc-link-lib=static=macbridge");
|
||||||
|
println!("cargo:rustc-link-lib=framework=Cocoa");
|
||||||
|
println!("cargo:rustc-link-lib=framework=IOKit");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
cc_config();
|
||||||
|
}
|
117
espanso-detect/src/event.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InputEvent {
|
||||||
|
Mouse(MouseEvent),
|
||||||
|
Keyboard(KeyboardEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MouseButton {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Middle,
|
||||||
|
Button1,
|
||||||
|
Button2,
|
||||||
|
Button3,
|
||||||
|
Button4,
|
||||||
|
Button5,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MouseEvent {
|
||||||
|
pub button: MouseButton,
|
||||||
|
pub status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Status {
|
||||||
|
Pressed,
|
||||||
|
Released,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Variant {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct KeyboardEvent {
|
||||||
|
pub key: Key,
|
||||||
|
pub value: Option<String>,
|
||||||
|
pub status: Status,
|
||||||
|
pub variant: Option<Variant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A subset of the Web's key values: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Key {
|
||||||
|
// Modifiers
|
||||||
|
Alt,
|
||||||
|
CapsLock,
|
||||||
|
Control,
|
||||||
|
Meta,
|
||||||
|
NumLock,
|
||||||
|
Shift,
|
||||||
|
|
||||||
|
// Whitespace
|
||||||
|
Enter,
|
||||||
|
Tab,
|
||||||
|
Space,
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowUp,
|
||||||
|
End,
|
||||||
|
Home,
|
||||||
|
PageDown,
|
||||||
|
PageUp,
|
||||||
|
|
||||||
|
// Editing keys
|
||||||
|
Backspace,
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
F1,
|
||||||
|
F2,
|
||||||
|
F3,
|
||||||
|
F4,
|
||||||
|
F5,
|
||||||
|
F6,
|
||||||
|
F7,
|
||||||
|
F8,
|
||||||
|
F9,
|
||||||
|
F10,
|
||||||
|
F11,
|
||||||
|
F12,
|
||||||
|
F13,
|
||||||
|
F14,
|
||||||
|
F15,
|
||||||
|
F16,
|
||||||
|
F17,
|
||||||
|
F18,
|
||||||
|
F19,
|
||||||
|
F20,
|
||||||
|
|
||||||
|
// Other keys, includes the raw code provided by the operating system
|
||||||
|
Other(i32),
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* This file is part of espanso.
|
* This file is part of espanso.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019 Federico Terzi
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
*
|
*
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,11 +17,15 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pub mod event;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) mod windows;
|
pub mod win32;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(test)]
|
||||||
pub(crate) mod linux;
|
mod tests {
|
||||||
|
#[test]
|
||||||
#[cfg(target_os = "macos")]
|
fn it_works() {
|
||||||
pub(crate) mod macos;
|
assert_eq!(2 + 2, 5);
|
||||||
|
}
|
||||||
|
}
|
237
espanso-detect/src/win32/mod.rs
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use log::{trace, warn};
|
||||||
|
use widestring::U16CStr;
|
||||||
|
|
||||||
|
use crate::event::Status::*;
|
||||||
|
use crate::event::Variant::*;
|
||||||
|
use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
|
||||||
|
use crate::event::{Key::*, MouseButton, MouseEvent};
|
||||||
|
|
||||||
|
const LEFT_VARIANT: i32 = 1;
|
||||||
|
const RIGHT_VARIANT: i32 = 2;
|
||||||
|
|
||||||
|
const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1;
|
||||||
|
const INPUT_EVENT_TYPE_MOUSE: i32 = 2;
|
||||||
|
|
||||||
|
const INPUT_STATUS_PRESSED: i32 = 1;
|
||||||
|
const INPUT_STATUS_RELEASED: i32 = 2;
|
||||||
|
|
||||||
|
const INPUT_MOUSE_LEFT_BUTTON: i32 = 1;
|
||||||
|
const INPUT_MOUSE_RIGHT_BUTTON: i32 = 2;
|
||||||
|
const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3;
|
||||||
|
const INPUT_MOUSE_BUTTON_1: i32 = 4;
|
||||||
|
const INPUT_MOUSE_BUTTON_2: i32 = 5;
|
||||||
|
const INPUT_MOUSE_BUTTON_3: i32 = 6;
|
||||||
|
const INPUT_MOUSE_BUTTON_4: i32 = 7;
|
||||||
|
const INPUT_MOUSE_BUTTON_5: i32 = 8;
|
||||||
|
|
||||||
|
// Take a look at the native.h header file for an explanation of the fields
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct RawInputEvent {
|
||||||
|
pub event_type: i32,
|
||||||
|
|
||||||
|
pub buffer: [u16; 24],
|
||||||
|
pub buffer_len: i32,
|
||||||
|
|
||||||
|
pub key_code: i32,
|
||||||
|
pub variant: i32,
|
||||||
|
pub status: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(improper_ctypes)]
|
||||||
|
#[link(name = "native", kind = "static")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn raw_eventloop(
|
||||||
|
_self: *const Win32Source,
|
||||||
|
event_callback: extern "C" fn(_self: *mut Win32Source, event: RawInputEvent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Win32SourceCallback = Box<dyn Fn(InputEvent)>;
|
||||||
|
pub struct Win32Source {
|
||||||
|
callback: Win32SourceCallback,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Win32Source {
|
||||||
|
pub fn new(callback: Win32SourceCallback) -> Win32Source {
|
||||||
|
Self {
|
||||||
|
callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn eventloop(&self) {
|
||||||
|
unsafe {
|
||||||
|
extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) {
|
||||||
|
let event: Option<InputEvent> = event.into();
|
||||||
|
if let Some(event) = event {
|
||||||
|
unsafe { (*(*_self).callback)(event) }
|
||||||
|
} else {
|
||||||
|
trace!("Unable to convert raw event to input event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_eventloop(
|
||||||
|
self as *const Win32Source,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RawInputEvent> for Option<InputEvent> {
|
||||||
|
fn from(raw: RawInputEvent) -> Option<InputEvent> {
|
||||||
|
let status = match raw.status {
|
||||||
|
INPUT_STATUS_RELEASED => Released,
|
||||||
|
INPUT_STATUS_PRESSED => Pressed,
|
||||||
|
_ => Pressed,
|
||||||
|
};
|
||||||
|
|
||||||
|
match raw.event_type {
|
||||||
|
// Keyboard events
|
||||||
|
INPUT_EVENT_TYPE_KEYBOARD => {
|
||||||
|
let (key, variant_hint) = key_code_to_key(raw.key_code);
|
||||||
|
|
||||||
|
// If the raw event does not include an explicit variant, use the hint provided by the key code
|
||||||
|
let variant = match raw.variant {
|
||||||
|
LEFT_VARIANT => Some(Left),
|
||||||
|
RIGHT_VARIANT => Some(Right),
|
||||||
|
_ => variant_hint,
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if raw.buffer_len > 0 {
|
||||||
|
let raw_string_result = U16CStr::from_slice_with_nul(&raw.buffer);
|
||||||
|
match raw_string_result {
|
||||||
|
Ok(c_string) => {
|
||||||
|
let string_result = c_string.to_string();
|
||||||
|
match string_result {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Widechar conversion error: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Received malformed widechar: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
return Some(InputEvent::Keyboard(KeyboardEvent {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
status,
|
||||||
|
variant,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Mouse events
|
||||||
|
INPUT_EVENT_TYPE_MOUSE => {
|
||||||
|
let button = raw_to_mouse_button(raw.key_code);
|
||||||
|
|
||||||
|
if let Some(button) = button {
|
||||||
|
return Some(InputEvent::Mouse(MouseEvent { button, status }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||||
|
fn key_code_to_key(key_code: i32) -> (Key, Option<Variant>) {
|
||||||
|
match key_code {
|
||||||
|
// Modifiers
|
||||||
|
0x12 => (Alt, None),
|
||||||
|
0xA4 => (Alt, Some(Left)),
|
||||||
|
0xA5 => (Alt, Some(Right)),
|
||||||
|
0x14 => (CapsLock, None),
|
||||||
|
0x11 => (Control, None),
|
||||||
|
0xA2 => (Control, Some(Left)),
|
||||||
|
0xA3 => (Control, Some(Right)),
|
||||||
|
0x5B => (Meta, Some(Left)),
|
||||||
|
0x5C => (Meta, Some(Right)),
|
||||||
|
0x90 => (NumLock, None),
|
||||||
|
0x10 => (Shift, None),
|
||||||
|
0xA0 => (Shift, Some(Left)),
|
||||||
|
0xA1 => (Shift, Some(Right)),
|
||||||
|
|
||||||
|
// Whitespace
|
||||||
|
0x0D => (Enter, None),
|
||||||
|
0x09 => (Tab, None),
|
||||||
|
0x20 => (Space, None),
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
0x28 => (ArrowDown, None),
|
||||||
|
0x25 => (ArrowLeft, None),
|
||||||
|
0x27 => (ArrowRight, None),
|
||||||
|
0x26 => (ArrowUp, None),
|
||||||
|
0x23 => (End, None),
|
||||||
|
0x24 => (Home, None),
|
||||||
|
0x22 => (PageDown, None),
|
||||||
|
0x21 => (PageUp, None),
|
||||||
|
|
||||||
|
// Editing keys
|
||||||
|
0x08 => (Backspace, None),
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
0x70 => (F1, None),
|
||||||
|
0x71 => (F2, None),
|
||||||
|
0x72 => (F3, None),
|
||||||
|
0x73 => (F4, None),
|
||||||
|
0x74 => (F5, None),
|
||||||
|
0x75 => (F6, None),
|
||||||
|
0x76 => (F7, None),
|
||||||
|
0x77 => (F8, None),
|
||||||
|
0x78 => (F9, None),
|
||||||
|
0x79 => (F10, None),
|
||||||
|
0x7A => (F11, None),
|
||||||
|
0x7B => (F12, None),
|
||||||
|
0x7C => (F13, None),
|
||||||
|
0x7D => (F14, None),
|
||||||
|
0x7E => (F15, None),
|
||||||
|
0x7F => (F16, None),
|
||||||
|
0x80 => (F17, None),
|
||||||
|
0x81 => (F18, None),
|
||||||
|
0x82 => (F19, None),
|
||||||
|
0x83 => (F20, None),
|
||||||
|
|
||||||
|
// Other keys, includes the raw code provided by the operating system
|
||||||
|
_ => (Other(key_code), None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_to_mouse_button(raw: i32) -> Option<MouseButton> {
|
||||||
|
match raw {
|
||||||
|
INPUT_MOUSE_LEFT_BUTTON => Some(MouseButton::Left),
|
||||||
|
INPUT_MOUSE_RIGHT_BUTTON => Some(MouseButton::Right),
|
||||||
|
INPUT_MOUSE_MIDDLE_BUTTON => Some(MouseButton::Middle),
|
||||||
|
INPUT_MOUSE_BUTTON_1 => Some(MouseButton::Button1),
|
||||||
|
INPUT_MOUSE_BUTTON_2 => Some(MouseButton::Button2),
|
||||||
|
INPUT_MOUSE_BUTTON_3 => Some(MouseButton::Button3),
|
||||||
|
INPUT_MOUSE_BUTTON_4 => Some(MouseButton::Button4),
|
||||||
|
INPUT_MOUSE_BUTTON_5 => Some(MouseButton::Button5),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
318
espanso-detect/src/win32/native.cpp
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "native.h"
|
||||||
|
#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 <Windows.h>
|
||||||
|
|
||||||
|
// How many milliseconds must pass between events before refreshing the keyboard layout
|
||||||
|
const long refreshKeyboardLayoutInterval = 2000;
|
||||||
|
const USHORT mouseDownFlags = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN |
|
||||||
|
RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN |
|
||||||
|
RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_5_DOWN;
|
||||||
|
const USHORT mouseUpFlags = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP |
|
||||||
|
RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_UP |
|
||||||
|
RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_UP;
|
||||||
|
|
||||||
|
DWORD lastKeyboardPressTick = 0;
|
||||||
|
HKL currentKeyboardLayout;
|
||||||
|
HWND window;
|
||||||
|
const wchar_t *const winclass = L"Espanso";
|
||||||
|
|
||||||
|
void *self = NULL;
|
||||||
|
EventCallback event_callback = NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Message handler procedure for the windows
|
||||||
|
*/
|
||||||
|
LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
|
||||||
|
{
|
||||||
|
switch (msg)
|
||||||
|
{
|
||||||
|
case WM_INPUT: // Message relative to the RAW INPUT events
|
||||||
|
{
|
||||||
|
InputEvent event = {};
|
||||||
|
|
||||||
|
// Get the input size
|
||||||
|
UINT dwSize;
|
||||||
|
GetRawInputData(
|
||||||
|
(HRAWINPUT)lp,
|
||||||
|
RID_INPUT,
|
||||||
|
NULL,
|
||||||
|
&dwSize,
|
||||||
|
sizeof(RAWINPUTHEADER));
|
||||||
|
|
||||||
|
// Create a proper sized structure to hold the data
|
||||||
|
std::vector<BYTE> lpb(dwSize);
|
||||||
|
|
||||||
|
// Request the Raw input data
|
||||||
|
if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize,
|
||||||
|
sizeof(RAWINPUTHEADER)) != dwSize)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the input data
|
||||||
|
RAWINPUT *raw = reinterpret_cast<RAWINPUT *>(lpb.data());
|
||||||
|
|
||||||
|
if (raw->header.dwType == RIM_TYPEKEYBOARD) // Keyboard events
|
||||||
|
{
|
||||||
|
// We only want KEY UP AND KEY DOWN events
|
||||||
|
if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP &&
|
||||||
|
raw->data.keyboard.Message != WM_SYSKEYDOWN)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The alt key sends a SYSKEYDOWN instead of KEYDOWN event
|
||||||
|
int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN ||
|
||||||
|
raw->data.keyboard.Message == WM_SYSKEYDOWN;
|
||||||
|
|
||||||
|
DWORD currentTick = GetTickCount();
|
||||||
|
|
||||||
|
// If enough time has passed between the last keypress and now, refresh the keyboard layout
|
||||||
|
if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Because keyboard layouts on windows are Window-specific, to get the current
|
||||||
|
// layout we need to get the foreground window and get its layout.
|
||||||
|
|
||||||
|
HWND hwnd = GetForegroundWindow();
|
||||||
|
if (hwnd)
|
||||||
|
{
|
||||||
|
DWORD threadID = GetWindowThreadProcessId(hwnd, NULL);
|
||||||
|
HKL newKeyboardLayout = GetKeyboardLayout(threadID);
|
||||||
|
|
||||||
|
// It's not always valid, so update the current value only if available.
|
||||||
|
if (newKeyboardLayout != 0)
|
||||||
|
{
|
||||||
|
currentKeyboardLayout = newKeyboardLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastKeyboardPressTick = currentTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get keyboard state ( necessary to decode the associated Unicode char )
|
||||||
|
std::vector<BYTE> lpKeyState(256);
|
||||||
|
if (GetKeyboardState(lpKeyState.data()))
|
||||||
|
{
|
||||||
|
// This flag is needed to avoid chaning the keyboard state for some layouts.
|
||||||
|
// Refer to issue: https://github.com/federico-terzi/espanso/issues/86
|
||||||
|
UINT flags = 1 << 2;
|
||||||
|
|
||||||
|
int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast<LPWSTR>(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, currentKeyboardLayout);
|
||||||
|
|
||||||
|
// Handle the corresponding string if present
|
||||||
|
if (result >= 1)
|
||||||
|
{
|
||||||
|
event.buffer_len = result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If the given key does not have a correspondent string, reset the buffer
|
||||||
|
memset(event.buffer, 0, sizeof(event.buffer));
|
||||||
|
event.buffer_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.event_type = INPUT_EVENT_TYPE_KEYBOARD;
|
||||||
|
event.key_code = raw->data.keyboard.VKey;
|
||||||
|
event.status = is_key_down ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED;
|
||||||
|
|
||||||
|
// Load the key variants when appropriate
|
||||||
|
if (raw->data.keyboard.VKey == VK_SHIFT)
|
||||||
|
{
|
||||||
|
// To discriminate between the left and right shift, we need to employ a workaround.
|
||||||
|
// See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput
|
||||||
|
if (raw->data.keyboard.MakeCode == 42)
|
||||||
|
{ // Left shift
|
||||||
|
event.variant = INPUT_LEFT_VARIANT;
|
||||||
|
}
|
||||||
|
if (raw->data.keyboard.MakeCode == 54)
|
||||||
|
{ // Right shift
|
||||||
|
event.variant = INPUT_RIGHT_VARIANT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Also the ALT and CTRL key are special cases
|
||||||
|
// Check out the previous Stackoverflow question for more information
|
||||||
|
if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU)
|
||||||
|
{
|
||||||
|
if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0)
|
||||||
|
{
|
||||||
|
event.variant = INPUT_RIGHT_VARIANT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
event.variant = INPUT_LEFT_VARIANT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse events
|
||||||
|
{
|
||||||
|
// Make sure the mouse event belongs to the supported ones
|
||||||
|
if ((raw->data.mouse.usButtonFlags & (mouseDownFlags | mouseUpFlags)) == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.event_type = INPUT_EVENT_TYPE_MOUSE;
|
||||||
|
|
||||||
|
if ((raw->data.mouse.usButtonFlags & mouseDownFlags) != 0)
|
||||||
|
{
|
||||||
|
event.status = INPUT_STATUS_PRESSED;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & mouseUpFlags) != 0) {
|
||||||
|
event.status = INPUT_STATUS_RELEASED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the mouse flags into custom button mappings
|
||||||
|
if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_LEFT_BUTTON_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_LEFT_BUTTON;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_RIGHT_BUTTON;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_MIDDLE_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_MIDDLE_BUTTON;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_1_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_BUTTON_1;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_2_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_BUTTON_2;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_3_DOWN | RI_MOUSE_BUTTON_3_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_BUTTON_3;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_4_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_BUTTON_4;
|
||||||
|
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_5_DOWN | RI_MOUSE_BUTTON_5_UP)) != 0) {
|
||||||
|
event.key_code = INPUT_MOUSE_BUTTON_5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If valid, send the event to the Rust layer
|
||||||
|
if (event.event_type != 0 && self != NULL && event_callback != NULL)
|
||||||
|
{
|
||||||
|
event_callback(self, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return DefWindowProc(window, msg, wp, lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t raw_eventloop(void *_self, EventCallback _callback)
|
||||||
|
{
|
||||||
|
// Initialize the default keyboard layout
|
||||||
|
currentKeyboardLayout = GetKeyboardLayout(0);
|
||||||
|
|
||||||
|
// Initialize the Worker window
|
||||||
|
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
|
||||||
|
WNDCLASSEX wndclass = {
|
||||||
|
sizeof(WNDCLASSEX), // cbSize: Size of this structure
|
||||||
|
0, // style: Class styles
|
||||||
|
window_procedure, // lpfnWndProc: Pointer to the window procedure
|
||||||
|
0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure
|
||||||
|
0, // cbWndExtra: The number of extra bytes to allocate following the window instance.
|
||||||
|
GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class.
|
||||||
|
NULL, // hIcon: A handle to the class icon.
|
||||||
|
LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor.
|
||||||
|
NULL, // hbrBackground: A handle to the class background brush.
|
||||||
|
NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
|
||||||
|
winclass, // lpszClassName: A pointer to a null-terminated string or is an atom.
|
||||||
|
NULL // hIconSm: A handle to a small icon that is associated with the window class.
|
||||||
|
};
|
||||||
|
|
||||||
|
if (RegisterClassEx(&wndclass))
|
||||||
|
{
|
||||||
|
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
|
||||||
|
window = CreateWindowEx(
|
||||||
|
0, // dwExStyle: The extended window style of the window being created.
|
||||||
|
winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
|
||||||
|
L"Espanso Worker Window", // lpWindowName: The window name.
|
||||||
|
WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created.
|
||||||
|
CW_USEDEFAULT, // X: The initial horizontal position of the window.
|
||||||
|
CW_USEDEFAULT, // Y: The initial vertical position of the window.
|
||||||
|
100, // nWidth: The width, in device units, of the window.
|
||||||
|
100, // nHeight: The height, in device units, of the window.
|
||||||
|
NULL, // hWndParent: handle to the parent or owner window of the window being created.
|
||||||
|
NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
|
||||||
|
GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window.
|
||||||
|
NULL // lpParam: Pointer to a value to be passed to the window
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register raw inputs
|
||||||
|
RAWINPUTDEVICE Rid[2];
|
||||||
|
|
||||||
|
Rid[0].usUsagePage = 0x01;
|
||||||
|
Rid[0].usUsage = 0x06;
|
||||||
|
Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK;
|
||||||
|
Rid[0].hwndTarget = window;
|
||||||
|
|
||||||
|
Rid[1].usUsagePage = 0x01;
|
||||||
|
Rid[1].usUsage = 0x02;
|
||||||
|
Rid[1].dwFlags = RIDEV_INPUTSINK;
|
||||||
|
Rid[1].hwndTarget = window;
|
||||||
|
|
||||||
|
if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE)
|
||||||
|
{ // Something went wrong, error.
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Something went wrong, error.
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
event_callback = _callback;
|
||||||
|
self = _self;
|
||||||
|
|
||||||
|
if (window)
|
||||||
|
{
|
||||||
|
// Hide the window
|
||||||
|
ShowWindow(window, SW_HIDE);
|
||||||
|
|
||||||
|
// Enter the Event loop
|
||||||
|
MSG msg;
|
||||||
|
while (GetMessage(&msg, 0, 0, 0))
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
event_callback = NULL;
|
||||||
|
self = NULL;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
69
espanso-detect/src/win32/native.h
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ESPANSO_DETECT_H
|
||||||
|
#define ESPANSO_DETECT_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define INPUT_EVENT_TYPE_KEYBOARD 1
|
||||||
|
#define INPUT_EVENT_TYPE_MOUSE 2
|
||||||
|
|
||||||
|
#define INPUT_STATUS_PRESSED 1
|
||||||
|
#define INPUT_STATUS_RELEASED 2
|
||||||
|
|
||||||
|
#define INPUT_LEFT_VARIANT 1
|
||||||
|
#define INPUT_RIGHT_VARIANT 2
|
||||||
|
|
||||||
|
#define INPUT_MOUSE_LEFT_BUTTON 1
|
||||||
|
#define INPUT_MOUSE_RIGHT_BUTTON 2
|
||||||
|
#define INPUT_MOUSE_MIDDLE_BUTTON 3
|
||||||
|
#define INPUT_MOUSE_BUTTON_1 4
|
||||||
|
#define INPUT_MOUSE_BUTTON_2 5
|
||||||
|
#define INPUT_MOUSE_BUTTON_3 6
|
||||||
|
#define INPUT_MOUSE_BUTTON_4 7
|
||||||
|
#define INPUT_MOUSE_BUTTON_5 8
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// Keyboard or Mouse event
|
||||||
|
int32_t event_type;
|
||||||
|
|
||||||
|
// Contains the string corresponding to the key, if any
|
||||||
|
uint16_t buffer[24];
|
||||||
|
// Length of the extracted string. Equals 0 if no string is extracted
|
||||||
|
int32_t buffer_len;
|
||||||
|
|
||||||
|
// Virtual key code of the pressed key in case of keyboard events
|
||||||
|
// Mouse button code otherwise.
|
||||||
|
int32_t key_code;
|
||||||
|
|
||||||
|
// Left or Right variant
|
||||||
|
int32_t variant;
|
||||||
|
|
||||||
|
// Pressed or Released status
|
||||||
|
int32_t status;
|
||||||
|
} InputEvent;
|
||||||
|
|
||||||
|
typedef void (*EventCallback)(void * self, InputEvent data);
|
||||||
|
extern EventCallback event_callback;
|
||||||
|
|
||||||
|
// Initialize the Raw Input API and run the event loop. Blocking call.
|
||||||
|
extern "C" int32_t raw_eventloop(void * self, EventCallback callback);
|
||||||
|
|
||||||
|
#endif //ESPANSO_DETECT_H
|
12
espanso/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "espanso"
|
||||||
|
version = "1.0.0"
|
||||||
|
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"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
espanso-detect = { path = "../espanso-detect" }
|
8
espanso/src/main.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!z");
|
||||||
|
|
||||||
|
let source = espanso_detect::win32::Win32Source::new(Box::new(|event| {
|
||||||
|
println!("ev {:?}", event);
|
||||||
|
}));
|
||||||
|
source.eventloop();
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.0)
|
|
||||||
project(liblinuxbridge)
|
|
||||||
|
|
||||||
set (CMAKE_CXX_STANDARD 14)
|
|
||||||
set(CMAKE_REQUIRED_INCLUDES "/usr/local/include" "/usr/include")
|
|
||||||
|
|
||||||
add_library(linuxbridge STATIC bridge.cpp bridge.h fast_xdo.cpp fast_xdo.h)
|
|
||||||
|
|
||||||
install(TARGETS linuxbridge DESTINATION .)
|
|
|
@ -1,625 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "bridge.h"
|
|
||||||
#include "fast_xdo.h"
|
|
||||||
|
|
||||||
#include <locale.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <array>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <X11/Xlibint.h>
|
|
||||||
#include <X11/Xlib.h>
|
|
||||||
#include <X11/Xutil.h>
|
|
||||||
#include <X11/cursorfont.h>
|
|
||||||
#include <X11/keysymdef.h>
|
|
||||||
#include <X11/keysym.h>
|
|
||||||
#include <X11/extensions/record.h>
|
|
||||||
#include <X11/extensions/XTest.h>
|
|
||||||
#include <X11/XKBlib.h>
|
|
||||||
#include <X11/Xatom.h>
|
|
||||||
extern "C" { // Needed to avoid C++ compiler name mangling
|
|
||||||
#include <xdo.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This code uses the X11 Record Extension to receive keyboard
|
|
||||||
events. Documentation of this library can be found here:
|
|
||||||
https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html
|
|
||||||
|
|
||||||
We will refer to this extension as RE from now on.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
This struct is needed to receive events from the RE.
|
|
||||||
The funny thing is: it's not defined there, it should though.
|
|
||||||
The only place this is mentioned is the libxnee library,
|
|
||||||
so check that out if you need a reference.
|
|
||||||
*/
|
|
||||||
typedef union {
|
|
||||||
unsigned char type ;
|
|
||||||
xEvent event ;
|
|
||||||
xResourceReq req ;
|
|
||||||
xGenericReply reply ;
|
|
||||||
xError error ;
|
|
||||||
xConnSetupPrefix setup;
|
|
||||||
} XRecordDatum;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Connections to the X server, RE recommends 2 connections:
|
|
||||||
one for recording control and one for reading the recorded data.
|
|
||||||
*/
|
|
||||||
Display *data_disp = NULL;
|
|
||||||
Display *ctrl_disp = NULL;
|
|
||||||
|
|
||||||
XRecordRange *record_range;
|
|
||||||
XRecordContext context;
|
|
||||||
|
|
||||||
xdo_t * xdo_context;
|
|
||||||
|
|
||||||
// Callback invoked when a new key event occur.
|
|
||||||
void event_callback (XPointer, XRecordInterceptData*);
|
|
||||||
int error_callback(Display *display, XErrorEvent *error);
|
|
||||||
|
|
||||||
KeypressCallback keypress_callback;
|
|
||||||
X11ErrorCallback x11_error_callback;
|
|
||||||
void * context_instance;
|
|
||||||
|
|
||||||
void register_keypress_callback(KeypressCallback callback) {
|
|
||||||
keypress_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void register_error_callback(X11ErrorCallback callback) {
|
|
||||||
x11_error_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t check_x11() {
|
|
||||||
Display *check_disp = XOpenDisplay(NULL);
|
|
||||||
|
|
||||||
if (!check_disp) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
XCloseDisplay(check_disp);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t initialize(void * _context_instance) {
|
|
||||||
setlocale(LC_ALL, "");
|
|
||||||
|
|
||||||
context_instance = _context_instance;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Open the connections to the X server.
|
|
||||||
RE recommends to open 2 connections to the X server:
|
|
||||||
one for the recording control and one to read the protocol
|
|
||||||
data.
|
|
||||||
*/
|
|
||||||
ctrl_disp = XOpenDisplay(NULL);
|
|
||||||
data_disp = XOpenDisplay(NULL);
|
|
||||||
|
|
||||||
if (!ctrl_disp || !data_disp) { // Display error
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
We must set the ctrl_disp to sync mode, or, when we the enable
|
|
||||||
context in data_disp, there will be a fatal X error.
|
|
||||||
*/
|
|
||||||
XSynchronize(ctrl_disp, True);
|
|
||||||
|
|
||||||
int dummy;
|
|
||||||
|
|
||||||
// Make sure the X RE is installed in this system.
|
|
||||||
if (!XRecordQueryVersion(ctrl_disp, &dummy, &dummy)) {
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the X Keyboard Extension is installed
|
|
||||||
if (!XkbQueryExtension(ctrl_disp, &dummy, &dummy, &dummy, &dummy, &dummy)) {
|
|
||||||
return -3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the record range, that is the kind of events we want to track.
|
|
||||||
record_range = XRecordAllocRange ();
|
|
||||||
if (!record_range) {
|
|
||||||
return -4;
|
|
||||||
}
|
|
||||||
record_range->device_events.first = KeyPress;
|
|
||||||
record_range->device_events.last = ButtonPress;
|
|
||||||
|
|
||||||
// We want to get the keys from all clients
|
|
||||||
XRecordClientSpec client_spec;
|
|
||||||
client_spec = XRecordAllClients;
|
|
||||||
|
|
||||||
// Initialize the context
|
|
||||||
context = XRecordCreateContext(ctrl_disp, 0, &client_spec, 1, &record_range, 1);
|
|
||||||
if (!context) {
|
|
||||||
return -5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!XRecordEnableContextAsync(data_disp, context, event_callback, NULL)) {
|
|
||||||
return -6;
|
|
||||||
}
|
|
||||||
|
|
||||||
xdo_context = xdo_new(NULL);
|
|
||||||
|
|
||||||
// Setup a custom error handler
|
|
||||||
XSetErrorHandler(&error_callback);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: We might never get a MappingNotify event if the
|
|
||||||
* modifier and keymap information was never cached in Xlib.
|
|
||||||
* The next line makes sure that this happens initially.
|
|
||||||
*/
|
|
||||||
XKeysymToKeycode(ctrl_disp, XK_F1);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t eventloop() {
|
|
||||||
bool running = true;
|
|
||||||
|
|
||||||
int ctrl_fd = XConnectionNumber(ctrl_disp);
|
|
||||||
int data_fd = XConnectionNumber(data_disp);
|
|
||||||
|
|
||||||
while (running)
|
|
||||||
{
|
|
||||||
fd_set fds;
|
|
||||||
FD_ZERO(&fds);
|
|
||||||
FD_SET(ctrl_fd, &fds);
|
|
||||||
FD_SET(data_fd, &fds);
|
|
||||||
timeval timeout;
|
|
||||||
timeout.tv_sec = 2;
|
|
||||||
timeout.tv_usec = 0;
|
|
||||||
int retval = select(max(ctrl_fd, data_fd) + 1,
|
|
||||||
&fds, NULL, NULL, &timeout);
|
|
||||||
|
|
||||||
if (FD_ISSET(data_fd, &fds)) {
|
|
||||||
XRecordProcessReplies(data_disp);
|
|
||||||
}
|
|
||||||
if (FD_ISSET(ctrl_fd, &fds)) {
|
|
||||||
XEvent event;
|
|
||||||
XNextEvent(ctrl_disp, &event);
|
|
||||||
if (event.type == MappingNotify) {
|
|
||||||
XMappingEvent *e = (XMappingEvent *) &event;
|
|
||||||
if (e->request == MappingKeyboard) {
|
|
||||||
XRefreshKeyboardMapping(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void cleanup() {
|
|
||||||
XRecordDisableContext(ctrl_disp, context);
|
|
||||||
XRecordFreeContext(ctrl_disp, context);
|
|
||||||
XFree (record_range);
|
|
||||||
XCloseDisplay(data_disp);
|
|
||||||
XCloseDisplay(ctrl_disp);
|
|
||||||
xdo_free(xdo_context);
|
|
||||||
}
|
|
||||||
|
|
||||||
void event_callback(XPointer p, XRecordInterceptData *hook)
|
|
||||||
{
|
|
||||||
// Make sure the event comes from the X11 server
|
|
||||||
if (hook->category != XRecordFromServer) {
|
|
||||||
XRecordFreeData(hook);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast the event payload to a XRecordDatum, needed later to access the fields
|
|
||||||
// This struct was hard to find and understand. Turn's out that all the
|
|
||||||
// required data are included in the "event" field of this structure.
|
|
||||||
// The funny thing is that it's not a XEvent as one might expect,
|
|
||||||
// but a xEvent, a very different beast defined in the Xproto.h header.
|
|
||||||
// I suggest you to look at that header if you want to understand where the
|
|
||||||
// upcoming field where taken from.
|
|
||||||
XRecordDatum *data = (XRecordDatum*) hook->data;
|
|
||||||
|
|
||||||
int event_type = data->type;
|
|
||||||
int key_code = data->event.u.u.detail;
|
|
||||||
|
|
||||||
// In order to convert the key_code into the corresponding string,
|
|
||||||
// we need to synthesize an artificial XKeyEvent, to feed later to the
|
|
||||||
// XLookupString function.
|
|
||||||
XKeyEvent event;
|
|
||||||
event.display = ctrl_disp;
|
|
||||||
event.window = data->event.u.focus.window;
|
|
||||||
event.root = XDefaultRootWindow(ctrl_disp);
|
|
||||||
event.subwindow = None;
|
|
||||||
event.time = data->event.u.keyButtonPointer.time;
|
|
||||||
event.x = 1;
|
|
||||||
event.y = 1;
|
|
||||||
event.x_root = 1;
|
|
||||||
event.y_root = 1;
|
|
||||||
event.same_screen = True;
|
|
||||||
event.keycode = key_code;
|
|
||||||
event.state = data->event.u.keyButtonPointer.state;
|
|
||||||
event.type = KeyPress;
|
|
||||||
|
|
||||||
// Extract the corresponding chars.
|
|
||||||
std::array<char, 10> buffer;
|
|
||||||
int res = XLookupString(&event, buffer.data(), buffer.size(), NULL, NULL);
|
|
||||||
|
|
||||||
switch (event_type) {
|
|
||||||
case KeyPress:
|
|
||||||
//printf ("Press %d %d %s\n", key_code, res, buffer.data());
|
|
||||||
if (res > 0 && key_code != 22) { // Printable character, but not backspace
|
|
||||||
keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code);
|
|
||||||
}else{ // Modifier key
|
|
||||||
keypress_callback(context_instance, NULL, 0, 1, key_code);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ButtonPress: // Send also mouse button presses as "other events"
|
|
||||||
//printf ("Press button %d\n", key_code);
|
|
||||||
keypress_callback(context_instance, NULL, 0, 2, key_code);
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
XRecordFreeData(hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
int error_callback(Display *display, XErrorEvent *error) {
|
|
||||||
x11_error_callback(context_instance, error->error_code, error->request_code, error->minor_code);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void release_all_keys() {
|
|
||||||
char keys[32];
|
|
||||||
XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard
|
|
||||||
for (int i = 0; i<32; i++) {
|
|
||||||
// Only those that show a keypress should be changed
|
|
||||||
if (keys[i] != 0) {
|
|
||||||
for (int k = 0; k<8; k++) {
|
|
||||||
if ((keys[i] & (1 << k)) != 0) { // Bit by bit check
|
|
||||||
int key_code = i*8 + k;
|
|
||||||
XTestFakeKeyEvent(xdo_context->xdpy, key_code, false, CurrentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_string(const char * string) {
|
|
||||||
// It may happen that when an expansion is triggered, some keys are still pressed.
|
|
||||||
// This causes a problem if the expanded match contains that character, as the injection
|
|
||||||
// will not be able to register that keypress (as it is already pressed).
|
|
||||||
// To solve the problem, before an expansion we get which keys are currently pressed
|
|
||||||
// and inject a key_release event so that they can be further registered.
|
|
||||||
release_all_keys();
|
|
||||||
|
|
||||||
xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_enter() {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_release_all_keys() {
|
|
||||||
Window focused;
|
|
||||||
int revert_to;
|
|
||||||
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
|
|
||||||
|
|
||||||
char keys[32];
|
|
||||||
XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard
|
|
||||||
for (int i = 0; i<32; i++) {
|
|
||||||
// Only those that show a keypress should be changed
|
|
||||||
if (keys[i] != 0) {
|
|
||||||
for (int k = 0; k<8; k++) {
|
|
||||||
if ((keys[i] & (1 << k)) != 0) { // Bit by bit check
|
|
||||||
int key_code = i*8 + k;
|
|
||||||
fast_send_event(xdo_context, focused, key_code, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
XFlush(xdo_context->xdpy);
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_send_string(const char * string, int32_t delay) {
|
|
||||||
// It may happen that when an expansion is triggered, some keys are still pressed.
|
|
||||||
// This causes a problem if the expanded match contains that character, as the injection
|
|
||||||
// will not be able to register that keypress (as it is already pressed).
|
|
||||||
// To solve the problem, before an expansion we get which keys are currently pressed
|
|
||||||
// and inject a key_release event so that they can be further registered.
|
|
||||||
fast_release_all_keys();
|
|
||||||
|
|
||||||
Window focused;
|
|
||||||
int revert_to;
|
|
||||||
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
|
|
||||||
|
|
||||||
int actual_delay = 1;
|
|
||||||
if (delay > 0) {
|
|
||||||
actual_delay = delay * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
fast_enter_text_window(xdo_context, focused, string, actual_delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count, int32_t delay) {
|
|
||||||
int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode);
|
|
||||||
|
|
||||||
Window focused;
|
|
||||||
int revert_to;
|
|
||||||
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
|
|
||||||
|
|
||||||
for (int i = 0; i<count; i++) {
|
|
||||||
fast_send_event(xdo_context, focused, keycode, 1);
|
|
||||||
fast_send_event(xdo_context, focused, keycode, 0);
|
|
||||||
|
|
||||||
if (delay > 0) {
|
|
||||||
usleep(delay * 1000);
|
|
||||||
XFlush(xdo_context->xdpy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
XFlush(xdo_context->xdpy);
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_send_enter() {
|
|
||||||
_fast_send_keycode_to_focused_window(XK_Return, 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void delete_string(int32_t count) {
|
|
||||||
for (int i = 0; i<count; i++) {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_delete_string(int32_t count, int32_t delay) {
|
|
||||||
_fast_send_keycode_to_focused_window(XK_BackSpace, count, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
void left_arrow(int32_t count) {
|
|
||||||
for (int i = 0; i<count; i++) {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_left_arrow(int32_t count) {
|
|
||||||
_fast_send_keycode_to_focused_window(XK_Left, count, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_paste() {
|
|
||||||
// Before sending the paste shortcut, trigger the press and release of the Shift key
|
|
||||||
// this is needed because for some triggers, for example ending with ":", the user
|
|
||||||
// will still have the Shift key pressed when espanso execute the pasting shortcut,
|
|
||||||
// therefore sending CTRL+Shift+V instead of CTRL+V.
|
|
||||||
// With this call, we force the shift key to be unpressed when pasting.
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift", 8000);
|
|
||||||
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+v", 8000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_terminal_paste() {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Shift+v", 8000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_shift_ins_paste() {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Insert", 8000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_alt_shift_ins_paste() {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_ctrl_alt_paste() {
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Alt+v", 8000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_copy() {
|
|
||||||
// Release the other keys, for an explanation, read the 'trigger_paste' method
|
|
||||||
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Alt", 8000);
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift", 8000);
|
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+c", 8000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SYSTEM MODULE
|
|
||||||
|
|
||||||
// Function taken from the wmlib tool source code
|
|
||||||
char *get_property(Display *disp, Window win,
|
|
||||||
Atom xa_prop_type, char *prop_name, unsigned long *size)
|
|
||||||
{
|
|
||||||
unsigned long ret_nitems, ret_bytes_after, tmp_size;
|
|
||||||
Atom xa_prop_name, xa_ret_type;
|
|
||||||
unsigned char *ret_prop;
|
|
||||||
int ret_format;
|
|
||||||
char *ret;
|
|
||||||
int size_in_byte;
|
|
||||||
|
|
||||||
xa_prop_name = XInternAtom(disp, prop_name, False);
|
|
||||||
|
|
||||||
if (XGetWindowProperty(disp, win, xa_prop_name, 0, 4096 / 4, False,
|
|
||||||
xa_prop_type, &xa_ret_type, &ret_format, &ret_nitems,
|
|
||||||
&ret_bytes_after, &ret_prop) != Success)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
if (xa_ret_type != xa_prop_type)
|
|
||||||
{
|
|
||||||
XFree(ret_prop);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(ret_format) {
|
|
||||||
case 8: size_in_byte = sizeof(char); break;
|
|
||||||
case 16: size_in_byte = sizeof(short); break;
|
|
||||||
case 32: size_in_byte = sizeof(long); break;
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp_size = size_in_byte * ret_nitems;
|
|
||||||
ret = (char*) malloc(tmp_size + 1);
|
|
||||||
memcpy(ret, ret_prop, tmp_size);
|
|
||||||
ret[tmp_size] = '\0';
|
|
||||||
|
|
||||||
if (size) *size = tmp_size;
|
|
||||||
|
|
||||||
XFree(ret_prop);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function taken from Window Management Library for Ruby
|
|
||||||
char *xwm_get_win_title(Display *disp, Window win)
|
|
||||||
{
|
|
||||||
char *wname = (char*)get_property(disp,win, XA_STRING, "WM_NAME", NULL);
|
|
||||||
char *nwname = (char*)get_property(disp,win, XInternAtom(disp,
|
|
||||||
"UTF8_STRING", False), "_NET_WM_NAME", NULL);
|
|
||||||
|
|
||||||
return nwname ? nwname : (wname ? wname : NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_active_window_name(char * buffer, int32_t size) {
|
|
||||||
xdo_t * x = xdo_new(NULL);
|
|
||||||
|
|
||||||
if (!x) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the active window
|
|
||||||
Window win;
|
|
||||||
int ret = xdo_get_active_window(x, &win);
|
|
||||||
int result = 1;
|
|
||||||
if (ret) {
|
|
||||||
fprintf(stderr, "xdo_get_active_window reported an error\n");
|
|
||||||
result = -2;
|
|
||||||
}else{
|
|
||||||
char * title = xwm_get_win_title(x->xdpy, win);
|
|
||||||
|
|
||||||
snprintf(buffer, size, "%s", title);
|
|
||||||
|
|
||||||
XFree(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
xdo_free(x);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_active_window_class(char * buffer, int32_t size) {
|
|
||||||
xdo_t * x = xdo_new(NULL);
|
|
||||||
|
|
||||||
if (!x) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the active window
|
|
||||||
Window win;
|
|
||||||
int ret = xdo_get_active_window(x, &win);
|
|
||||||
int result = 1;
|
|
||||||
if (ret) {
|
|
||||||
fprintf(stderr, "xdo_get_active_window reported an error\n");
|
|
||||||
result = -2;
|
|
||||||
}else{
|
|
||||||
XClassHint hint;
|
|
||||||
|
|
||||||
if (XGetClassHint(x->xdpy, win, &hint)) {
|
|
||||||
snprintf(buffer, size, "%s", hint.res_class);
|
|
||||||
XFree(hint.res_name);
|
|
||||||
XFree(hint.res_class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xdo_free(x);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_active_window_executable(char *buffer, int32_t size) {
|
|
||||||
xdo_t * x = xdo_new(NULL);
|
|
||||||
|
|
||||||
if (!x) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the active window
|
|
||||||
Window win;
|
|
||||||
int ret = xdo_get_active_window(x, &win);
|
|
||||||
int result = 1;
|
|
||||||
if (ret) {
|
|
||||||
fprintf(stderr, "xdo_get_active_window reported an error\n");
|
|
||||||
result = -2;
|
|
||||||
}else{
|
|
||||||
// Get the window process PID
|
|
||||||
char *pid_raw = (char*)get_property(x->xdpy,win, XA_CARDINAL, "_NET_WM_PID", NULL);
|
|
||||||
if (pid_raw == NULL) {
|
|
||||||
result = -3;
|
|
||||||
}else{
|
|
||||||
int pid = pid_raw[0] | pid_raw[1] << 8 | pid_raw[2] << 16 | pid_raw[3] << 24;
|
|
||||||
|
|
||||||
// Get the executable path from it
|
|
||||||
char proc_path[250];
|
|
||||||
snprintf(proc_path, 250, "/proc/%d/exe", pid);
|
|
||||||
|
|
||||||
readlink(proc_path, buffer, size);
|
|
||||||
|
|
||||||
XFree(pid_raw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xdo_free(x);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t is_current_window_special() {
|
|
||||||
char class_buffer[250];
|
|
||||||
int res = get_active_window_class(class_buffer, 250);
|
|
||||||
if (res > 0) {
|
|
||||||
if (strstr(class_buffer, "terminal") != NULL) {
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "URxvt") != NULL) { // urxvt terminal
|
|
||||||
return 4;
|
|
||||||
}else if (strstr(class_buffer, "XTerm") != NULL) { // XTerm and UXTerm
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "Termite") != NULL) { // Termite
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "konsole") != NULL) { // KDE Konsole
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "Terminator") != NULL) { // Terminator
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "stterm") != NULL) { // Simple terminal 3
|
|
||||||
return 2;
|
|
||||||
}else if (strstr(class_buffer, "St") != NULL) { // Simple terminal
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "st") != NULL) { // Simple terminal 2
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "Alacritty") != NULL) { // Alacritty terminal
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "Emacs") != NULL) { // Emacs
|
|
||||||
return 3;
|
|
||||||
}else if (strstr(class_buffer, "yakuake") != NULL) { // Yakuake terminal
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "Tilix") != NULL) { // Tilix terminal
|
|
||||||
return 1;
|
|
||||||
}else if (strstr(class_buffer, "kitty") != NULL) { // kitty terminal
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef ESPANSO_BRIDGE_H
|
|
||||||
#define ESPANSO_BRIDGE_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
extern void * context_instance;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check if the X11 context is available
|
|
||||||
*/
|
|
||||||
extern "C" int32_t check_x11();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Initialize the X11 context and parameters
|
|
||||||
*/
|
|
||||||
extern "C" int32_t initialize(void * context_instance);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Start the event loop indefinitely. Blocking call.
|
|
||||||
*/
|
|
||||||
extern "C" int32_t eventloop();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Clean all the X11 resources allocated during the initialization.
|
|
||||||
*/
|
|
||||||
extern "C" void cleanup();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when a new keypress is made, the first argument is an char array,
|
|
||||||
* while the second is the size of the array.
|
|
||||||
*/
|
|
||||||
typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code);
|
|
||||||
extern KeypressCallback keypress_callback;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Register the callback that will be called when a keypress was made
|
|
||||||
*/
|
|
||||||
extern "C" void register_keypress_callback(KeypressCallback callback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when a X11 error occurs
|
|
||||||
*/
|
|
||||||
typedef void (*X11ErrorCallback)(void * self, char error_code, char request_code, char minor_code);
|
|
||||||
extern X11ErrorCallback x11_error_callback;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Register the callback that will be called when an X11 error occurs
|
|
||||||
*/
|
|
||||||
extern "C" void register_error_callback(X11ErrorCallback callback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Type the given string by simulating Key Presses
|
|
||||||
*/
|
|
||||||
extern "C" void send_string(const char * string);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Type the given string by simulating Key Presses using a faster inject method
|
|
||||||
*/
|
|
||||||
extern "C" void fast_send_string(const char * string, int32_t delay);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the backspace keypress, *count* times.
|
|
||||||
*/
|
|
||||||
extern "C" void delete_string(int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the backspace keypress, *count* times using a faster inject method
|
|
||||||
*/
|
|
||||||
extern "C" void fast_delete_string(int32_t count, int32_t delay);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send an Enter key press
|
|
||||||
*/
|
|
||||||
extern "C" void send_enter();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send an Enter key press using a faster inject method
|
|
||||||
*/
|
|
||||||
extern "C" void fast_send_enter();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the left arrow keypress, *count* times.
|
|
||||||
*/
|
|
||||||
extern "C" void left_arrow(int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the left arrow keypress, *count* times using a faster inject method
|
|
||||||
*/
|
|
||||||
extern "C" void fast_left_arrow(int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger normal paste ( Pressing CTRL+V )
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger terminal paste ( Pressing CTRL+SHIFT+V )
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_terminal_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger shift ins pasting( Pressing SHIFT+INS )
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_shift_ins_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger alt shift ins pasting( Pressing ALT+SHIFT+INS )
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_alt_shift_ins_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger CTRL+ALT+V pasting
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_ctrl_alt_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger copy shortcut ( Pressing CTRL+C )
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_copy();
|
|
||||||
|
|
||||||
// SYSTEM MODULE
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active windows's WM_NAME
|
|
||||||
*/
|
|
||||||
extern "C" int32_t get_active_window_name(char * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active windows's WM_CLASS
|
|
||||||
*/
|
|
||||||
extern "C" int32_t get_active_window_class(char * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active windows's executable path
|
|
||||||
*/
|
|
||||||
extern "C" int32_t get_active_window_executable(char * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return a value greater than 0 if the current window needs a special paste combination, 0 otherwise.
|
|
||||||
*/
|
|
||||||
extern "C" int32_t is_current_window_special();
|
|
||||||
|
|
||||||
#endif //ESPANSO_BRIDGE_H
|
|
|
@ -1,245 +0,0 @@
|
||||||
//
|
|
||||||
// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT
|
|
||||||
// and modified to use XSendEvent instead of XTestFakeKeyEvent.
|
|
||||||
|
|
||||||
#include <locale.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include "fast_xdo.h"
|
|
||||||
|
|
||||||
extern "C" { // Needed to avoid C++ compiler name mangling
|
|
||||||
#include <xdo.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) {
|
|
||||||
xk->display = xdo->xdpy;
|
|
||||||
xk->subwindow = None;
|
|
||||||
xk->time = CurrentTime;
|
|
||||||
xk->same_screen = True;
|
|
||||||
|
|
||||||
/* Should we set these at all? */
|
|
||||||
xk->x = xk->y = xk->x_root = xk->y_root = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
|
|
||||||
int modstate, int is_press, useconds_t delay) {
|
|
||||||
/* Properly ensure the modstate is set by finding a key
|
|
||||||
* that activates each bit in the modifier state */
|
|
||||||
int mask = modstate | key->modmask;
|
|
||||||
|
|
||||||
/* Since key events have 'state' (shift, etc) in the event, we don't
|
|
||||||
* need to worry about key press ordering. */
|
|
||||||
XKeyEvent xk;
|
|
||||||
fast_init_xkeyevent(xdo, &xk);
|
|
||||||
xk.window = window;
|
|
||||||
xk.keycode = key->code;
|
|
||||||
xk.state = mask | (key->group << 13);
|
|
||||||
xk.type = (is_press ? KeyPress : KeyRelease);
|
|
||||||
XSendEvent(xdo->xdpy, xk.window, True, 0, (XEvent *)&xk);
|
|
||||||
|
|
||||||
/* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */
|
|
||||||
XFlush(xdo->xdpy);
|
|
||||||
if (delay > 0) {
|
|
||||||
usleep(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int fast_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys,
|
|
||||||
int nkeys, int pressed, int *modifier, useconds_t delay) {
|
|
||||||
int i = 0;
|
|
||||||
int modstate = 0;
|
|
||||||
int keymapchanged = 0;
|
|
||||||
|
|
||||||
/* Find an unused keycode in case we need to bind unmapped keysyms */
|
|
||||||
KeySym *keysyms = NULL;
|
|
||||||
int keysyms_per_keycode = 0;
|
|
||||||
int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */
|
|
||||||
keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low,
|
|
||||||
xdo->keycode_high - xdo->keycode_low,
|
|
||||||
&keysyms_per_keycode);
|
|
||||||
|
|
||||||
/* Find a keycode that is unused for scratchspace */
|
|
||||||
for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) {
|
|
||||||
int j = 0;
|
|
||||||
int key_is_empty = 1;
|
|
||||||
for (j = 0; j < keysyms_per_keycode; j++) {
|
|
||||||
/*char *symname;*/
|
|
||||||
int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j;
|
|
||||||
/*symname = XKeysymToString(keysyms[symindex]);*/
|
|
||||||
if (keysyms[symindex] != 0) {
|
|
||||||
key_is_empty = 0;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (key_is_empty) {
|
|
||||||
scratch_keycode = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
XFree(keysyms);
|
|
||||||
|
|
||||||
/* Allow passing NULL for modifier in case we don't care about knowing
|
|
||||||
* the modifier map state after we finish */
|
|
||||||
if (modifier == NULL)
|
|
||||||
modifier = &modstate;
|
|
||||||
|
|
||||||
for (i = 0; i < nkeys; i++) {
|
|
||||||
if (keys[i].needs_binding == 1) {
|
|
||||||
KeySym keysym_list[] = { keys[i].symbol };
|
|
||||||
//_xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode);
|
|
||||||
XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
|
|
||||||
XSync(xdo->xdpy, False);
|
|
||||||
/* override the code in our current key to use the scratch_keycode */
|
|
||||||
keys[i].code = scratch_keycode;
|
|
||||||
keymapchanged = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n",
|
|
||||||
//keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier);
|
|
||||||
fast_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay);
|
|
||||||
|
|
||||||
if (keys[i].needs_binding == 1) {
|
|
||||||
/* If we needed to make a new keymapping for this keystroke, we
|
|
||||||
* should sync with the server now, after the keypress, so that
|
|
||||||
* the next mapping or removal doesn't conflict. */
|
|
||||||
XSync(xdo->xdpy, False);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pressed) {
|
|
||||||
*modifier |= keys[i].modmask;
|
|
||||||
} else {
|
|
||||||
*modifier &= ~(keys[i].modmask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (keymapchanged) {
|
|
||||||
KeySym keysym_list[] = { 0 };
|
|
||||||
//printf(xdo, "Reverting scratch keycode (sym %lu to %d)",
|
|
||||||
// keys[i].symbol, scratch_keycode);
|
|
||||||
XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Necessary? */
|
|
||||||
XFlush(xdo->xdpy);
|
|
||||||
return XDO_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key) {
|
|
||||||
int i = 0;
|
|
||||||
int len = xdo->charcodes_len;
|
|
||||||
|
|
||||||
//printf("Finding symbol for key '%c'\n", key);
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
//printf(" => %c vs %c (%d)\n",
|
|
||||||
//key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key));
|
|
||||||
if (xdo->charcodes[i].key == key) {
|
|
||||||
//printf(" => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol);
|
|
||||||
return xdo->charcodes[i].symbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key >= 0x100) key += 0x01000000;
|
|
||||||
if (XKeysymToString(key)) return key;
|
|
||||||
return NoSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) {
|
|
||||||
int i = 0;
|
|
||||||
int len = xdo->charcodes_len;
|
|
||||||
|
|
||||||
key->code = 0;
|
|
||||||
key->symbol = keysym;
|
|
||||||
key->group = 0;
|
|
||||||
key->modmask = 0;
|
|
||||||
key->needs_binding = 1;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
if (xdo->charcodes[i].symbol == keysym) {
|
|
||||||
key->code = xdo->charcodes[i].code;
|
|
||||||
key->group = xdo->charcodes[i].group;
|
|
||||||
key->modmask = xdo->charcodes[i].modmask;
|
|
||||||
key->needs_binding = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) {
|
|
||||||
KeySym keysym = fast_keysym_from_char(xdo, key->key);
|
|
||||||
fast_charcodemap_from_keysym(xdo, key, keysym);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* XXX: Return proper code if errors found */
|
|
||||||
int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) {
|
|
||||||
|
|
||||||
/* Since we're doing down/up, the delay should be based on the number
|
|
||||||
* of keys pressed (including shift). Since up/down is two calls,
|
|
||||||
* divide by two. */
|
|
||||||
delay /= 2;
|
|
||||||
|
|
||||||
/* XXX: Add error handling */
|
|
||||||
//int nkeys = strlen(string);
|
|
||||||
//charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t));
|
|
||||||
charcodemap_t key;
|
|
||||||
//int modifier = 0;
|
|
||||||
setlocale(LC_CTYPE,"");
|
|
||||||
mbstate_t ps = { 0 };
|
|
||||||
ssize_t len;
|
|
||||||
while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) {
|
|
||||||
if (len == -1) {
|
|
||||||
fprintf(stderr, "Invalid multi-byte sequence encountered\n");
|
|
||||||
return XDO_ERROR;
|
|
||||||
}
|
|
||||||
fast_charcodemap_from_char(xdo, &key);
|
|
||||||
if (key.code == 0 && key.symbol == NoSymbol) {
|
|
||||||
fprintf(stderr, "I don't what key produces '%lc', skipping.\n",
|
|
||||||
key.key);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
//printf("Found key for %c\n", key.key);
|
|
||||||
//printf("code: %d\n", key.code);
|
|
||||||
//printf("sym: %s\n", XKeysymToString(key.symbol));
|
|
||||||
}
|
|
||||||
|
|
||||||
//printf(stderr,
|
|
||||||
//"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n",
|
|
||||||
//key.key, key.code, key.symbol, key.group, key.modmask,
|
|
||||||
//(key.needs_binding == 1) ? "needs binding" : "ok");
|
|
||||||
|
|
||||||
//_xdo_send_key(xdo, window, keycode, modstate, True, delay);
|
|
||||||
//_xdo_send_key(xdo, window, keycode, modstate, False, delay);
|
|
||||||
fast_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2);
|
|
||||||
key.needs_binding = 0;
|
|
||||||
fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2);
|
|
||||||
|
|
||||||
XFlush(xdo->xdpy);
|
|
||||||
} /* walk string generating a keysequence */
|
|
||||||
|
|
||||||
//free(keys);
|
|
||||||
return XDO_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed) {
|
|
||||||
XKeyEvent xk;
|
|
||||||
xk.display = xdo->xdpy;
|
|
||||||
xk.window = window;
|
|
||||||
xk.root = XDefaultRootWindow(xdo->xdpy);
|
|
||||||
xk.subwindow = None;
|
|
||||||
xk.time = CurrentTime;
|
|
||||||
xk.x = 1;
|
|
||||||
xk.y = 1;
|
|
||||||
xk.x_root = 1;
|
|
||||||
xk.y_root = 1;
|
|
||||||
xk.same_screen = True;
|
|
||||||
xk.keycode = keycode;
|
|
||||||
xk.state = 0;
|
|
||||||
xk.type = (pressed ? KeyPress : KeyRelease);
|
|
||||||
|
|
||||||
XEvent event;
|
|
||||||
event.xkey =xk;
|
|
||||||
|
|
||||||
XSendEvent(xdo->xdpy, window, True, 0, &event);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
//
|
|
||||||
// Most of this code has been taken from the wonderful XDOTOOL: https://github.com/jordansissel/xdotool/blob/master/COPYRIGHT
|
|
||||||
// and modified to use XSendEvent instead of XTestFakeKeyEvent.
|
|
||||||
|
|
||||||
#ifndef LIBLINUXBRIDGE_FAST_XDO_H
|
|
||||||
#define LIBLINUXBRIDGE_FAST_XDO_H
|
|
||||||
|
|
||||||
extern "C" { // Needed to avoid C++ compiler name mangling
|
|
||||||
#include <xdo.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key);
|
|
||||||
void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key);
|
|
||||||
void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym);
|
|
||||||
void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk);
|
|
||||||
void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
|
|
||||||
int modstate, int is_press, useconds_t delay);
|
|
||||||
int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay);
|
|
||||||
void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed);
|
|
||||||
|
|
||||||
#endif //LIBLINUXBRIDGE_FAST_XDO_H
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <AppKit/AppKit.h>
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
#include "bridge.h"
|
|
||||||
|
|
||||||
@interface AppDelegate : NSObject <NSApplicationDelegate> {
|
|
||||||
@public NSStatusItem *myStatusItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
|
||||||
- (IBAction) statusIconClick: (id) sender;
|
|
||||||
- (IBAction) contextMenuClick: (id) sender;
|
|
||||||
- (void) updateIcon: (char *)iconPath;
|
|
||||||
- (void) setIcon: (char *)iconPath;
|
|
||||||
|
|
||||||
@end
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "AppDelegate.h"
|
|
||||||
|
|
||||||
@implementation AppDelegate
|
|
||||||
|
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
|
||||||
{
|
|
||||||
// Setup status icon
|
|
||||||
if (show_icon) {
|
|
||||||
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
|
||||||
[self setIcon: icon_path];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup key listener
|
|
||||||
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown)
|
|
||||||
handler:^(NSEvent *event){
|
|
||||||
|
|
||||||
if (event.type == NSEventTypeKeyDown
|
|
||||||
&& event.keyCode != 0x33) { // Send backspace as a modifier
|
|
||||||
|
|
||||||
const char *chars = [event.characters UTF8String];
|
|
||||||
int len = event.characters.length;
|
|
||||||
|
|
||||||
keypress_callback(context_instance, chars, len, 0, event.keyCode);
|
|
||||||
//NSLog(@"keydown: %@, %d", event.characters, event.keyCode);
|
|
||||||
}else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown) {
|
|
||||||
// Send the mouse button clicks as "other" events, used to improve word matches reliability
|
|
||||||
keypress_callback(context_instance, NULL, 0, 2, event.buttonNumber);
|
|
||||||
}else{
|
|
||||||
// Because this event is triggered for both the press and release of a modifier, trigger the callback
|
|
||||||
// only on release
|
|
||||||
if (([event modifierFlags] & (NSEventModifierFlagShift | NSEventModifierFlagCommand |
|
|
||||||
NSEventModifierFlagControl | NSEventModifierFlagOption)) == 0) {
|
|
||||||
|
|
||||||
keypress_callback(context_instance, NULL, 0, 1, event.keyCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
//NSLog(@"keydown: %d", event.keyCode);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateIcon: (char *)iconPath {
|
|
||||||
if (show_icon) {
|
|
||||||
[self setIcon: iconPath];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) setIcon: (char *)iconPath {
|
|
||||||
if (show_icon) {
|
|
||||||
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
|
|
||||||
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
|
|
||||||
[statusImage setTemplate:YES];
|
|
||||||
|
|
||||||
[myStatusItem.button setImage:statusImage];
|
|
||||||
[myStatusItem setHighlightMode:YES];
|
|
||||||
[myStatusItem.button setAction:@selector(statusIconClick:)];
|
|
||||||
[myStatusItem.button setTarget:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction) statusIconClick: (id) sender {
|
|
||||||
icon_click_callback(context_instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction) contextMenuClick: (id) sender {
|
|
||||||
NSInteger item_id = [[sender valueForKey:@"tag"] integerValue];
|
|
||||||
|
|
||||||
context_menu_click_callback(context_instance, static_cast<int32_t>(item_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
|
@ -1,9 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.0)
|
|
||||||
project(libmacbridge)
|
|
||||||
|
|
||||||
set (CMAKE_CXX_STANDARD 11)
|
|
||||||
set(CMAKE_C_FLAGS "-x objective-c")
|
|
||||||
|
|
||||||
add_library(macbridge STATIC bridge.mm bridge.h AppDelegate.h AppDelegate.mm)
|
|
||||||
|
|
||||||
install(TARGETS macbridge DESTINATION .)
|
|
|
@ -1,189 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef ESPANSO_BRIDGE_H
|
|
||||||
#define ESPANSO_BRIDGE_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
extern void * context_instance;
|
|
||||||
extern char * icon_path;
|
|
||||||
extern char * disabled_icon_path;
|
|
||||||
extern int32_t show_icon;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Initialize the AppDelegate and check for accessibility permissions
|
|
||||||
*/
|
|
||||||
int32_t initialize(void * context, const char * icon_path, const char * disabled_icon_path, int32_t show_icon);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Start the event loop indefinitely. Blocking call.
|
|
||||||
*/
|
|
||||||
int32_t eventloop();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Initialize the application and start the headless eventloop, used for the espanso detect command
|
|
||||||
*/
|
|
||||||
int32_t headless_eventloop();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when a new keypress is made, the first argument is an char array,
|
|
||||||
* while the second is the size of the array.
|
|
||||||
*/
|
|
||||||
typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code);
|
|
||||||
|
|
||||||
extern KeypressCallback keypress_callback;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Register the callback that will be called when a keypress was made
|
|
||||||
*/
|
|
||||||
void register_keypress_callback(KeypressCallback callback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Type the given string by using the CGEventKeyboardSetUnicodeString call
|
|
||||||
*/
|
|
||||||
void send_string(const char * string);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the Virtual Key press
|
|
||||||
*/
|
|
||||||
void send_vkey(int32_t vk);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the Virtual Key press multiple times
|
|
||||||
*/
|
|
||||||
void send_multi_vkey(int32_t vk, int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the backspace keypress, *count* times.
|
|
||||||
*/
|
|
||||||
void delete_string(int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed
|
|
||||||
*/
|
|
||||||
int32_t are_modifiers_pressed();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger normal paste ( Pressing CMD+V )
|
|
||||||
*/
|
|
||||||
void trigger_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trigger normal copy ( Pressing CMD+C )
|
|
||||||
*/
|
|
||||||
void trigger_copy();
|
|
||||||
|
|
||||||
// UI
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when the tray icon is clicked
|
|
||||||
*/
|
|
||||||
typedef void (*IconClickCallback)(void * self);
|
|
||||||
extern IconClickCallback icon_click_callback;
|
|
||||||
void register_icon_click_callback(IconClickCallback callback);
|
|
||||||
|
|
||||||
// CONTEXT MENU
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
int32_t id;
|
|
||||||
int32_t type;
|
|
||||||
char name[100];
|
|
||||||
} MenuItem;
|
|
||||||
|
|
||||||
int32_t show_context_menu(MenuItem * items, int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when the context menu is clicked
|
|
||||||
*/
|
|
||||||
typedef void (*ContextMenuClickCallback)(void * self, int32_t id);
|
|
||||||
extern ContextMenuClickCallback context_menu_click_callback;
|
|
||||||
extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update the tray icon status
|
|
||||||
*/
|
|
||||||
extern "C" void update_tray_icon(int32_t enabled);
|
|
||||||
|
|
||||||
// SYSTEM
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check if espanso is authorized to control accessibility features, needed to detect key presses.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
int32_t check_accessibility();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Prompt to authorize the accessibility features.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
int32_t prompt_accessibility();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Open Security & Privacy settings panel
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
void open_settings_panel();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active NSRunningApplication path
|
|
||||||
*/
|
|
||||||
int32_t get_active_app_bundle(char * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active NSRunningApplication bundle identifier
|
|
||||||
*/
|
|
||||||
int32_t get_active_app_identifier(char * buffer, int32_t size);
|
|
||||||
|
|
||||||
// CLIPBOARD
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the clipboard text
|
|
||||||
*/
|
|
||||||
int32_t get_clipboard(char * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the clipboard text
|
|
||||||
*/
|
|
||||||
int32_t set_clipboard(char * text);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the clipboard image to the given file
|
|
||||||
*/
|
|
||||||
int32_t set_clipboard_image(char * path);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the clipboard html
|
|
||||||
*/
|
|
||||||
int32_t set_clipboard_html(char * html, char * fallback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
|
|
||||||
*/
|
|
||||||
int32_t get_secure_input_process(int64_t *pid);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Find the executable path corresponding to the given PID, return 0 if no process was found.
|
|
||||||
*/
|
|
||||||
int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size);
|
|
||||||
|
|
||||||
};
|
|
||||||
#endif //ESPANSO_BRIDGE_H
|
|
|
@ -1,434 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "bridge.h"
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#include <IOKit/IOKitLib.h>
|
|
||||||
#include "AppDelegate.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <libproc.h>
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
void * context_instance;
|
|
||||||
char * icon_path;
|
|
||||||
char * disabled_icon_path;
|
|
||||||
int32_t show_icon;
|
|
||||||
AppDelegate * delegate_ptr;
|
|
||||||
|
|
||||||
KeypressCallback keypress_callback;
|
|
||||||
IconClickCallback icon_click_callback;
|
|
||||||
ContextMenuClickCallback context_menu_click_callback;
|
|
||||||
|
|
||||||
int32_t initialize(void * context, const char * _icon_path, const char * _disabled_icon_path, int32_t _show_icon) {
|
|
||||||
context_instance = context;
|
|
||||||
icon_path = strdup(_icon_path);
|
|
||||||
disabled_icon_path = strdup(_disabled_icon_path);
|
|
||||||
show_icon = _show_icon;
|
|
||||||
|
|
||||||
AppDelegate *delegate = [[AppDelegate alloc] init];
|
|
||||||
delegate_ptr = delegate;
|
|
||||||
NSApplication * application = [NSApplication sharedApplication];
|
|
||||||
[application setDelegate:delegate];
|
|
||||||
}
|
|
||||||
|
|
||||||
void register_keypress_callback(KeypressCallback callback) {
|
|
||||||
keypress_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void register_icon_click_callback(IconClickCallback callback) {
|
|
||||||
icon_click_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void register_context_menu_click_callback(ContextMenuClickCallback callback) {
|
|
||||||
context_menu_click_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int32_t eventloop() {
|
|
||||||
[NSApp run];
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t headless_eventloop() {
|
|
||||||
NSApplication * application = [NSApplication sharedApplication];
|
|
||||||
[NSApp run];
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void update_tray_icon(int32_t enabled) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
NSApplication * application = [NSApplication sharedApplication];
|
|
||||||
char * iconPath = icon_path;
|
|
||||||
if (!enabled) {
|
|
||||||
iconPath = disabled_icon_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[application delegate] updateIcon: iconPath];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_string(const char * string) {
|
|
||||||
char * stringCopy = strdup(string);
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
// Convert the c string to a UniChar array as required by the CGEventKeyboardSetUnicodeString method
|
|
||||||
NSString *nsString = [NSString stringWithUTF8String:stringCopy];
|
|
||||||
CFStringRef cfString = (__bridge CFStringRef) nsString;
|
|
||||||
std::vector <UniChar> buffer(nsString.length);
|
|
||||||
CFStringGetCharacters(cfString, CFRangeMake(0, nsString.length), buffer.data());
|
|
||||||
|
|
||||||
free(stringCopy);
|
|
||||||
|
|
||||||
// Send the event
|
|
||||||
|
|
||||||
// Check if the shift key is down, and if so, release it
|
|
||||||
// To see why: https://github.com/federico-terzi/espanso/issues/279
|
|
||||||
if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, 0x38)) {
|
|
||||||
CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x38, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, e2);
|
|
||||||
CFRelease(e2);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because of a bug ( or undocumented limit ) of the CGEventKeyboardSetUnicodeString method
|
|
||||||
// the string gets truncated after 20 characters, so we need to send multiple events.
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
while (i < buffer.size()) {
|
|
||||||
int chunk_size = 20;
|
|
||||||
if ((i+chunk_size) > buffer.size()) {
|
|
||||||
chunk_size = buffer.size() - i;
|
|
||||||
}
|
|
||||||
|
|
||||||
UniChar * offset_buffer = buffer.data() + i;
|
|
||||||
CGEventRef e = CGEventCreateKeyboardEvent(NULL, 0x31, true);
|
|
||||||
CGEventKeyboardSetUnicodeString(e, chunk_size, offset_buffer);
|
|
||||||
CGEventPost(kCGHIDEventTap, e);
|
|
||||||
CFRelease(e);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
// Some applications require an explicit release of the space key
|
|
||||||
// For more information: https://github.com/federico-terzi/espanso/issues/159
|
|
||||||
CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, e2);
|
|
||||||
CFRelease(e2);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
i += chunk_size;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void delete_string(int32_t count) {
|
|
||||||
send_multi_vkey(0x33, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_vkey(int32_t vk) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
CGEventRef keydown;
|
|
||||||
keydown = CGEventCreateKeyboardEvent(NULL, vk, true);
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown);
|
|
||||||
CFRelease(keydown);
|
|
||||||
|
|
||||||
usleep(500);
|
|
||||||
|
|
||||||
CGEventRef keyup;
|
|
||||||
keyup = CGEventCreateKeyboardEvent(NULL, vk, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup);
|
|
||||||
CFRelease(keyup);
|
|
||||||
|
|
||||||
usleep(500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_multi_vkey(int32_t vk, int32_t count) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
CGEventRef keydown;
|
|
||||||
keydown = CGEventCreateKeyboardEvent(NULL, vk, true);
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown);
|
|
||||||
CFRelease(keydown);
|
|
||||||
|
|
||||||
usleep(500);
|
|
||||||
|
|
||||||
CGEventRef keyup;
|
|
||||||
keyup = CGEventCreateKeyboardEvent(NULL, vk, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup);
|
|
||||||
CFRelease(keyup);
|
|
||||||
|
|
||||||
usleep(500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_paste() {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
CGEventRef keydown;
|
|
||||||
keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true); // CMD
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown);
|
|
||||||
CFRelease(keydown);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keydown2;
|
|
||||||
keydown2 = CGEventCreateKeyboardEvent(NULL, 0x09, true); // V key
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown2);
|
|
||||||
CFRelease(keydown2);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keyup;
|
|
||||||
keyup = CGEventCreateKeyboardEvent(NULL, 0x09, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup);
|
|
||||||
CFRelease(keyup);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keyup2;
|
|
||||||
keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false); // CMD
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup2);
|
|
||||||
CFRelease(keyup2);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void trigger_copy() {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
CGEventRef keydown;
|
|
||||||
keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true); // CMD
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown);
|
|
||||||
CFRelease(keydown);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keydown2;
|
|
||||||
keydown2 = CGEventCreateKeyboardEvent(NULL, 0x08, true); // C key
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown2);
|
|
||||||
CFRelease(keydown2);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keyup;
|
|
||||||
keyup = CGEventCreateKeyboardEvent(NULL, 0x08, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup);
|
|
||||||
CFRelease(keyup);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keyup2;
|
|
||||||
keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false); // CMD
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup2);
|
|
||||||
CFRelease(keyup2);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t are_modifiers_pressed() {
|
|
||||||
if ((NSEventModifierFlagControl | NSEventModifierFlagOption |
|
|
||||||
NSEventModifierFlagCommand | NSEventModifierFlagShift) & [NSEvent modifierFlags]) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_active_app_bundle(char * buffer, int32_t size) {
|
|
||||||
NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
|
||||||
NSString *bundlePath = [frontApp bundleURL].path;
|
|
||||||
const char * path = [bundlePath UTF8String];
|
|
||||||
|
|
||||||
snprintf(buffer, size, "%s", path);
|
|
||||||
|
|
||||||
[bundlePath release];
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_active_app_identifier(char * buffer, int32_t size) {
|
|
||||||
NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
|
||||||
NSString *bundleId = frontApp.bundleIdentifier;
|
|
||||||
const char * bundle = [bundleId UTF8String];
|
|
||||||
|
|
||||||
snprintf(buffer, size, "%s", bundle);
|
|
||||||
|
|
||||||
[bundleId release];
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_clipboard(char * buffer, int32_t size) {
|
|
||||||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
|
||||||
for (id element in pasteboard.pasteboardItems) {
|
|
||||||
NSString *string = [element stringForType: NSPasteboardTypeString];
|
|
||||||
if (string != NULL) {
|
|
||||||
const char * text = [string UTF8String];
|
|
||||||
snprintf(buffer, size, "%s", text);
|
|
||||||
|
|
||||||
[string release];
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t set_clipboard(char * text) {
|
|
||||||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
|
||||||
NSArray *array = @[NSPasteboardTypeString];
|
|
||||||
[pasteboard declareTypes:array owner:nil];
|
|
||||||
|
|
||||||
NSString *nsText = [NSString stringWithUTF8String:text];
|
|
||||||
[pasteboard setString:nsText forType:NSPasteboardTypeString];
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t set_clipboard_image(char *path) {
|
|
||||||
NSString *pathString = [NSString stringWithUTF8String:path];
|
|
||||||
NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString];
|
|
||||||
int result = 0;
|
|
||||||
|
|
||||||
if (image != nil) {
|
|
||||||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
|
||||||
[pasteboard clearContents];
|
|
||||||
NSArray *copiedObjects = [NSArray arrayWithObject:image];
|
|
||||||
[pasteboard writeObjects:copiedObjects];
|
|
||||||
result = 1;
|
|
||||||
}
|
|
||||||
[image release];
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t set_clipboard_html(char * html, char * fallback_text) {
|
|
||||||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
|
||||||
NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString];
|
|
||||||
[pasteboard declareTypes:array owner:nil];
|
|
||||||
|
|
||||||
NSString *nsHtml = [NSString stringWithUTF8String:html];
|
|
||||||
NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil];
|
|
||||||
NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil];
|
|
||||||
|
|
||||||
NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length])
|
|
||||||
documentAttributes:nil];
|
|
||||||
|
|
||||||
[pasteboard setData:rtf forType:NSRTFPboardType];
|
|
||||||
|
|
||||||
NSString *nsText = [NSString stringWithUTF8String:fallback_text];
|
|
||||||
[pasteboard setString:nsText forType:NSPasteboardTypeString];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// CONTEXT MENU
|
|
||||||
|
|
||||||
int32_t show_context_menu(MenuItem * items, int32_t count) {
|
|
||||||
MenuItem * item_copy = (MenuItem*)malloc(sizeof(MenuItem)*count);
|
|
||||||
memcpy(item_copy, items, sizeof(MenuItem)*count);
|
|
||||||
int32_t count_copy = count;
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
|
||||||
|
|
||||||
NSMenu *espansoMenu = [[NSMenu alloc] initWithTitle:@"Espanso"];
|
|
||||||
|
|
||||||
for (int i = 0; i<count_copy; i++) {
|
|
||||||
if (item_copy[i].type == 1) {
|
|
||||||
NSString *title = [NSString stringWithUTF8String:item_copy[i].name];
|
|
||||||
NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:title action:@selector(contextMenuClick:) keyEquivalent:@""];
|
|
||||||
[newMenu setTag:(NSInteger)item_copy[i].id];
|
|
||||||
[espansoMenu addItem: newMenu];
|
|
||||||
}else{
|
|
||||||
[espansoMenu addItem: [NSMenuItem separatorItem]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(item_copy);
|
|
||||||
|
|
||||||
[delegate_ptr->myStatusItem popUpStatusItemMenu:espansoMenu];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10.9+ only, see this url for compatibility:
|
|
||||||
// http://stackoverflow.com/questions/17693408/enable-access-for-assistive-devices-programmatically-on-10-9
|
|
||||||
int32_t check_accessibility() {
|
|
||||||
NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @NO};
|
|
||||||
return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t prompt_accessibility() {
|
|
||||||
NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES};
|
|
||||||
return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
void open_settings_panel() {
|
|
||||||
NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
||||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys
|
|
||||||
int32_t get_secure_input_process(int64_t *pid) {
|
|
||||||
NSArray *consoleUsersArray;
|
|
||||||
io_service_t rootService;
|
|
||||||
int32_t result = 0;
|
|
||||||
|
|
||||||
if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0)
|
|
||||||
{
|
|
||||||
if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil)
|
|
||||||
{
|
|
||||||
if ([consoleUsersArray isKindOfClass:[NSArray class]]) // Be careful - ensure this really is an array
|
|
||||||
{
|
|
||||||
for (NSDictionary *consoleUserDict in consoleUsersArray) {
|
|
||||||
NSNumber *secureInputPID;
|
|
||||||
|
|
||||||
if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil)
|
|
||||||
{
|
|
||||||
if ([secureInputPID isKindOfClass:[NSNumber class]])
|
|
||||||
{
|
|
||||||
*pid = ((UInt64) [secureInputPID intValue]);
|
|
||||||
result = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CFRelease((CFTypeRef)consoleUsersArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
IOObjectRelease((io_object_t) rootService);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size) {
|
|
||||||
int res = proc_pidpath((pid_t) pid, buff, buff_size);
|
|
||||||
if ( res <= 0 ) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.0)
|
|
||||||
project(libwinbridge)
|
|
||||||
|
|
||||||
set (CMAKE_CXX_STANDARD 14)
|
|
||||||
|
|
||||||
add_library(winbridge STATIC bridge.cpp bridge.h)
|
|
||||||
|
|
||||||
install(TARGETS winbridge DESTINATION .)
|
|
|
@ -1,934 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "bridge.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <iostream>
|
|
||||||
#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 <shellapi.h>
|
|
||||||
|
|
||||||
#pragma comment( lib, "gdiplus.lib" )
|
|
||||||
#include <gdiplus.h>
|
|
||||||
#include <Windows.h>
|
|
||||||
|
|
||||||
// How many milliseconds must pass between keystrokes to refresh the keyboard layout
|
|
||||||
const long refreshKeyboardLayoutInterval = 2000;
|
|
||||||
|
|
||||||
void * manager_instance;
|
|
||||||
int32_t show_icon;
|
|
||||||
|
|
||||||
// Keyboard listening
|
|
||||||
|
|
||||||
DWORD lastKeyboardPressTick = 0;
|
|
||||||
HKL currentKeyboardLayout;
|
|
||||||
HWND window;
|
|
||||||
const wchar_t* const winclass = L"Espanso";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// UI
|
|
||||||
|
|
||||||
#define APPWM_ICON_CLICK (WM_APP + 1)
|
|
||||||
#define APPWM_NOTIFICATION_POPUP (WM_APP + 2)
|
|
||||||
#define APPWM_NOTIFICATION_CLOSE (WM_APP + 3)
|
|
||||||
#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 4)
|
|
||||||
|
|
||||||
const wchar_t* const notification_winclass = L"EspansoNotification";
|
|
||||||
HWND nw = NULL;
|
|
||||||
HWND hwnd_st_u = NULL;
|
|
||||||
HBITMAP g_espanso_bmp = NULL;
|
|
||||||
HICON g_espanso_ico = NULL;
|
|
||||||
HICON g_espanso_red_ico = NULL;
|
|
||||||
NOTIFYICONDATA nid = {};
|
|
||||||
|
|
||||||
UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
|
|
||||||
KeypressCallback keypress_callback = NULL;
|
|
||||||
IconClickCallback icon_click_callback = NULL;
|
|
||||||
ContextMenuClickCallback context_menu_click_callback = NULL;
|
|
||||||
|
|
||||||
void register_keypress_callback(KeypressCallback callback) {
|
|
||||||
keypress_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void register_icon_click_callback(IconClickCallback callback) {
|
|
||||||
icon_click_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void register_context_menu_click_callback(ContextMenuClickCallback callback) {
|
|
||||||
context_menu_click_callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Message handler procedure for the windows
|
|
||||||
*/
|
|
||||||
LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
|
|
||||||
{
|
|
||||||
HDC hdcStatic = NULL;
|
|
||||||
|
|
||||||
switch (msg)
|
|
||||||
{
|
|
||||||
case WM_DESTROY:
|
|
||||||
std::cout << "\ndestroying window\n";
|
|
||||||
PostQuitMessage(0);
|
|
||||||
DeleteObject(g_espanso_bmp);
|
|
||||||
DeleteObject(g_espanso_ico);
|
|
||||||
return 0L;
|
|
||||||
case WM_COMMAND: // Click on the tray icon context menu
|
|
||||||
{
|
|
||||||
UINT idItem = (UINT)LOWORD(wp);
|
|
||||||
UINT flags = (UINT)HIWORD(wp);
|
|
||||||
|
|
||||||
if (flags == 0) {
|
|
||||||
context_menu_click_callback(manager_instance, (int32_t)idItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APPWM_NOTIFICATION_POPUP: // Request to show a notification
|
|
||||||
{
|
|
||||||
std::unique_ptr<wchar_t[]> ptr(reinterpret_cast<wchar_t*>(wp));
|
|
||||||
|
|
||||||
SetWindowText(hwnd_st_u, L" "); // Clear the previous text
|
|
||||||
SetWindowText(hwnd_st_u, ptr.get());
|
|
||||||
|
|
||||||
// Show the window
|
|
||||||
ShowWindow(nw, SW_SHOWNOACTIVATE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APPWM_NOTIFICATION_CLOSE: // Request to close a notification
|
|
||||||
{
|
|
||||||
// Hide the window
|
|
||||||
ShowWindow(nw, SW_HIDE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu
|
|
||||||
{
|
|
||||||
HMENU hPopupMenu = CreatePopupMenu();
|
|
||||||
|
|
||||||
// Create the menu
|
|
||||||
|
|
||||||
int32_t count = static_cast<int32_t>(lp);
|
|
||||||
std::unique_ptr<MenuItem[]> items(reinterpret_cast<MenuItem*>(wp));
|
|
||||||
|
|
||||||
for (int i = 0; i<count; i++) {
|
|
||||||
if (items[i].type == 1) {
|
|
||||||
InsertMenu(hPopupMenu, i, MF_BYPOSITION | MF_STRING, items[i].id, items[i].name);
|
|
||||||
}else{
|
|
||||||
InsertMenu(hPopupMenu, i, MF_BYPOSITION | MF_SEPARATOR, items[i].id, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
POINT pt;
|
|
||||||
GetCursorPos(&pt);
|
|
||||||
SetForegroundWindow(nw);
|
|
||||||
TrackPopupMenu(hPopupMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, nw, NULL);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case APPWM_ICON_CLICK: // Click on the tray icon
|
|
||||||
{
|
|
||||||
switch (lp)
|
|
||||||
{
|
|
||||||
case WM_LBUTTONUP:
|
|
||||||
case WM_RBUTTONUP:
|
|
||||||
icon_click_callback(manager_instance);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case WM_PAINT:
|
|
||||||
{
|
|
||||||
BITMAP bm;
|
|
||||||
PAINTSTRUCT ps;
|
|
||||||
|
|
||||||
HDC hdc = BeginPaint(window, &ps);
|
|
||||||
|
|
||||||
HDC hdcMem = CreateCompatibleDC(hdc);
|
|
||||||
HBITMAP hbmOld = (HBITMAP) SelectObject(hdcMem, g_espanso_bmp);
|
|
||||||
|
|
||||||
GetObject(g_espanso_bmp, sizeof(bm), &bm);
|
|
||||||
|
|
||||||
BitBlt(hdc, 10, 10, 80, 80, hdcMem, 0, 0, SRCCOPY);
|
|
||||||
|
|
||||||
SelectObject(hdcMem, hbmOld);
|
|
||||||
DeleteDC(hdcMem);
|
|
||||||
|
|
||||||
EndPaint(window, &ps);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WM_CTLCOLORSTATIC:
|
|
||||||
hdcStatic = (HDC)wp;
|
|
||||||
SetTextColor(hdcStatic, RGB(0, 0, 0));
|
|
||||||
SetBkColor(hdcStatic, RGB(255, 255, 255));
|
|
||||||
//SetBkMode(hdcStatic, OPAQUE);
|
|
||||||
|
|
||||||
return (LRESULT)GetStockObject(NULL_BRUSH);
|
|
||||||
case WM_INPUT: // Message relative to the RAW INPUT events
|
|
||||||
{
|
|
||||||
// Get the input size
|
|
||||||
UINT dwSize;
|
|
||||||
GetRawInputData(
|
|
||||||
(HRAWINPUT)lp,
|
|
||||||
RID_INPUT,
|
|
||||||
NULL,
|
|
||||||
&dwSize,
|
|
||||||
sizeof(RAWINPUTHEADER)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a proper sized structure to hold the data
|
|
||||||
std::vector<BYTE> lpb(dwSize);
|
|
||||||
|
|
||||||
// Request the Raw input data
|
|
||||||
if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize,
|
|
||||||
sizeof(RAWINPUTHEADER)) != dwSize) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the input data
|
|
||||||
RAWINPUT* raw = reinterpret_cast<RAWINPUT*>(lpb.data());
|
|
||||||
// Make sure it's a keyboard type event, relative to a key press.
|
|
||||||
if (raw->header.dwType == RIM_TYPEKEYBOARD)
|
|
||||||
{
|
|
||||||
// We only want KEY UP AND KEY DOWN events
|
|
||||||
if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP &&
|
|
||||||
raw->data.keyboard.Message != WM_SYSKEYDOWN) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The alt key sends a SYSKEYDOWN instead of KEYDOWN event
|
|
||||||
int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN ||
|
|
||||||
raw->data.keyboard.Message == WM_SYSKEYDOWN;
|
|
||||||
|
|
||||||
DWORD currentTick = GetTickCount();
|
|
||||||
|
|
||||||
// If enough time has passed between the last keypress and now, refresh the keyboard layout
|
|
||||||
if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval) {
|
|
||||||
|
|
||||||
// Because keyboard layouts on windows are Window-specific, to get the current
|
|
||||||
// layout we need to get the foreground window and get its layout.
|
|
||||||
|
|
||||||
HWND hwnd = GetForegroundWindow();
|
|
||||||
if (hwnd) {
|
|
||||||
DWORD threadID = GetWindowThreadProcessId(hwnd, NULL);
|
|
||||||
HKL newKeyboardLayout = GetKeyboardLayout(threadID);
|
|
||||||
|
|
||||||
// It's not always valid, so update the current value only if available.
|
|
||||||
if (newKeyboardLayout != 0) {
|
|
||||||
currentKeyboardLayout = newKeyboardLayout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastKeyboardPressTick = currentTick;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get keyboard state ( necessary to decode the associated Unicode char )
|
|
||||||
std::vector<BYTE> lpKeyState(256);
|
|
||||||
if (GetKeyboardState(lpKeyState.data())) {
|
|
||||||
// Convert the virtual key to an unicode char
|
|
||||||
std::array<WCHAR, 4> buffer;
|
|
||||||
|
|
||||||
// This flag is needed to avoid chaning the keyboard state for some layouts.
|
|
||||||
// Refer to issue: https://github.com/federico-terzi/espanso/issues/86
|
|
||||||
UINT flags = 1 << 2;
|
|
||||||
|
|
||||||
int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), flags, currentKeyboardLayout);
|
|
||||||
|
|
||||||
//std::cout << result << " " << buffer[0] << " " << raw->data.keyboard.VKey << std::endl;
|
|
||||||
|
|
||||||
// We need to call the callback in two different ways based on the type of key
|
|
||||||
// The only modifier we use that has a result > 0 is the BACKSPACE, so we have to consider it.
|
|
||||||
if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) {
|
|
||||||
keypress_callback(manager_instance, reinterpret_cast<uint16_t*>(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, 0, is_key_down);
|
|
||||||
}else{
|
|
||||||
//std::cout << raw->data.keyboard.MakeCode << " " << raw->data.keyboard.Flags << std::endl;
|
|
||||||
int variant = 0;
|
|
||||||
if (raw->data.keyboard.VKey == VK_SHIFT) {
|
|
||||||
// To discriminate between the left and right shift, we need to employ a workaround.
|
|
||||||
// See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput
|
|
||||||
if (raw->data.keyboard.MakeCode == 42) { // Left shift
|
|
||||||
variant = LEFT_VARIANT;
|
|
||||||
}if (raw->data.keyboard.MakeCode == 54) { // Right shift
|
|
||||||
variant = RIGHT_VARIANT;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
// Also the ALT and CTRL key are special cases
|
|
||||||
// Check out the previous Stackoverflow question for more information
|
|
||||||
if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU) {
|
|
||||||
if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0) {
|
|
||||||
variant = RIGHT_VARIANT;
|
|
||||||
}else{
|
|
||||||
variant = LEFT_VARIANT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, variant, is_key_down);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse input, registered as "other" events. Needed to improve the reliability of word matches
|
|
||||||
{
|
|
||||||
if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN)) != 0) {
|
|
||||||
//std::cout << "mouse down" << std::endl;
|
|
||||||
keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if (msg == WM_TASKBARCREATED) { // Explorer crashed, recreate the icon
|
|
||||||
if (show_icon) {
|
|
||||||
Shell_NotifyIcon(NIM_ADD, &nid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DefWindowProc(window, msg, wp, lp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t _show_icon) {
|
|
||||||
manager_instance = self;
|
|
||||||
show_icon = _show_icon;
|
|
||||||
|
|
||||||
// Load the images
|
|
||||||
g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
|
|
||||||
g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
|
|
||||||
g_espanso_red_ico = (HICON)LoadImage(NULL, red_ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE);
|
|
||||||
|
|
||||||
// Make the notification capable of handling different screen definitions
|
|
||||||
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
|
|
||||||
|
|
||||||
// Initialize the default keyboard layout
|
|
||||||
currentKeyboardLayout = GetKeyboardLayout(0);
|
|
||||||
|
|
||||||
// Initialize the Worker window
|
|
||||||
|
|
||||||
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
|
|
||||||
WNDCLASSEX wndclass = {
|
|
||||||
sizeof(WNDCLASSEX), // cbSize: Size of this structure
|
|
||||||
0, // style: Class styles
|
|
||||||
window_procedure, // lpfnWndProc: Pointer to the window procedure
|
|
||||||
0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure
|
|
||||||
0, // cbWndExtra: The number of extra bytes to allocate following the window instance.
|
|
||||||
GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class.
|
|
||||||
NULL, // hIcon: A handle to the class icon.
|
|
||||||
LoadCursor(0,IDC_ARROW), // hCursor: A handle to the class cursor.
|
|
||||||
NULL, // hbrBackground: A handle to the class background brush.
|
|
||||||
NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
|
|
||||||
winclass, // lpszClassName: A pointer to a null-terminated string or is an atom.
|
|
||||||
NULL // hIconSm: A handle to a small icon that is associated with the window class.
|
|
||||||
};
|
|
||||||
|
|
||||||
// Notification Window
|
|
||||||
|
|
||||||
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
|
|
||||||
WNDCLASSEX notificationwndclass = {
|
|
||||||
sizeof(WNDCLASSEX), // cbSize: Size of this structure
|
|
||||||
0, // style: Class styles
|
|
||||||
window_procedure, // lpfnWndProc: Pointer to the window procedure
|
|
||||||
0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure
|
|
||||||
0, // cbWndExtra: The number of extra bytes to allocate following the window instance.
|
|
||||||
GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class.
|
|
||||||
NULL, // hIcon: A handle to the class icon.
|
|
||||||
LoadCursor(0,IDC_ARROW), // hCursor: A handle to the class cursor.
|
|
||||||
NULL, // hbrBackground: A handle to the class background brush.
|
|
||||||
NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
|
|
||||||
notification_winclass, // lpszClassName: A pointer to a null-terminated string or is an atom.
|
|
||||||
NULL // hIconSm: A handle to a small icon that is associated with the window class.
|
|
||||||
};
|
|
||||||
|
|
||||||
if (RegisterClassEx(&wndclass) && RegisterClassEx(¬ificationwndclass))
|
|
||||||
{
|
|
||||||
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
|
|
||||||
window = CreateWindowEx(
|
|
||||||
0, // dwExStyle: The extended window style of the window being created.
|
|
||||||
winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
|
|
||||||
L"Espanso Worker Window", // lpWindowName: The window name.
|
|
||||||
WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created.
|
|
||||||
CW_USEDEFAULT, // X: The initial horizontal position of the window.
|
|
||||||
CW_USEDEFAULT, // Y: The initial vertical position of the window.
|
|
||||||
100, // nWidth: The width, in device units, of the window.
|
|
||||||
100, // nHeight: The height, in device units, of the window.
|
|
||||||
NULL, // hWndParent: handle to the parent or owner window of the window being created.
|
|
||||||
NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
|
|
||||||
GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window.
|
|
||||||
NULL // lpParam: Pointer to a value to be passed to the window
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register raw inputs
|
|
||||||
RAWINPUTDEVICE Rid[2];
|
|
||||||
|
|
||||||
Rid[0].usUsagePage = 0x01;
|
|
||||||
Rid[0].usUsage = 0x06;
|
|
||||||
Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID keyboard and also ignores legacy keyboard messages
|
|
||||||
Rid[0].hwndTarget = window;
|
|
||||||
|
|
||||||
Rid[1].usUsagePage = 0x01;
|
|
||||||
Rid[1].usUsage = 0x02;
|
|
||||||
Rid[1].dwFlags = RIDEV_INPUTSINK; // adds HID mouse and also ignores legacy mouse messages
|
|
||||||
Rid[1].hwndTarget = window;
|
|
||||||
|
|
||||||
if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) { // Something went wrong, error.
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the notification window
|
|
||||||
nw = CreateWindowEx(
|
|
||||||
WS_EX_TOOLWINDOW | WS_EX_TOPMOST, // dwExStyle: The extended window style of the window being created.
|
|
||||||
notification_winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
|
|
||||||
L"Espanso Notification", // lpWindowName: The window name.
|
|
||||||
WS_POPUPWINDOW, // dwStyle: The style of the window being created.
|
|
||||||
CW_USEDEFAULT, // X: The initial horizontal position of the window.
|
|
||||||
CW_USEDEFAULT, // Y: The initial vertical position of the window.
|
|
||||||
300, // nWidth: The width, in device units, of the window.
|
|
||||||
100, // nHeight: The height, in device units, of the window.
|
|
||||||
NULL, // hWndParent: handle to the parent or owner window of the window being created.
|
|
||||||
NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
|
|
||||||
GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window.
|
|
||||||
NULL // lpParam: Pointer to a value to be passed to the window
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nw)
|
|
||||||
{
|
|
||||||
int x, w, y, h;
|
|
||||||
y = 40; h = 30;
|
|
||||||
x = 100; w = 180;
|
|
||||||
hwnd_st_u = CreateWindowEx(0, L"static", L"ST_U",
|
|
||||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | SS_CENTER,
|
|
||||||
x, y, w, h,
|
|
||||||
nw, (HMENU)(501),
|
|
||||||
(HINSTANCE)GetWindowLong(nw, GWLP_HINSTANCE), NULL);
|
|
||||||
|
|
||||||
SetWindowText(hwnd_st_u, L"Loading...");
|
|
||||||
|
|
||||||
int posX = GetSystemMetrics(SM_CXSCREEN) - 350;
|
|
||||||
int posY = GetSystemMetrics(SM_CYSCREEN) - 200;
|
|
||||||
|
|
||||||
SetWindowPos(nw, HWND_TOP, posX, posY, 0, 0, SWP_NOSIZE);
|
|
||||||
|
|
||||||
// Hide the window
|
|
||||||
ShowWindow(nw, SW_HIDE);
|
|
||||||
|
|
||||||
// Setup the icon in the notification space
|
|
||||||
|
|
||||||
SendMessage(nw, WM_SETICON, ICON_BIG, (LPARAM)g_espanso_ico);
|
|
||||||
SendMessage(nw, WM_SETICON, ICON_SMALL, (LPARAM)g_espanso_ico);
|
|
||||||
|
|
||||||
//Notification
|
|
||||||
nid.cbSize = sizeof(nid);
|
|
||||||
nid.hWnd = nw;
|
|
||||||
nid.uID = 1;
|
|
||||||
nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
|
|
||||||
nid.uCallbackMessage = APPWM_ICON_CLICK;
|
|
||||||
nid.hIcon = g_espanso_ico;
|
|
||||||
StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), L"espanso");
|
|
||||||
|
|
||||||
// Show the notification.
|
|
||||||
if (show_icon) {
|
|
||||||
Shell_NotifyIcon(NIM_ADD, &nid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
// Something went wrong, error.
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void eventloop() {
|
|
||||||
if (window)
|
|
||||||
{
|
|
||||||
// Hide the window
|
|
||||||
ShowWindow(window, SW_HIDE);
|
|
||||||
|
|
||||||
// Enter the Event loop
|
|
||||||
MSG msg;
|
|
||||||
while (GetMessage(&msg, 0, 0, 0)) DispatchMessage(&msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Something went wrong, this should have been an infinite loop.
|
|
||||||
}
|
|
||||||
|
|
||||||
void update_tray_icon(int32_t enabled) {
|
|
||||||
if (enabled) {
|
|
||||||
nid.hIcon = g_espanso_ico;
|
|
||||||
}else{
|
|
||||||
nid.hIcon = g_espanso_red_ico;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
if (show_icon) {
|
|
||||||
Shell_NotifyIcon(NIM_MODIFY, &nid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Type the given string simulating keyboard presses.
|
|
||||||
*/
|
|
||||||
void send_string(const wchar_t * string) {
|
|
||||||
std::wstring msg = string;
|
|
||||||
|
|
||||||
std::vector<INPUT> vec;
|
|
||||||
for (auto ch : msg)
|
|
||||||
{
|
|
||||||
INPUT input = { 0 };
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.dwFlags = KEYEVENTF_UNICODE;
|
|
||||||
input.ki.wScan = ch;
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
|
||||||
vec.push_back(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the backspace keypress, *count* times.
|
|
||||||
*/
|
|
||||||
void delete_string(int32_t count, int32_t delay) {
|
|
||||||
if (delay != 0) {
|
|
||||||
send_multi_vkey_with_delay(VK_BACK, count, delay);
|
|
||||||
}else{
|
|
||||||
send_multi_vkey(VK_BACK, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_vkey(int32_t vk) {
|
|
||||||
std::vector<INPUT> vec;
|
|
||||||
|
|
||||||
INPUT input = { 0 };
|
|
||||||
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.wScan = 0;
|
|
||||||
input.ki.time = 0;
|
|
||||||
input.ki.dwExtraInfo = 0;
|
|
||||||
input.ki.wVk = vk;
|
|
||||||
input.ki.dwFlags = 0; // 0 for key press
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_multi_vkey(int32_t vk, int32_t count) {
|
|
||||||
std::vector<INPUT> vec;
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
INPUT input = { 0 };
|
|
||||||
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.wScan = 0;
|
|
||||||
input.ki.time = 0;
|
|
||||||
input.ki.dwExtraInfo = 0;
|
|
||||||
input.ki.wVk = vk;
|
|
||||||
input.ki.dwFlags = 0; // 0 for key press
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
|
|
||||||
vec.push_back(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay) {
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
INPUT input = { 0 };
|
|
||||||
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.wScan = 0;
|
|
||||||
input.ki.time = 0;
|
|
||||||
input.ki.dwExtraInfo = 0;
|
|
||||||
input.ki.wVk = vk;
|
|
||||||
input.ki.dwFlags = 0; // 0 for key press
|
|
||||||
SendInput(1, &input, sizeof(INPUT));
|
|
||||||
|
|
||||||
Sleep(delay);
|
|
||||||
|
|
||||||
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
|
|
||||||
SendInput(1, &input, sizeof(INPUT));
|
|
||||||
|
|
||||||
Sleep(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void trigger_shift_paste() {
|
|
||||||
std::vector<INPUT> vec;
|
|
||||||
|
|
||||||
INPUT input = { 0 };
|
|
||||||
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.wScan = 0;
|
|
||||||
input.ki.time = 0;
|
|
||||||
input.ki.dwExtraInfo = 0;
|
|
||||||
input.ki.wVk = VK_CONTROL;
|
|
||||||
input.ki.dwFlags = 0; // 0 for key press
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = VK_SHIFT; // SHIFT KEY
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = 0x56; // V KEY
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = VK_SHIFT; // SHIFT KEY
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = VK_CONTROL;
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_paste() {
|
|
||||||
std::vector<INPUT> vec;
|
|
||||||
|
|
||||||
INPUT input = { 0 };
|
|
||||||
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.wScan = 0;
|
|
||||||
input.ki.time = 0;
|
|
||||||
input.ki.dwExtraInfo = 0;
|
|
||||||
input.ki.wVk = VK_CONTROL;
|
|
||||||
input.ki.dwFlags = 0; // 0 for key press
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = 0x56; // V KEY
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = VK_CONTROL;
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
void trigger_copy() {
|
|
||||||
std::vector<INPUT> vec;
|
|
||||||
|
|
||||||
INPUT input = { 0 };
|
|
||||||
|
|
||||||
input.type = INPUT_KEYBOARD;
|
|
||||||
input.ki.wScan = 0;
|
|
||||||
input.ki.time = 0;
|
|
||||||
input.ki.dwExtraInfo = 0;
|
|
||||||
input.ki.wVk = VK_CONTROL;
|
|
||||||
input.ki.dwFlags = 0; // 0 for key press
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = 0x43; // C KEY
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
input.ki.wVk = VK_CONTROL;
|
|
||||||
vec.push_back(input);
|
|
||||||
|
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t are_modifiers_pressed() {
|
|
||||||
short ctrl_pressed = GetAsyncKeyState(VK_CONTROL);
|
|
||||||
short enter_pressed = GetAsyncKeyState(VK_RETURN);
|
|
||||||
short alt_pressed = GetAsyncKeyState(VK_MENU);
|
|
||||||
short shift_pressed = GetAsyncKeyState(VK_SHIFT);
|
|
||||||
short meta_pressed = GetAsyncKeyState(VK_LWIN);
|
|
||||||
short rmeta_pressed = GetAsyncKeyState(VK_RWIN);
|
|
||||||
if (((ctrl_pressed & 0x8000) +
|
|
||||||
(enter_pressed & 0x8000) +
|
|
||||||
(alt_pressed & 0x8000) +
|
|
||||||
(shift_pressed & 0x8000) +
|
|
||||||
(meta_pressed & 0x8000) +
|
|
||||||
(rmeta_pressed & 0x8000)) != 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// SYSTEM
|
|
||||||
|
|
||||||
int32_t get_active_window_name(wchar_t * buffer, int32_t size) {
|
|
||||||
HWND hwnd = GetForegroundWindow();
|
|
||||||
|
|
||||||
return GetWindowText(hwnd, buffer, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_active_window_executable(wchar_t * buffer, int32_t size) {
|
|
||||||
HWND hwnd = GetForegroundWindow();
|
|
||||||
|
|
||||||
// Extract the window PID
|
|
||||||
DWORD windowPid;
|
|
||||||
GetWindowThreadProcessId(hwnd, &windowPid);
|
|
||||||
|
|
||||||
DWORD dsize = (DWORD) size;
|
|
||||||
|
|
||||||
// Extract the process executable file path
|
|
||||||
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid);
|
|
||||||
int res = QueryFullProcessImageNameW(process, 0, buffer, &dsize);
|
|
||||||
CloseHandle(process);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notifications
|
|
||||||
|
|
||||||
int32_t show_notification(wchar_t * message) {
|
|
||||||
if (nw != NULL) {
|
|
||||||
wchar_t * buffer = new wchar_t[100];
|
|
||||||
swprintf(buffer, 100, L"%ls", message);
|
|
||||||
|
|
||||||
PostMessage(nw, APPWM_NOTIFICATION_POPUP, reinterpret_cast<WPARAM>(buffer), 0);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void close_notification() {
|
|
||||||
if (nw != NULL) {
|
|
||||||
PostMessage(nw, APPWM_NOTIFICATION_CLOSE, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t show_context_menu(MenuItem * items, int32_t count) {
|
|
||||||
if (nw != NULL) {
|
|
||||||
MenuItem * items_buffer = new MenuItem[count];
|
|
||||||
memcpy(items_buffer, items, sizeof(MenuItem)*count);
|
|
||||||
|
|
||||||
PostMessage(nw, APPWM_SHOW_CONTEXT_MENU, reinterpret_cast<WPARAM>(items_buffer), static_cast<LPARAM>(count));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void cleanup_ui() {
|
|
||||||
Shell_NotifyIcon(NIM_DELETE, &nid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SYSTEM
|
|
||||||
|
|
||||||
int32_t start_daemon_process() {
|
|
||||||
wchar_t cmd[MAX_PATH];
|
|
||||||
swprintf(cmd, MAX_PATH, L"espanso.exe daemon");
|
|
||||||
|
|
||||||
// Get current espanso directory
|
|
||||||
TCHAR espansoFilePath[MAX_PATH];
|
|
||||||
GetModuleFileName(NULL, espansoFilePath, MAX_PATH);
|
|
||||||
|
|
||||||
STARTUPINFO si = { sizeof(si) };
|
|
||||||
PROCESS_INFORMATION pi;
|
|
||||||
|
|
||||||
// Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
|
|
||||||
BOOL res = CreateProcess(
|
|
||||||
espansoFilePath,
|
|
||||||
cmd,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
FALSE,
|
|
||||||
DETACHED_PROCESS | CREATE_NO_WINDOW,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
&si,
|
|
||||||
&pi
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int32_t start_process(wchar_t * _cmd) {
|
|
||||||
wchar_t cmd[MAX_PATH];
|
|
||||||
swprintf(cmd, MAX_PATH, _cmd);
|
|
||||||
|
|
||||||
STARTUPINFO si = { sizeof(si) };
|
|
||||||
PROCESS_INFORMATION pi;
|
|
||||||
|
|
||||||
// Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
|
|
||||||
BOOL res = CreateProcess(
|
|
||||||
NULL,
|
|
||||||
cmd,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
FALSE,
|
|
||||||
DETACHED_PROCESS,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
&si,
|
|
||||||
&pi
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLIPBOARD
|
|
||||||
|
|
||||||
int32_t set_clipboard(wchar_t *text) {
|
|
||||||
int32_t result = 0;
|
|
||||||
const size_t len = wcslen(text) + 1;
|
|
||||||
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t));
|
|
||||||
memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t));
|
|
||||||
GlobalUnlock(hMem);
|
|
||||||
if (!OpenClipboard(NULL)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
EmptyClipboard();
|
|
||||||
if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
|
|
||||||
result = -2;
|
|
||||||
}
|
|
||||||
CloseClipboard();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t get_clipboard(wchar_t *buffer, int32_t size) {
|
|
||||||
int32_t result = 1;
|
|
||||||
if (!OpenClipboard(NULL)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get handle of clipboard object for ANSI text
|
|
||||||
HANDLE hData = GetClipboardData(CF_UNICODETEXT);
|
|
||||||
if (!hData) {
|
|
||||||
result = -2;
|
|
||||||
}else{
|
|
||||||
HGLOBAL hMem = GlobalLock(hData);
|
|
||||||
if (!hMem) {
|
|
||||||
result = -3;
|
|
||||||
}else{
|
|
||||||
GlobalUnlock(hMem);
|
|
||||||
swprintf(buffer, size, L"%s", hMem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CloseClipboard();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t set_clipboard_image(wchar_t *path) {
|
|
||||||
bool result = false;
|
|
||||||
|
|
||||||
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
|
|
||||||
ULONG_PTR gdiplusToken;
|
|
||||||
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
|
|
||||||
|
|
||||||
Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path);
|
|
||||||
if (gdibmp)
|
|
||||||
{
|
|
||||||
HBITMAP hbitmap;
|
|
||||||
gdibmp->GetHBITMAP(0, &hbitmap);
|
|
||||||
if (OpenClipboard(NULL))
|
|
||||||
{
|
|
||||||
EmptyClipboard();
|
|
||||||
DIBSECTION ds;
|
|
||||||
if (GetObject(hbitmap, sizeof(DIBSECTION), &ds))
|
|
||||||
{
|
|
||||||
HDC hdc = GetDC(HWND_DESKTOP);
|
|
||||||
//create compatible bitmap (get DDB from DIB)
|
|
||||||
HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT,
|
|
||||||
ds.dsBm.bmBits, (BITMAPINFO*)&ds.dsBmih, DIB_RGB_COLORS);
|
|
||||||
ReleaseDC(HWND_DESKTOP, hdc);
|
|
||||||
SetClipboardData(CF_BITMAP, hbitmap_ddb);
|
|
||||||
DeleteObject(hbitmap_ddb);
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
CloseClipboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
//cleanup:
|
|
||||||
DeleteObject(hbitmap);
|
|
||||||
delete gdibmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gdiplus::GdiplusShutdown(gdiplusToken);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
|
|
||||||
int32_t set_clipboard_html(char * html, wchar_t * text_fallback) {
|
|
||||||
// Get clipboard id for HTML format
|
|
||||||
static int cfid = 0;
|
|
||||||
if(!cfid) {
|
|
||||||
cfid = RegisterClipboardFormat(L"HTML Format");
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t result = 0;
|
|
||||||
const size_t html_len = strlen(html) + 1;
|
|
||||||
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char));
|
|
||||||
memcpy(GlobalLock(hMem), html, html_len * sizeof(char));
|
|
||||||
GlobalUnlock(hMem);
|
|
||||||
|
|
||||||
const size_t fallback_len = wcslen(text_fallback) + 1;
|
|
||||||
HGLOBAL hMemFallback = GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t));
|
|
||||||
memcpy(GlobalLock(hMemFallback), text_fallback, fallback_len * sizeof(wchar_t));
|
|
||||||
GlobalUnlock(hMemFallback);
|
|
||||||
|
|
||||||
if (!OpenClipboard(NULL)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
EmptyClipboard();
|
|
||||||
if (!SetClipboardData(cfid, hMem)) {
|
|
||||||
result = -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!SetClipboardData(CF_UNICODETEXT, hMemFallback)) {
|
|
||||||
result = -3;
|
|
||||||
}
|
|
||||||
CloseClipboard();
|
|
||||||
GlobalFree(hMem);
|
|
||||||
return result;
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef ESPANSO_BRIDGE_H
|
|
||||||
#define ESPANSO_BRIDGE_H
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
// SYSTEM
|
|
||||||
|
|
||||||
extern "C" int32_t start_daemon_process();
|
|
||||||
|
|
||||||
extern void * manager_instance;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Initialize the Windows parameters
|
|
||||||
* return: 1 if OK, -1 otherwise.
|
|
||||||
*/
|
|
||||||
extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t show_icon);
|
|
||||||
|
|
||||||
#define LEFT_VARIANT 1
|
|
||||||
#define RIGHT_VARIANT 2
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when a new keypress is made, the first argument is an int array,
|
|
||||||
* while the second is the size of the array.
|
|
||||||
*/
|
|
||||||
typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t event_type, int32_t key_code, int32_t variant, int32_t is_key_down);
|
|
||||||
extern KeypressCallback keypress_callback;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Register the callback that will be called when a keypress was made
|
|
||||||
*/
|
|
||||||
extern "C" void register_keypress_callback(KeypressCallback callback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Start the event loop indefinitely. Blocking call.
|
|
||||||
*/
|
|
||||||
extern "C" void eventloop();
|
|
||||||
|
|
||||||
// Keyboard Manager
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Type the given string by simulating Key Presses
|
|
||||||
*/
|
|
||||||
extern "C" void send_string(const wchar_t * string);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the given Virtual Key press
|
|
||||||
*/
|
|
||||||
extern "C" void send_vkey(int32_t vk);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the given Virtual Key press multiple times
|
|
||||||
*/
|
|
||||||
extern "C" void send_multi_vkey(int32_t vk, int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the given Virtual Key press multiple times adding a delay between each keypress
|
|
||||||
*/
|
|
||||||
extern "C" void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the backspace keypress, *count* times.
|
|
||||||
*/
|
|
||||||
extern "C" void delete_string(int32_t count, int32_t delay);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the Paste keyboard shortcut (CTRL+V)
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the Paste keyboard shortcut (CTRL+SHIFT+V)
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_shift_paste();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send the copy keyboard shortcut (CTRL+C)
|
|
||||||
*/
|
|
||||||
extern "C" void trigger_copy();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed
|
|
||||||
*/
|
|
||||||
extern "C" int32_t are_modifiers_pressed();
|
|
||||||
|
|
||||||
// Detect current application commands
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active windows's title
|
|
||||||
*/
|
|
||||||
extern "C" int32_t get_active_window_name(wchar_t * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the active windows's executable path
|
|
||||||
*/
|
|
||||||
extern "C" int32_t get_active_window_executable(wchar_t * buffer, int32_t size);
|
|
||||||
|
|
||||||
// UI
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when the tray icon is clicked
|
|
||||||
*/
|
|
||||||
typedef void (*IconClickCallback)(void * self);
|
|
||||||
extern IconClickCallback icon_click_callback;
|
|
||||||
extern "C" void register_icon_click_callback(IconClickCallback callback);
|
|
||||||
|
|
||||||
// CONTEXT MENU
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
int32_t id;
|
|
||||||
int32_t type;
|
|
||||||
wchar_t name[100];
|
|
||||||
} MenuItem;
|
|
||||||
|
|
||||||
extern "C" int32_t show_context_menu(MenuItem * items, int32_t count);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when the context menu is clicked
|
|
||||||
*/
|
|
||||||
typedef void (*ContextMenuClickCallback)(void * self, int32_t id);
|
|
||||||
extern ContextMenuClickCallback context_menu_click_callback;
|
|
||||||
extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Hide the tray icon
|
|
||||||
*/
|
|
||||||
extern "C" void cleanup_ui();
|
|
||||||
|
|
||||||
// NOTIFICATION
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Show a window containing the notification.
|
|
||||||
*/
|
|
||||||
extern "C" int32_t show_notification(wchar_t * message);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Close the notification if present
|
|
||||||
*/
|
|
||||||
extern "C" void close_notification();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update the tray icon status
|
|
||||||
*/
|
|
||||||
extern "C" void update_tray_icon(int32_t enabled);
|
|
||||||
|
|
||||||
// CLIPBOARD
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the clipboard text
|
|
||||||
*/
|
|
||||||
extern "C" int32_t get_clipboard(wchar_t * buffer, int32_t size);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the clipboard text
|
|
||||||
*/
|
|
||||||
extern "C" int32_t set_clipboard(wchar_t * text);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the clipboard image to the given path
|
|
||||||
*/
|
|
||||||
extern "C" int32_t set_clipboard_image(wchar_t * path);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set clipboard HTML. Notice how in this case, text is not a wide char but instead
|
|
||||||
* uses the UTF8 encoding.
|
|
||||||
* Also set the text fallback, in case some applications don't support HTML clipboard.
|
|
||||||
*/
|
|
||||||
extern "C" int32_t set_clipboard_html(char * html, wchar_t * text_fallback);
|
|
||||||
|
|
||||||
// PROCESSES
|
|
||||||
|
|
||||||
extern "C" int32_t start_process(wchar_t * cmd);
|
|
||||||
|
|
||||||
#endif //ESPANSO_BRIDGE_H
|
|
|
@ -1,312 +0,0 @@
|
||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 50;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
B6F9DF16232283F8005233EB /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = B6F9DF15232283F8005233EB /* AppDelegate.m */; };
|
|
||||||
B6F9DF18232283F8005233EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B6F9DF17232283F8005233EB /* Assets.xcassets */; };
|
|
||||||
B6F9DF1E232283F8005233EB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = B6F9DF1D232283F8005233EB /* main.m */; };
|
|
||||||
/* End PBXBuildFile section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EspansoNotifyHelper.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
B6F9DF14232283F8005233EB /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
|
||||||
B6F9DF15232283F8005233EB /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
|
||||||
B6F9DF17232283F8005233EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
||||||
B6F9DF1C232283F8005233EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
B6F9DF1D232283F8005233EB /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
|
||||||
B6F9DF1F232283F8005233EB /* EspansoNotifyHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EspansoNotifyHelper.entitlements; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
B6F9DF0E232283F8005233EB /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
B6F9DF08232283F8005233EB = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B6F9DF13232283F8005233EB /* EspansoNotifyHelper */,
|
|
||||||
B6F9DF12232283F8005233EB /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B6F9DF12232283F8005233EB /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B6F9DF13232283F8005233EB /* EspansoNotifyHelper */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B6F9DF14232283F8005233EB /* AppDelegate.h */,
|
|
||||||
B6F9DF15232283F8005233EB /* AppDelegate.m */,
|
|
||||||
B6F9DF17232283F8005233EB /* Assets.xcassets */,
|
|
||||||
B6F9DF1C232283F8005233EB /* Info.plist */,
|
|
||||||
B6F9DF1D232283F8005233EB /* main.m */,
|
|
||||||
B6F9DF1F232283F8005233EB /* EspansoNotifyHelper.entitlements */,
|
|
||||||
);
|
|
||||||
path = EspansoNotifyHelper;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
B6F9DF10232283F8005233EB /* EspansoNotifyHelper */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = B6F9DF22232283F8005233EB /* Build configuration list for PBXNativeTarget "EspansoNotifyHelper" */;
|
|
||||||
buildPhases = (
|
|
||||||
B6F9DF0D232283F8005233EB /* Sources */,
|
|
||||||
B6F9DF0E232283F8005233EB /* Frameworks */,
|
|
||||||
B6F9DF0F232283F8005233EB /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
name = EspansoNotifyHelper;
|
|
||||||
productName = EspansoNotifyHelper;
|
|
||||||
productReference = B6F9DF11232283F8005233EB /* EspansoNotifyHelper.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
B6F9DF09232283F8005233EB /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
LastUpgradeCheck = 1010;
|
|
||||||
ORGANIZATIONNAME = "Federico Terzi";
|
|
||||||
TargetAttributes = {
|
|
||||||
B6F9DF10232283F8005233EB = {
|
|
||||||
CreatedOnToolsVersion = 10.1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = B6F9DF0C232283F8005233EB /* Build configuration list for PBXProject "EspansoNotifyHelper" */;
|
|
||||||
compatibilityVersion = "Xcode 9.3";
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = B6F9DF08232283F8005233EB;
|
|
||||||
productRefGroup = B6F9DF12232283F8005233EB /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
B6F9DF10232283F8005233EB /* EspansoNotifyHelper */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
B6F9DF0F232283F8005233EB /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
B6F9DF18232283F8005233EB /* Assets.xcassets in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
B6F9DF0D232283F8005233EB /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
B6F9DF1E232283F8005233EB /* main.m in Sources */,
|
|
||||||
B6F9DF16232283F8005233EB /* AppDelegate.m in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
B6F9DF20232283F8005233EB /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = macosx;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
B6F9DF21232283F8005233EB /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = macosx;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
B6F9DF23232283F8005233EB /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = EspansoNotifyHelper/EspansoNotifyHelper.entitlements;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
DEVELOPMENT_TEAM = N69XJWRM3X;
|
|
||||||
INFOPLIST_FILE = EspansoNotifyHelper/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.federicoterzi.EspansoNotifyHelper;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
B6F9DF24232283F8005233EB /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = EspansoNotifyHelper/EspansoNotifyHelper.entitlements;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
DEVELOPMENT_TEAM = N69XJWRM3X;
|
|
||||||
INFOPLIST_FILE = EspansoNotifyHelper/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.federicoterzi.EspansoNotifyHelper;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
B6F9DF0C232283F8005233EB /* Build configuration list for PBXProject "EspansoNotifyHelper" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
B6F9DF20232283F8005233EB /* Debug */,
|
|
||||||
B6F9DF21232283F8005233EB /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
B6F9DF22232283F8005233EB /* Build configuration list for PBXNativeTarget "EspansoNotifyHelper" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
B6F9DF23232283F8005233EB /* Debug */,
|
|
||||||
B6F9DF24232283F8005233EB /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
};
|
|
||||||
rootObject = B6F9DF09232283F8005233EB /* Project object */;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:EspansoNotifyHelper.xcodeproj">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>IDEDidComputeMac32BitWarning</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,91 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1010"
|
|
||||||
version = "1.3">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B6F9DF10232283F8005233EB"
|
|
||||||
BuildableName = "EspansoNotifyHelper.app"
|
|
||||||
BlueprintName = "EspansoNotifyHelper"
|
|
||||||
ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
||||||
<Testables>
|
|
||||||
</Testables>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B6F9DF10232283F8005233EB"
|
|
||||||
BuildableName = "EspansoNotifyHelper.app"
|
|
||||||
BlueprintName = "EspansoNotifyHelper"
|
|
||||||
ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B6F9DF10232283F8005233EB"
|
|
||||||
BuildableName = "EspansoNotifyHelper.app"
|
|
||||||
BlueprintName = "EspansoNotifyHelper"
|
|
||||||
ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B6F9DF10232283F8005233EB"
|
|
||||||
BuildableName = "EspansoNotifyHelper.app"
|
|
||||||
BlueprintName = "EspansoNotifyHelper"
|
|
||||||
ReferencedContainer = "container:EspansoNotifyHelper.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>EspansoNotifyHelper.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>SuppressBuildableAutocreation</key>
|
|
||||||
<dict>
|
|
||||||
<key>B6F9DF10232283F8005233EB</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
|
||||||
@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate>
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "AppDelegate.h"
|
|
||||||
|
|
||||||
@interface AppDelegate ()
|
|
||||||
|
|
||||||
@property (weak) IBOutlet NSWindow *window;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation AppDelegate
|
|
||||||
|
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
||||||
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
|
|
||||||
|
|
||||||
NSArray *args = [[NSProcessInfo processInfo] arguments];
|
|
||||||
|
|
||||||
NSString *title = @"Title";
|
|
||||||
NSString *desc = @"Description";
|
|
||||||
double delay = 1.5;
|
|
||||||
|
|
||||||
if ([args count] > 3) {
|
|
||||||
title = args[1];
|
|
||||||
desc = args[2];
|
|
||||||
delay = [args[3] doubleValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSUserNotification *notification = [[NSUserNotification alloc] init];
|
|
||||||
notification.title = title;
|
|
||||||
notification.informativeText = desc;
|
|
||||||
notification.soundName = nil;
|
|
||||||
|
|
||||||
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
|
|
||||||
|
|
||||||
[[NSUserNotificationCenter defaultUserNotificationCenter] performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:delay];
|
|
||||||
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
|
||||||
NSRunningApplication *app = [NSRunningApplication currentApplication];
|
|
||||||
[app terminate];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
|
||||||
// Insert code here to tear down your application
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification{
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
|
@ -1,68 +0,0 @@
|
||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "16x16",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-16.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "16x16",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-32.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "32x32",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-32.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "32x32",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-64.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "128x128",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-128.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "128x128",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-256.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "256x256",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-256.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "256x256",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-512.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "512x512",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-512.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "512x512",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "icongreen-1024.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 4.4 KiB |
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>LSApplicationCategoryType</key>
|
|
||||||
<string>public.app-category.utilities</string>
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>Copyright © 2019 Federico Terzi. All rights reserved.</string>
|
|
||||||
<key>NSMainNibFile</key>
|
|
||||||
<string>MainMenu</string>
|
|
||||||
<key>LSUIElement</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "AppDelegate.h"
|
|
||||||
|
|
||||||
int main(int argc, const char * argv[]) {
|
|
||||||
AppDelegate *delegate = [[AppDelegate alloc] init];
|
|
||||||
NSApplication * application = [NSApplication sharedApplication];
|
|
||||||
[application setDelegate:delegate];
|
|
||||||
[NSApp run];
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
1
rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
tab_spaces = 2
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::os::raw::{c_char, c_void};
|
|
||||||
|
|
||||||
#[allow(improper_ctypes)]
|
|
||||||
#[link(name = "linuxbridge", kind = "static")]
|
|
||||||
extern "C" {
|
|
||||||
pub fn check_x11() -> i32;
|
|
||||||
pub fn initialize(s: *const c_void) -> i32;
|
|
||||||
pub fn eventloop();
|
|
||||||
pub fn cleanup();
|
|
||||||
|
|
||||||
// System
|
|
||||||
pub fn get_active_window_name(buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
pub fn get_active_window_executable(buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
pub fn is_current_window_special() -> i32;
|
|
||||||
pub fn register_error_callback(
|
|
||||||
cb: extern "C" fn(
|
|
||||||
_self: *mut c_void,
|
|
||||||
error_code: c_char,
|
|
||||||
request_code: c_char,
|
|
||||||
minor_code: c_char,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keyboard
|
|
||||||
pub fn register_keypress_callback(
|
|
||||||
cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32),
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn send_string(string: *const c_char);
|
|
||||||
pub fn delete_string(count: i32);
|
|
||||||
pub fn left_arrow(count: i32);
|
|
||||||
pub fn send_enter();
|
|
||||||
pub fn trigger_paste();
|
|
||||||
pub fn trigger_terminal_paste();
|
|
||||||
pub fn trigger_shift_ins_paste();
|
|
||||||
pub fn trigger_alt_shift_ins_paste();
|
|
||||||
pub fn trigger_ctrl_alt_paste();
|
|
||||||
pub fn trigger_copy();
|
|
||||||
|
|
||||||
pub fn fast_send_string(string: *const c_char, delay: i32);
|
|
||||||
pub fn fast_delete_string(count: i32, delay: i32);
|
|
||||||
pub fn fast_left_arrow(count: i32);
|
|
||||||
pub fn fast_send_enter();
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::os::raw::{c_char, c_void};
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct MacMenuItem {
|
|
||||||
pub item_id: i32,
|
|
||||||
pub item_type: i32,
|
|
||||||
pub item_name: [c_char; 100],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(improper_ctypes)]
|
|
||||||
#[link(name = "macbridge", kind = "static")]
|
|
||||||
extern "C" {
|
|
||||||
pub fn initialize(
|
|
||||||
s: *const c_void,
|
|
||||||
icon_path: *const c_char,
|
|
||||||
disabled_icon_path: *const c_char,
|
|
||||||
show_icon: i32,
|
|
||||||
);
|
|
||||||
pub fn eventloop();
|
|
||||||
pub fn headless_eventloop();
|
|
||||||
|
|
||||||
// System
|
|
||||||
pub fn check_accessibility() -> i32;
|
|
||||||
pub fn prompt_accessibility() -> i32;
|
|
||||||
pub fn open_settings_panel();
|
|
||||||
pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
pub fn get_secure_input_process(pid: *mut i64) -> i32;
|
|
||||||
pub fn get_path_from_pid(pid: i64, buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
|
|
||||||
// Clipboard
|
|
||||||
pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;
|
|
||||||
pub fn set_clipboard(text: *const c_char) -> i32;
|
|
||||||
pub fn set_clipboard_image(path: *const c_char) -> i32;
|
|
||||||
pub fn set_clipboard_html(html: *const c_char, text_fallback: *const c_char) -> i32;
|
|
||||||
|
|
||||||
// UI
|
|
||||||
pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
|
|
||||||
pub fn show_context_menu(items: *const MacMenuItem, count: i32) -> i32;
|
|
||||||
pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
|
|
||||||
pub fn update_tray_icon(enabled: i32);
|
|
||||||
|
|
||||||
// Keyboard
|
|
||||||
pub fn register_keypress_callback(
|
|
||||||
cb: extern "C" fn(_self: *mut c_void, *const u8, i32, i32, i32),
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn send_string(string: *const c_char);
|
|
||||||
pub fn send_vkey(vk: i32);
|
|
||||||
pub fn send_multi_vkey(vk: i32, count: i32);
|
|
||||||
pub fn delete_string(count: i32);
|
|
||||||
pub fn trigger_paste();
|
|
||||||
pub fn trigger_copy();
|
|
||||||
pub fn are_modifiers_pressed() -> i32;
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::os::raw::{c_char, c_void};
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct WindowsMenuItem {
|
|
||||||
pub item_id: i32,
|
|
||||||
pub item_type: i32,
|
|
||||||
pub item_name: [u16; 100],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(improper_ctypes)]
|
|
||||||
#[link(name = "winbridge", kind = "static")]
|
|
||||||
extern "C" {
|
|
||||||
pub fn start_daemon_process() -> i32;
|
|
||||||
pub fn initialize(
|
|
||||||
s: *const c_void,
|
|
||||||
ico_path: *const u16,
|
|
||||||
red_ico_path: *const u16,
|
|
||||||
bmp_path: *const u16,
|
|
||||||
show_icon: i32,
|
|
||||||
) -> i32;
|
|
||||||
|
|
||||||
// SYSTEM
|
|
||||||
pub fn get_active_window_name(buffer: *mut u16, size: i32) -> i32;
|
|
||||||
pub fn get_active_window_executable(buffer: *mut u16, size: i32) -> i32;
|
|
||||||
|
|
||||||
// UI
|
|
||||||
pub fn show_notification(message: *const u16) -> i32;
|
|
||||||
pub fn close_notification();
|
|
||||||
pub fn show_context_menu(items: *const WindowsMenuItem, count: i32) -> i32;
|
|
||||||
pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
|
|
||||||
pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
|
|
||||||
pub fn cleanup_ui();
|
|
||||||
pub fn update_tray_icon(enabled: i32);
|
|
||||||
|
|
||||||
// CLIPBOARD
|
|
||||||
pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
|
|
||||||
pub fn set_clipboard(payload: *const u16) -> i32;
|
|
||||||
pub fn set_clipboard_image(path: *const u16) -> i32;
|
|
||||||
pub fn set_clipboard_html(html: *const c_char, text_fallback: *const u16) -> i32;
|
|
||||||
|
|
||||||
// KEYBOARD
|
|
||||||
pub fn register_keypress_callback(
|
|
||||||
cb: extern "C" fn(_self: *mut c_void, *const u16, i32, i32, i32, i32, i32),
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn eventloop();
|
|
||||||
pub fn send_string(string: *const u16);
|
|
||||||
pub fn send_vkey(vk: i32);
|
|
||||||
pub fn send_multi_vkey(vk: i32, count: i32);
|
|
||||||
pub fn delete_string(count: i32, delay: i32);
|
|
||||||
pub fn trigger_paste();
|
|
||||||
pub fn trigger_shift_paste();
|
|
||||||
pub fn trigger_copy();
|
|
||||||
pub fn are_modifiers_pressed() -> i32;
|
|
||||||
|
|
||||||
// PROCESSES
|
|
||||||
|
|
||||||
pub fn start_process(cmd: *const u16) -> i32;
|
|
||||||
}
|
|
71
src/check.rs
|
@ -1,71 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This functions are used to check if the required dependencies and conditions are satisfied
|
|
||||||
// before starting espanso
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn check_preconditions() -> bool {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let mut result = true;
|
|
||||||
|
|
||||||
// Make sure notify-send is installed
|
|
||||||
let status = Command::new("notify-send").arg("-v").output();
|
|
||||||
if status.is_err() {
|
|
||||||
println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it.");
|
|
||||||
result = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure xclip is installed
|
|
||||||
let status = Command::new("xclip").arg("-version").output();
|
|
||||||
if status.is_err() {
|
|
||||||
println!(
|
|
||||||
"Error: 'xclip' command is needed for espanso to work correctly, please install it."
|
|
||||||
);
|
|
||||||
result = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn check_preconditions() -> bool {
|
|
||||||
// Make sure no app is currently using secure input.
|
|
||||||
let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application();
|
|
||||||
|
|
||||||
if let Some((app_name, process)) = secure_input_app {
|
|
||||||
eprintln!("WARNING: An application is currently using SecureInput and might prevent espanso from working correctly.");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("APP: {}", app_name);
|
|
||||||
eprintln!("PROC: {}", process);
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Please close it or disable SecureInput for that application (most apps that use it have a");
|
|
||||||
eprintln!("setting to disable it).");
|
|
||||||
eprintln!("Until then, espanso might not work as expected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn check_preconditions() -> bool {
|
|
||||||
// Nothing needed on windows
|
|
||||||
true
|
|
||||||
}
|
|
87
src/cli.rs
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::config::ConfigSet;
|
|
||||||
use crate::matcher::{Match, MatchContentType};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
pub fn list_matches(config_set: ConfigSet, onlytriggers: bool, preserve_newlines: bool) {
|
|
||||||
let matches = filter_matches(config_set);
|
|
||||||
|
|
||||||
for m in matches {
|
|
||||||
for trigger in m.triggers.iter() {
|
|
||||||
if onlytriggers {
|
|
||||||
println!("{}", trigger);
|
|
||||||
} else {
|
|
||||||
match m.content {
|
|
||||||
MatchContentType::Text(ref text) => {
|
|
||||||
let replace = if preserve_newlines {
|
|
||||||
text.replace.to_owned()
|
|
||||||
} else {
|
|
||||||
text.replace.replace("\n", " ")
|
|
||||||
};
|
|
||||||
println!("{} - {}", trigger, replace)
|
|
||||||
}
|
|
||||||
MatchContentType::Image(_) => {
|
|
||||||
// Skip image matches for now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct JsonMatchEntry {
|
|
||||||
triggers: Vec<String>,
|
|
||||||
replace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_matches_as_json(config_set: ConfigSet) {
|
|
||||||
let matches = filter_matches(config_set);
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
|
|
||||||
for m in matches {
|
|
||||||
match m.content {
|
|
||||||
MatchContentType::Text(ref text) => entries.push(JsonMatchEntry {
|
|
||||||
triggers: m.triggers,
|
|
||||||
replace: text.replace.clone(),
|
|
||||||
}),
|
|
||||||
MatchContentType::Image(_) => {
|
|
||||||
// Skip image matches for now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = serde_json::to_string(&entries);
|
|
||||||
|
|
||||||
println!("{}", output.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filter_matches(config_set: ConfigSet) -> Vec<Match> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
output.extend(config_set.default.matches);
|
|
||||||
|
|
||||||
// TODO: consider specific matches by class, title or exe path
|
|
||||||
// for specific in config_set.specific {
|
|
||||||
// output.extend(specific.matches)
|
|
||||||
// }
|
|
||||||
output
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use log::error;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
pub struct LinuxClipboardManager {}
|
|
||||||
|
|
||||||
impl super::ClipboardManager for LinuxClipboardManager {
|
|
||||||
fn get_clipboard(&self) -> Option<String> {
|
|
||||||
let res = Command::new("xclip").args(&["-o", "-sel", "clip"]).output();
|
|
||||||
|
|
||||||
if let Ok(output) = res {
|
|
||||||
if output.status.success() {
|
|
||||||
let s = String::from_utf8_lossy(&output.stdout);
|
|
||||||
return Some((*s).to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard(&self, payload: &str) {
|
|
||||||
let res = Command::new("xclip")
|
|
||||||
.args(&["-sel", "clip"])
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.spawn();
|
|
||||||
|
|
||||||
if let Ok(mut child) = res {
|
|
||||||
let stdin = child.stdin.as_mut();
|
|
||||||
|
|
||||||
if let Some(output) = stdin {
|
|
||||||
let res = output.write_all(payload.as_bytes());
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
error!("Could not set clipboard: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = child.wait();
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
error!("Could not set clipboard: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard_image(&self, image_path: &Path) {
|
|
||||||
let extension = image_path.extension();
|
|
||||||
let mime = match extension {
|
|
||||||
Some(ext) => {
|
|
||||||
let ext = ext.to_string_lossy().to_lowercase();
|
|
||||||
match ext.as_ref() {
|
|
||||||
"png" => "image/png",
|
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
|
||||||
"gif" => "image/gif",
|
|
||||||
"svg" => "image/svg",
|
|
||||||
_ => "image/png",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => "image/png",
|
|
||||||
};
|
|
||||||
|
|
||||||
let image_path = image_path.to_string_lossy().into_owned();
|
|
||||||
|
|
||||||
let res = Command::new("xclip")
|
|
||||||
.args(&["-selection", "clipboard", "-t", mime, "-i", &image_path])
|
|
||||||
.spawn();
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
error!("Could not set image clipboard: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard_html(&self, html: &str) {
|
|
||||||
let res = Command::new("xclip")
|
|
||||||
.args(&["-sel", "clip", "-t", "text/html"])
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.spawn();
|
|
||||||
|
|
||||||
if let Ok(mut child) = res {
|
|
||||||
let stdin = child.stdin.as_mut();
|
|
||||||
|
|
||||||
if let Some(output) = stdin {
|
|
||||||
let res = output.write_all(html.as_bytes());
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
error!("Could not set clipboard html: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = child.wait();
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
error!("Could not set clipboard html: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinuxClipboardManager {
|
|
||||||
pub fn new() -> LinuxClipboardManager {
|
|
||||||
LinuxClipboardManager {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::bridge::macos::*;
|
|
||||||
use log::{error, warn};
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
use std::os::raw::c_char;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub struct MacClipboardManager {}
|
|
||||||
|
|
||||||
impl super::ClipboardManager for MacClipboardManager {
|
|
||||||
fn get_clipboard(&self) -> Option<String> {
|
|
||||||
unsafe {
|
|
||||||
let mut buffer: [c_char; 2000] = [0; 2000];
|
|
||||||
let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
|
||||||
|
|
||||||
if res > 0 {
|
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
|
||||||
|
|
||||||
let string = c_string.to_str();
|
|
||||||
if let Ok(string) = string {
|
|
||||||
return Some((*string).to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard(&self, payload: &str) {
|
|
||||||
let res = CString::new(payload);
|
|
||||||
if let Ok(cstr) = res {
|
|
||||||
unsafe {
|
|
||||||
set_clipboard(cstr.as_ptr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard_image(&self, image_path: &Path) {
|
|
||||||
let path_string = image_path.to_string_lossy().into_owned();
|
|
||||||
let res = CString::new(path_string);
|
|
||||||
if let Ok(path) = res {
|
|
||||||
unsafe {
|
|
||||||
let result = set_clipboard_image(path.as_ptr());
|
|
||||||
if result != 1 {
|
|
||||||
warn!("Couldn't set clipboard for image: {:?}", image_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard_html(&self, html: &str) {
|
|
||||||
// Render the text fallback for those applications that don't support HTML clipboard
|
|
||||||
let decorator = html2text::render::text_renderer::TrivialDecorator::new();
|
|
||||||
let text_fallback =
|
|
||||||
html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator);
|
|
||||||
unsafe {
|
|
||||||
let payload_c = CString::new(html).expect("unable to create CString for html content");
|
|
||||||
let payload_fallback_c = CString::new(text_fallback).unwrap();
|
|
||||||
set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MacClipboardManager {
|
|
||||||
pub fn new() -> MacClipboardManager {
|
|
||||||
MacClipboardManager {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
mod windows;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
mod linux;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
mod macos;
|
|
||||||
|
|
||||||
pub trait ClipboardManager {
|
|
||||||
fn get_clipboard(&self) -> Option<String>;
|
|
||||||
fn set_clipboard(&self, payload: &str);
|
|
||||||
fn set_clipboard_image(&self, image_path: &Path);
|
|
||||||
fn set_clipboard_html(&self, html: &str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// LINUX IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn get_manager() -> impl ClipboardManager {
|
|
||||||
linux::LinuxClipboardManager::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn get_manager() -> impl ClipboardManager {
|
|
||||||
windows::WindowsClipboardManager::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAC IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn get_manager() -> impl ClipboardManager {
|
|
||||||
macos::MacClipboardManager::new()
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::bridge::windows::{
|
|
||||||
get_clipboard, set_clipboard, set_clipboard_html, set_clipboard_image,
|
|
||||||
};
|
|
||||||
use std::{ffi::CString, path::Path};
|
|
||||||
use widestring::U16CString;
|
|
||||||
|
|
||||||
pub struct WindowsClipboardManager {}
|
|
||||||
|
|
||||||
impl WindowsClipboardManager {
|
|
||||||
pub fn new() -> WindowsClipboardManager {
|
|
||||||
WindowsClipboardManager {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::ClipboardManager for WindowsClipboardManager {
|
|
||||||
fn get_clipboard(&self) -> Option<String> {
|
|
||||||
unsafe {
|
|
||||||
let mut buffer: [u16; 2000] = [0; 2000];
|
|
||||||
let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
|
||||||
|
|
||||||
if res > 0 {
|
|
||||||
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
|
||||||
|
|
||||||
let string = c_string.to_string_lossy();
|
|
||||||
return Some((*string).to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard(&self, payload: &str) {
|
|
||||||
unsafe {
|
|
||||||
let payload_c = U16CString::from_str(payload).unwrap();
|
|
||||||
set_clipboard(payload_c.as_ptr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard_image(&self, image_path: &Path) {
|
|
||||||
let path_string = image_path.to_string_lossy().into_owned();
|
|
||||||
unsafe {
|
|
||||||
let payload_c = U16CString::from_str(path_string).unwrap();
|
|
||||||
set_clipboard_image(payload_c.as_ptr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_clipboard_html(&self, html: &str) {
|
|
||||||
// In order to set the HTML clipboard, we have to create a prefix with a specific format
|
|
||||||
// For more information, look here:
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
|
|
||||||
// https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
|
|
||||||
let mut tokens = Vec::new();
|
|
||||||
tokens.push("Version:0.9");
|
|
||||||
tokens.push("StartHTML:<<STR*#>");
|
|
||||||
tokens.push("EndHTML:<<END*#>");
|
|
||||||
tokens.push("StartFragment:<<SFG#*>");
|
|
||||||
tokens.push("EndFragment:<<EFG#*>");
|
|
||||||
tokens.push("<html>");
|
|
||||||
tokens.push("<body>");
|
|
||||||
let content = format!("<!--StartFragment-->{}<!--EndFragment-->", html);
|
|
||||||
tokens.push(&content);
|
|
||||||
tokens.push("</body>");
|
|
||||||
tokens.push("</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 the text fallback for those applications that don't support HTML clipboard
|
|
||||||
let decorator = html2text::render::text_renderer::TrivialDecorator::new();
|
|
||||||
let text_fallback =
|
|
||||||
html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator);
|
|
||||||
unsafe {
|
|
||||||
let payload_c =
|
|
||||||
CString::new(render).expect("unable to create CString for html content");
|
|
||||||
let payload_fallback_c = U16CString::from_str(text_fallback).unwrap();
|
|
||||||
set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
1937
src/config/mod.rs
|
@ -1,555 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::{ConfigSet, Configs};
|
|
||||||
use crate::matcher::Match;
|
|
||||||
use crate::system::SystemManager;
|
|
||||||
use log::{debug, warn};
|
|
||||||
use regex::Regex;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
pub struct RuntimeConfigManager<'a, S: SystemManager> {
|
|
||||||
set: ConfigSet,
|
|
||||||
|
|
||||||
// Filter regexps
|
|
||||||
title_regexps: Vec<Option<Regex>>,
|
|
||||||
class_regexps: Vec<Option<Regex>>,
|
|
||||||
exec_regexps: Vec<Option<Regex>>,
|
|
||||||
|
|
||||||
system_manager: S,
|
|
||||||
|
|
||||||
// Cache
|
|
||||||
last_config_update: RefCell<SystemTime>,
|
|
||||||
last_config: RefCell<Option<&'a Configs>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, S: SystemManager> RuntimeConfigManager<'a, S> {
|
|
||||||
pub fn new<'b>(set: ConfigSet, system_manager: S) -> RuntimeConfigManager<'b, S> {
|
|
||||||
// Compile all the regexps
|
|
||||||
let title_regexps = set.specific.iter().map(
|
|
||||||
|config| {
|
|
||||||
if config.filter_title.is_empty() {
|
|
||||||
None
|
|
||||||
}else{
|
|
||||||
let res = Regex::new(&config.filter_title);
|
|
||||||
if let Ok(regex) = res {
|
|
||||||
Some(regex)
|
|
||||||
}else{
|
|
||||||
warn!("Invalid regex in 'filter_title' field of configuration {}, ignoring it...", config.name);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).collect();
|
|
||||||
|
|
||||||
let class_regexps = set.specific.iter().map(
|
|
||||||
|config| {
|
|
||||||
if config.filter_class.is_empty() {
|
|
||||||
None
|
|
||||||
}else{
|
|
||||||
let res = Regex::new(&config.filter_class);
|
|
||||||
if let Ok(regex) = res {
|
|
||||||
Some(regex)
|
|
||||||
}else{
|
|
||||||
warn!("Invalid regex in 'filter_class' field of configuration {}, ignoring it...", config.name);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).collect();
|
|
||||||
|
|
||||||
let exec_regexps = set.specific.iter().map(
|
|
||||||
|config| {
|
|
||||||
if config.filter_exec.is_empty() {
|
|
||||||
None
|
|
||||||
}else{
|
|
||||||
let res = Regex::new(&config.filter_exec);
|
|
||||||
if let Ok(regex) = res {
|
|
||||||
Some(regex)
|
|
||||||
}else{
|
|
||||||
warn!("Invalid regex in 'filter_exec' field of configuration {}, ignoring it...", config.name);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).collect();
|
|
||||||
|
|
||||||
let last_config_update = RefCell::new(SystemTime::now());
|
|
||||||
let last_config = RefCell::new(None);
|
|
||||||
|
|
||||||
RuntimeConfigManager {
|
|
||||||
set,
|
|
||||||
title_regexps,
|
|
||||||
class_regexps,
|
|
||||||
exec_regexps,
|
|
||||||
system_manager,
|
|
||||||
last_config_update,
|
|
||||||
last_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_active_config(&'a self) -> &'a Configs {
|
|
||||||
// TODO: optimize performance by avoiding some of these checks if no Configs use the filters
|
|
||||||
|
|
||||||
debug!("Requested config for window:");
|
|
||||||
|
|
||||||
let active_title = self.system_manager.get_current_window_title();
|
|
||||||
|
|
||||||
if let Some(title) = active_title {
|
|
||||||
debug!("=> Title: '{}'", title);
|
|
||||||
|
|
||||||
for (i, regex) in self.title_regexps.iter().enumerate() {
|
|
||||||
if let Some(regex) = regex {
|
|
||||||
if regex.is_match(&title) {
|
|
||||||
debug!(
|
|
||||||
"Matched 'filter_title' for '{}' config, using custom settings.",
|
|
||||||
self.set.specific[i].name
|
|
||||||
);
|
|
||||||
|
|
||||||
return &self.set.specific[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let active_executable = self.system_manager.get_current_window_executable();
|
|
||||||
|
|
||||||
if let Some(executable) = active_executable {
|
|
||||||
debug!("=> Executable: '{}'", executable);
|
|
||||||
|
|
||||||
for (i, regex) in self.exec_regexps.iter().enumerate() {
|
|
||||||
if let Some(regex) = regex {
|
|
||||||
if regex.is_match(&executable) {
|
|
||||||
debug!(
|
|
||||||
"Matched 'filter_exec' for '{}' config, using custom settings.",
|
|
||||||
self.set.specific[i].name
|
|
||||||
);
|
|
||||||
|
|
||||||
return &self.set.specific[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let active_class = self.system_manager.get_current_window_class();
|
|
||||||
|
|
||||||
if let Some(class) = active_class {
|
|
||||||
debug!("=> Class: '{}'", class);
|
|
||||||
|
|
||||||
for (i, regex) in self.class_regexps.iter().enumerate() {
|
|
||||||
if let Some(regex) = regex {
|
|
||||||
if regex.is_match(&class) {
|
|
||||||
debug!(
|
|
||||||
"Matched 'filter_class' for '{}' config, using custom settings.",
|
|
||||||
self.set.specific[i].name
|
|
||||||
);
|
|
||||||
|
|
||||||
return &self.set.specific[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No matches, return the default mapping
|
|
||||||
debug!("No matches for custom configs, using default settings.");
|
|
||||||
&self.set.default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a, S> {
|
|
||||||
fn active_config(&'a self) -> &'a Configs {
|
|
||||||
let mut last_config_update = self.last_config_update.borrow_mut();
|
|
||||||
if let Ok(elapsed) = (*last_config_update).elapsed() {
|
|
||||||
*last_config_update = SystemTime::now();
|
|
||||||
|
|
||||||
if elapsed.as_millis() < self.set.default.config_caching_interval as u128 {
|
|
||||||
let last_config = self.last_config.borrow();
|
|
||||||
if let Some(cached_config) = *last_config {
|
|
||||||
debug!("Using cached config");
|
|
||||||
return cached_config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = self.calculate_active_config();
|
|
||||||
|
|
||||||
let mut last_config = self.last_config.borrow_mut();
|
|
||||||
*last_config = Some(config);
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_config(&'a self) -> &'a Configs {
|
|
||||||
&self.set.default
|
|
||||||
}
|
|
||||||
|
|
||||||
fn matches(&'a self) -> &'a Vec<Match> {
|
|
||||||
&self.active_config().matches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTS
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::config::tests::{create_temp_espanso_directories, create_user_config_file};
|
|
||||||
use crate::config::ConfigManager;
|
|
||||||
|
|
||||||
struct DummySystemManager {
|
|
||||||
title: RefCell<String>,
|
|
||||||
class: RefCell<String>,
|
|
||||||
exec: RefCell<String>,
|
|
||||||
}
|
|
||||||
impl SystemManager for DummySystemManager {
|
|
||||||
fn get_current_window_title(&self) -> Option<String> {
|
|
||||||
Some(self.title.borrow().clone())
|
|
||||||
}
|
|
||||||
fn get_current_window_class(&self) -> Option<String> {
|
|
||||||
Some(self.class.borrow().clone())
|
|
||||||
}
|
|
||||||
fn get_current_window_executable(&self) -> Option<String> {
|
|
||||||
Some(self.exec.borrow().clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl DummySystemManager {
|
|
||||||
pub fn new_custom(title: &str, class: &str, exec: &str) -> DummySystemManager {
|
|
||||||
DummySystemManager {
|
|
||||||
title: RefCell::new(title.to_owned()),
|
|
||||||
class: RefCell::new(class.to_owned()),
|
|
||||||
exec: RefCell::new(exec.to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> DummySystemManager {
|
|
||||||
DummySystemManager::new_custom("title", "class", "exec")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn change(&self, title: &str, class: &str, exec: &str) {
|
|
||||||
*self.title.borrow_mut() = title.to_owned();
|
|
||||||
*self.class.borrow_mut() = class.to_owned();
|
|
||||||
*self.exec.borrow_mut() = exec.to_owned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_constructor_regex_load_correctly() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: myname1
|
|
||||||
filter_exec: "Title"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific2.yml",
|
|
||||||
r###"
|
|
||||||
name: myname2
|
|
||||||
filter_title: "Yeah"
|
|
||||||
filter_class: "Car"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific3.yml",
|
|
||||||
r###"
|
|
||||||
name: myname3
|
|
||||||
filter_title: "Nice"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager = DummySystemManager::new();
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
let sp1index = config_manager
|
|
||||||
.set
|
|
||||||
.specific
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.name == "myname1")
|
|
||||||
.unwrap();
|
|
||||||
let sp2index = config_manager
|
|
||||||
.set
|
|
||||||
.specific
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.name == "myname2")
|
|
||||||
.unwrap();
|
|
||||||
let sp3index = config_manager
|
|
||||||
.set
|
|
||||||
.specific
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.name == "myname3")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(config_manager.exec_regexps.len(), 3);
|
|
||||||
assert_eq!(config_manager.title_regexps.len(), 3);
|
|
||||||
assert_eq!(config_manager.class_regexps.len(), 3);
|
|
||||||
|
|
||||||
assert!(config_manager.class_regexps[sp1index].is_none());
|
|
||||||
assert!(config_manager.class_regexps[sp2index].is_some());
|
|
||||||
assert!(config_manager.class_regexps[sp3index].is_none());
|
|
||||||
|
|
||||||
assert!(config_manager.title_regexps[sp1index].is_none());
|
|
||||||
assert!(config_manager.title_regexps[sp2index].is_some());
|
|
||||||
assert!(config_manager.title_regexps[sp3index].is_some());
|
|
||||||
|
|
||||||
assert!(config_manager.exec_regexps[sp1index].is_some());
|
|
||||||
assert!(config_manager.exec_regexps[sp2index].is_none());
|
|
||||||
assert!(config_manager.exec_regexps[sp3index].is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_constructor_malformed_regexes_are_ignored() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: myname1
|
|
||||||
filter_exec: "[`-_]"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific2.yml",
|
|
||||||
r###"
|
|
||||||
name: myname2
|
|
||||||
filter_title: "[`-_]"
|
|
||||||
filter_class: "Car"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific3.yml",
|
|
||||||
r###"
|
|
||||||
name: myname3
|
|
||||||
filter_title: "Nice"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager = DummySystemManager::new();
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
let sp1index = config_manager
|
|
||||||
.set
|
|
||||||
.specific
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.name == "myname1")
|
|
||||||
.unwrap();
|
|
||||||
let sp2index = config_manager
|
|
||||||
.set
|
|
||||||
.specific
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.name == "myname2")
|
|
||||||
.unwrap();
|
|
||||||
let sp3index = config_manager
|
|
||||||
.set
|
|
||||||
.specific
|
|
||||||
.iter()
|
|
||||||
.position(|x| x.name == "myname3")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(config_manager.exec_regexps.len(), 3);
|
|
||||||
assert_eq!(config_manager.title_regexps.len(), 3);
|
|
||||||
assert_eq!(config_manager.class_regexps.len(), 3);
|
|
||||||
|
|
||||||
assert!(config_manager.class_regexps[sp1index].is_none());
|
|
||||||
assert!(config_manager.class_regexps[sp2index].is_some());
|
|
||||||
assert!(config_manager.class_regexps[sp3index].is_none());
|
|
||||||
|
|
||||||
assert!(config_manager.title_regexps[sp1index].is_none());
|
|
||||||
assert!(config_manager.title_regexps[sp2index].is_none());
|
|
||||||
assert!(config_manager.title_regexps[sp3index].is_some());
|
|
||||||
|
|
||||||
assert!(config_manager.exec_regexps[sp1index].is_none());
|
|
||||||
assert!(config_manager.exec_regexps[sp2index].is_none());
|
|
||||||
assert!(config_manager.exec_regexps[sp3index].is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_calculate_active_config_specific_title_match() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: chrome
|
|
||||||
filter_title: "Chrome"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager =
|
|
||||||
DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "chrome");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_calculate_active_config_specific_class_match() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: chrome
|
|
||||||
filter_class: "Chrome"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager =
|
|
||||||
DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "chrome");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_calculate_active_config_specific_exec_match() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: chrome
|
|
||||||
filter_exec: "chrome.exe"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager =
|
|
||||||
DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "chrome");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_calculate_active_config_specific_multi_filter_match() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: chrome
|
|
||||||
filter_class: Browser
|
|
||||||
filter_exec: "firefox.exe"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager =
|
|
||||||
DummySystemManager::new_custom("Google Chrome", "Browser", "C:\\Path\\chrome.exe");
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "chrome");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_calculate_active_config_no_match() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: firefox
|
|
||||||
filter_title: "Firefox"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager =
|
|
||||||
DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "default");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_runtime_active_config_cache() {
|
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories();
|
|
||||||
|
|
||||||
create_user_config_file(
|
|
||||||
&data_dir.path(),
|
|
||||||
"specific.yml",
|
|
||||||
r###"
|
|
||||||
name: firefox
|
|
||||||
filter_title: "Firefox"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path());
|
|
||||||
assert!(config_set.is_ok());
|
|
||||||
|
|
||||||
let dummy_system_manager =
|
|
||||||
DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe");
|
|
||||||
|
|
||||||
let config_manager = RuntimeConfigManager::new(config_set.unwrap(), dummy_system_manager);
|
|
||||||
|
|
||||||
assert_eq!(config_manager.active_config().name, "default");
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "default");
|
|
||||||
|
|
||||||
config_manager
|
|
||||||
.system_manager
|
|
||||||
.change("Firefox", "Browser", "C\\Path\\firefox.exe");
|
|
||||||
|
|
||||||
// Active config should have changed, but not cached one
|
|
||||||
assert_eq!(config_manager.calculate_active_config().name, "firefox");
|
|
||||||
assert_eq!(config_manager.active_config().name, "default");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::bridge::linux::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::event::KeyModifier::*;
|
|
||||||
use crate::event::*;
|
|
||||||
use log::{debug, error, warn};
|
|
||||||
use std::ffi::CStr;
|
|
||||||
use std::os::raw::{c_char, c_void};
|
|
||||||
use std::process::exit;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::atomic::Ordering::Acquire;
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct LinuxContext {
|
|
||||||
pub send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinuxContext {
|
|
||||||
pub fn new(
|
|
||||||
_: Configs,
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Box<LinuxContext> {
|
|
||||||
// Check if the X11 context is available
|
|
||||||
let x11_available = unsafe { check_x11() };
|
|
||||||
|
|
||||||
if x11_available < 0 {
|
|
||||||
error!("Error, can't connect to X11 context");
|
|
||||||
std::process::exit(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = Box::new(LinuxContext {
|
|
||||||
send_channel,
|
|
||||||
is_injecting,
|
|
||||||
});
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let context_ptr = &*context as *const LinuxContext as *const c_void;
|
|
||||||
|
|
||||||
register_keypress_callback(keypress_callback);
|
|
||||||
register_error_callback(error_callback);
|
|
||||||
|
|
||||||
let res = initialize(context_ptr);
|
|
||||||
if res <= 0 {
|
|
||||||
error!("Could not initialize linux context, error: {}", res);
|
|
||||||
exit(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Context for LinuxContext {
|
|
||||||
fn eventloop(&self) {
|
|
||||||
unsafe {
|
|
||||||
eventloop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for LinuxContext {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native bridge code
|
|
||||||
|
|
||||||
extern "C" fn keypress_callback(
|
|
||||||
_self: *mut c_void,
|
|
||||||
raw_buffer: *const u8,
|
|
||||||
_len: i32,
|
|
||||||
event_type: i32,
|
|
||||||
key_code: i32,
|
|
||||||
) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut LinuxContext;
|
|
||||||
|
|
||||||
// If espanso is currently injecting text, we should avoid processing
|
|
||||||
// external events, as it could happen that espanso reinterpret its
|
|
||||||
// own input.
|
|
||||||
if (*_self).is_injecting.load(Acquire) {
|
|
||||||
debug!("Input ignored while espanso is injecting text...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if event_type == 0 {
|
|
||||||
// Char event
|
|
||||||
// Convert the received buffer to a string
|
|
||||||
let c_str = CStr::from_ptr(raw_buffer as *const c_char);
|
|
||||||
let char_str = c_str.to_str();
|
|
||||||
|
|
||||||
// Send the char through the channel
|
|
||||||
match char_str {
|
|
||||||
Ok(char_str) => {
|
|
||||||
let event = Event::Key(KeyEvent::Char(char_str.to_owned()));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Unable to receive char: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if event_type == 1 {
|
|
||||||
// Modifier event
|
|
||||||
|
|
||||||
let modifier: Option<KeyModifier> = match key_code {
|
|
||||||
133 => Some(LEFT_META),
|
|
||||||
134 => Some(RIGHT_META),
|
|
||||||
50 => Some(LEFT_SHIFT),
|
|
||||||
62 => Some(RIGHT_SHIFT),
|
|
||||||
64 => Some(LEFT_ALT),
|
|
||||||
108 => Some(RIGHT_ALT),
|
|
||||||
37 => Some(LEFT_CTRL),
|
|
||||||
105 => Some(RIGHT_CTRL),
|
|
||||||
22 => Some(BACKSPACE),
|
|
||||||
66 => Some(CAPS_LOCK),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(modifier) = modifier {
|
|
||||||
let event = Event::Key(KeyEvent::Modifier(modifier));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
} else {
|
|
||||||
// Not one of the default modifiers, send an "other" event
|
|
||||||
let event = Event::Key(KeyEvent::Other);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other type of event
|
|
||||||
let event = Event::Key(KeyEvent::Other);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn error_callback(
|
|
||||||
_self: *mut c_void,
|
|
||||||
error_code: c_char,
|
|
||||||
request_code: c_char,
|
|
||||||
minor_code: c_char,
|
|
||||||
) {
|
|
||||||
warn!(
|
|
||||||
"X11 reported an error code: {}, request_code: {} and minor_code: {}",
|
|
||||||
error_code, request_code, minor_code
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,275 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::bridge::macos::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::event::KeyModifier::*;
|
|
||||||
use crate::event::{ActionType, Event, KeyEvent, KeyModifier, SystemEvent};
|
|
||||||
use crate::system::macos::MacSystemManager;
|
|
||||||
use log::{debug, error, info};
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
use std::os::raw::{c_char, c_void};
|
|
||||||
use std::process::exit;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::atomic::Ordering::Acquire;
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{fs, thread};
|
|
||||||
|
|
||||||
const STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icon.png");
|
|
||||||
const DISABLED_STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icondisabled.png");
|
|
||||||
|
|
||||||
pub struct MacContext {
|
|
||||||
pub send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
secure_input_watcher_enabled: bool,
|
|
||||||
secure_input_watcher_interval: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MacContext {
|
|
||||||
pub fn new(
|
|
||||||
config: Configs,
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Box<MacContext> {
|
|
||||||
// Check accessibility
|
|
||||||
unsafe {
|
|
||||||
let res = prompt_accessibility();
|
|
||||||
|
|
||||||
if res == 0 {
|
|
||||||
error!("Accessibility must be enabled to make espanso work on MacOS.");
|
|
||||||
error!(
|
|
||||||
"Please allow espanso in the Security & Privacy panel, then restart espanso."
|
|
||||||
);
|
|
||||||
error!("For more information: https://espanso.org/install/mac/");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = Box::new(MacContext {
|
|
||||||
send_channel,
|
|
||||||
is_injecting,
|
|
||||||
secure_input_watcher_enabled: config.secure_input_watcher_enabled,
|
|
||||||
secure_input_watcher_interval: config.secure_input_watcher_interval,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize the status icon path
|
|
||||||
let espanso_dir = super::get_data_dir();
|
|
||||||
let status_icon_target = espanso_dir.join("icon.png");
|
|
||||||
let disabled_status_icon_target = espanso_dir.join("icondisabled.png");
|
|
||||||
|
|
||||||
if status_icon_target.exists() {
|
|
||||||
info!("Status icon already initialized, skipping.");
|
|
||||||
} else {
|
|
||||||
fs::write(&status_icon_target, STATUS_ICON_BINARY).unwrap_or_else(|e| {
|
|
||||||
error!(
|
|
||||||
"Error copying the Status Icon to the espanso data directory: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if disabled_status_icon_target.exists() {
|
|
||||||
info!("Status icon (disabled) already initialized, skipping.");
|
|
||||||
} else {
|
|
||||||
fs::write(&disabled_status_icon_target, DISABLED_STATUS_ICON_BINARY).unwrap_or_else(
|
|
||||||
|e| {
|
|
||||||
error!(
|
|
||||||
"Error copying the Status Icon (disabled) to the espanso data directory: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let context_ptr = &*context as *const MacContext as *const c_void;
|
|
||||||
|
|
||||||
register_keypress_callback(keypress_callback);
|
|
||||||
register_icon_click_callback(icon_click_callback);
|
|
||||||
register_context_menu_click_callback(context_menu_click_callback);
|
|
||||||
|
|
||||||
let status_icon_path =
|
|
||||||
CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default();
|
|
||||||
let disabled_status_icon_path =
|
|
||||||
CString::new(disabled_status_icon_target.to_str().unwrap_or_default())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let show_icon = if config.show_icon { 1 } else { 0 };
|
|
||||||
|
|
||||||
initialize(
|
|
||||||
context_ptr,
|
|
||||||
status_icon_path.as_ptr(),
|
|
||||||
disabled_status_icon_path.as_ptr(),
|
|
||||||
show_icon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_secure_input_watcher(&self) {
|
|
||||||
let send_channel = self.send_channel.clone();
|
|
||||||
let secure_input_watcher_interval = self.secure_input_watcher_interval as u64;
|
|
||||||
|
|
||||||
let secure_input_watcher = thread::Builder::new().name("secure_input_watcher".to_string()).spawn(move || {
|
|
||||||
let mut last_secure_input_pid: Option<i64> = None;
|
|
||||||
loop {
|
|
||||||
let pid = MacSystemManager::get_secure_input_pid();
|
|
||||||
|
|
||||||
if let Some(pid) = pid { // Some application is currently on SecureInput
|
|
||||||
let should_notify = if let Some(old_pid) = last_secure_input_pid { // We already detected a SecureInput app
|
|
||||||
if old_pid != pid { // The old app is different from the current one, we should take action
|
|
||||||
true
|
|
||||||
}else{ // We already notified this application before
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}else{ // First time we see this SecureInput app, we should take action
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_notify {
|
|
||||||
let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application();
|
|
||||||
|
|
||||||
if let Some((app_name, path)) = secure_input_app {
|
|
||||||
let event = Event::System(SystemEvent::SecureInputEnabled(app_name, path));
|
|
||||||
send_channel.send(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last_secure_input_pid = Some(pid);
|
|
||||||
}else{ // No app is currently keeping SecureInput
|
|
||||||
if let Some(old_pid) = last_secure_input_pid { // If there was an app with SecureInput, notify that is now free
|
|
||||||
let event = Event::System(SystemEvent::SecureInputDisabled);
|
|
||||||
send_channel.send(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
last_secure_input_pid = None
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::sleep(std::time::Duration::from_millis(secure_input_watcher_interval));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_icon(enabled: bool) {
|
|
||||||
unsafe {
|
|
||||||
crate::bridge::macos::update_tray_icon(if enabled { 1 } else { 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Context for MacContext {
|
|
||||||
fn eventloop(&self) {
|
|
||||||
// Start the SecureInput watcher thread
|
|
||||||
if self.secure_input_watcher_enabled {
|
|
||||||
self.start_secure_input_watcher();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
eventloop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native bridge code
|
|
||||||
|
|
||||||
extern "C" fn keypress_callback(
|
|
||||||
_self: *mut c_void,
|
|
||||||
raw_buffer: *const u8,
|
|
||||||
len: i32,
|
|
||||||
event_type: i32,
|
|
||||||
key_code: i32,
|
|
||||||
) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut MacContext;
|
|
||||||
|
|
||||||
// If espanso is currently injecting text, we should avoid processing
|
|
||||||
// external events, as it could happen that espanso reinterpret its
|
|
||||||
// own input.
|
|
||||||
if (*_self).is_injecting.load(Acquire) {
|
|
||||||
debug!("Input ignored while espanso is injecting text...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if event_type == 0 {
|
|
||||||
// Char event
|
|
||||||
// Convert the received buffer to a string
|
|
||||||
let c_str = CStr::from_ptr(raw_buffer as (*const c_char));
|
|
||||||
let char_str = c_str.to_str();
|
|
||||||
|
|
||||||
// Send the char through the channel
|
|
||||||
match char_str {
|
|
||||||
Ok(char_str) => {
|
|
||||||
let event = Event::Key(KeyEvent::Char(char_str.to_owned()));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to receive char: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if event_type == 1 {
|
|
||||||
// Modifier event
|
|
||||||
let modifier: Option<KeyModifier> = match key_code {
|
|
||||||
0x37 => Some(LEFT_META),
|
|
||||||
0x36 => Some(RIGHT_META),
|
|
||||||
0x38 => Some(LEFT_SHIFT),
|
|
||||||
0x3C => Some(RIGHT_SHIFT),
|
|
||||||
0x3A => Some(LEFT_ALT),
|
|
||||||
0x3D => Some(RIGHT_ALT),
|
|
||||||
0x3B => Some(LEFT_CTRL),
|
|
||||||
0x3E => Some(RIGHT_CTRL),
|
|
||||||
0x33 => Some(BACKSPACE),
|
|
||||||
0x39 => Some(CAPS_LOCK),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(modifier) = modifier {
|
|
||||||
let event = Event::Key(KeyEvent::Modifier(modifier));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
} else {
|
|
||||||
// Not one of the default modifiers, send an "other" event
|
|
||||||
let event = Event::Key(KeyEvent::Other);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other type of event
|
|
||||||
let event = Event::Key(KeyEvent::Other);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn icon_click_callback(_self: *mut c_void) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut MacContext;
|
|
||||||
|
|
||||||
let event = Event::Action(ActionType::IconClick);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut MacContext;
|
|
||||||
|
|
||||||
let event = Event::Action(ActionType::from(id));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,173 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
mod windows;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
mod linux;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub(crate) mod macos;
|
|
||||||
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::event::Event;
|
|
||||||
use std::fs::create_dir_all;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::{Arc, Once};
|
|
||||||
|
|
||||||
pub trait Context {
|
|
||||||
fn eventloop(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAC IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn new(
|
|
||||||
config: Configs,
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Box<dyn Context> {
|
|
||||||
macos::MacContext::new(config, send_channel, is_injecting)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn update_icon(enabled: bool) {
|
|
||||||
macos::update_icon(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn get_icon_path() -> Option<PathBuf> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
// LINUX IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn new(
|
|
||||||
config: Configs,
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Box<dyn Context> {
|
|
||||||
linux::LinuxContext::new(config, send_channel, is_injecting)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn update_icon(enabled: bool) {
|
|
||||||
// No icon on Linux
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn get_icon_path() -> Option<PathBuf> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn new(
|
|
||||||
config: Configs,
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Box<dyn Context> {
|
|
||||||
windows::WindowsContext::new(config, send_channel, is_injecting)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn update_icon(enabled: bool) {
|
|
||||||
windows::update_icon(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn get_icon_path() -> Option<PathBuf> {
|
|
||||||
Some(windows::get_icon_path(&get_data_dir()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// espanso directories
|
|
||||||
|
|
||||||
static WARING_INIT: Once = Once::new();
|
|
||||||
|
|
||||||
pub fn get_data_dir() -> PathBuf {
|
|
||||||
let data_dir = dirs::data_local_dir().expect("Can't obtain data_local_dir(), terminating.");
|
|
||||||
let espanso_dir = data_dir.join("espanso");
|
|
||||||
create_dir_all(&espanso_dir).expect("Error creating espanso data directory");
|
|
||||||
espanso_dir
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_config_dir() -> PathBuf {
|
|
||||||
// Portable mode check
|
|
||||||
// Get the espanso executable path
|
|
||||||
let espanso_exe_path = std::env::current_exe().expect("Could not get espanso executable path");
|
|
||||||
let exe_dir = espanso_exe_path.parent();
|
|
||||||
if let Some(parent) = exe_dir {
|
|
||||||
let config_dir = parent.join(".espanso");
|
|
||||||
if config_dir.exists() {
|
|
||||||
println!(
|
|
||||||
"PORTABLE MODE, using config folder: '{}'",
|
|
||||||
config_dir.to_string_lossy()
|
|
||||||
);
|
|
||||||
return config_dir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For compatibility purposes, check if the $HOME/.espanso directory is available
|
|
||||||
let home_dir = dirs::home_dir().expect("Can't obtain the user home directory, terminating.");
|
|
||||||
let legacy_espanso_dir = home_dir.join(".espanso");
|
|
||||||
if legacy_espanso_dir.exists() {
|
|
||||||
// Avoid printing the warning multiple times with std::sync::Once
|
|
||||||
WARING_INIT.call_once(|| {
|
|
||||||
eprintln!("WARNING: using legacy espanso config location in $HOME/.espanso is DEPRECATED");
|
|
||||||
eprintln!("Starting from espanso v0.3.0, espanso config location is changed.");
|
|
||||||
eprintln!("Please check out the documentation to find out more: https://espanso.org/docs/configuration/");
|
|
||||||
eprintln!()
|
|
||||||
});
|
|
||||||
|
|
||||||
return legacy_espanso_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for $HOME/.config/espanso location
|
|
||||||
let home_config_dir = home_dir.join(".config");
|
|
||||||
let config_espanso_dir = home_config_dir.join("espanso");
|
|
||||||
if config_espanso_dir.exists() {
|
|
||||||
return config_espanso_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// New config location, from version v0.3.0
|
|
||||||
// Refer to issue #73 for more information: https://github.com/federico-terzi/espanso/issues/73
|
|
||||||
let config_dir = dirs::config_dir().expect("Can't obtain config_dir(), terminating.");
|
|
||||||
let espanso_dir = config_dir.join("espanso");
|
|
||||||
create_dir_all(&espanso_dir).expect("Error creating espanso config directory");
|
|
||||||
espanso_dir
|
|
||||||
}
|
|
||||||
|
|
||||||
const PACKAGES_FOLDER_NAME: &str = "packages";
|
|
||||||
|
|
||||||
pub fn get_package_dir() -> PathBuf {
|
|
||||||
// Deprecated $HOME/.espanso/packages directory compatibility check
|
|
||||||
let config_dir = get_config_dir();
|
|
||||||
let legacy_package_dir = config_dir.join(PACKAGES_FOLDER_NAME);
|
|
||||||
if legacy_package_dir.exists() {
|
|
||||||
return legacy_package_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// New package location, starting from version v0.3.0
|
|
||||||
let data_dir = get_data_dir();
|
|
||||||
let package_dir = data_dir.join(PACKAGES_FOLDER_NAME);
|
|
||||||
create_dir_all(&package_dir).expect("Error creating espanso packages directory");
|
|
||||||
package_dir
|
|
||||||
}
|
|
|
@ -1,252 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::bridge::windows::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::event::KeyModifier::*;
|
|
||||||
use crate::event::{ActionType, Event, KeyEvent, KeyModifier};
|
|
||||||
use log::{debug, error, info};
|
|
||||||
use std::ffi::c_void;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::atomic::Ordering::Acquire;
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use widestring::{U16CStr, U16CString};
|
|
||||||
|
|
||||||
const BMP_BINARY: &[u8] = include_bytes!("../res/win/espanso.bmp");
|
|
||||||
const ICO_BINARY: &[u8] = include_bytes!("../res/win/espanso.ico");
|
|
||||||
const RED_ICO_BINARY: &[u8] = include_bytes!("../res/win/espansored.ico");
|
|
||||||
|
|
||||||
pub struct WindowsContext {
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsContext {
|
|
||||||
pub fn new(
|
|
||||||
config: Configs,
|
|
||||||
send_channel: Sender<Event>,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Box<WindowsContext> {
|
|
||||||
// Initialize image resources
|
|
||||||
|
|
||||||
let espanso_dir = super::get_data_dir();
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Initializing Espanso resources in {}",
|
|
||||||
espanso_dir.as_path().display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let espanso_bmp_image = espanso_dir.join("espansoicon.bmp");
|
|
||||||
if espanso_bmp_image.exists() {
|
|
||||||
info!("BMP already initialized, skipping.");
|
|
||||||
} else {
|
|
||||||
fs::write(&espanso_bmp_image, BMP_BINARY).expect("Unable to write windows bmp file");
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Extracted bmp icon to: {}",
|
|
||||||
espanso_bmp_image.to_str().unwrap_or("error")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let espanso_ico_image = get_icon_path(&espanso_dir);
|
|
||||||
if espanso_ico_image.exists() {
|
|
||||||
info!("ICO already initialized, skipping.");
|
|
||||||
} else {
|
|
||||||
fs::write(&espanso_ico_image, ICO_BINARY).expect("Unable to write windows ico file");
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Extracted 'ico' icon to: {}",
|
|
||||||
espanso_ico_image.to_str().unwrap_or("error")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let espanso_red_ico_image = espanso_dir.join("espansored.ico");
|
|
||||||
if espanso_red_ico_image.exists() {
|
|
||||||
info!("red ICO already initialized, skipping.");
|
|
||||||
} else {
|
|
||||||
fs::write(&espanso_red_ico_image, RED_ICO_BINARY)
|
|
||||||
.expect("Unable to write windows ico file");
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Extracted 'red ico' icon to: {}",
|
|
||||||
espanso_red_ico_image.to_str().unwrap_or("error")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default();
|
|
||||||
let ico_icon = espanso_ico_image.to_str().unwrap_or_default();
|
|
||||||
let red_ico_icon = espanso_red_ico_image.to_str().unwrap_or_default();
|
|
||||||
|
|
||||||
let send_channel = send_channel;
|
|
||||||
|
|
||||||
let context = Box::new(WindowsContext {
|
|
||||||
send_channel,
|
|
||||||
is_injecting,
|
|
||||||
});
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let context_ptr = &*context as *const WindowsContext as *const c_void;
|
|
||||||
|
|
||||||
// Register callbacks
|
|
||||||
register_keypress_callback(keypress_callback);
|
|
||||||
register_icon_click_callback(icon_click_callback);
|
|
||||||
register_context_menu_click_callback(context_menu_click_callback);
|
|
||||||
|
|
||||||
let ico_file_c = U16CString::from_str(ico_icon).unwrap();
|
|
||||||
let red_ico_file_c = U16CString::from_str(red_ico_icon).unwrap();
|
|
||||||
let bmp_file_c = U16CString::from_str(bmp_icon).unwrap();
|
|
||||||
|
|
||||||
let show_icon = if config.show_icon { 1 } else { 0 };
|
|
||||||
|
|
||||||
// Initialize the windows
|
|
||||||
let res = initialize(
|
|
||||||
context_ptr,
|
|
||||||
ico_file_c.as_ptr(),
|
|
||||||
red_ico_file_c.as_ptr(),
|
|
||||||
bmp_file_c.as_ptr(),
|
|
||||||
show_icon,
|
|
||||||
);
|
|
||||||
if res != 1 {
|
|
||||||
panic!("Can't initialize Windows context")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Context for WindowsContext {
|
|
||||||
fn eventloop(&self) {
|
|
||||||
unsafe {
|
|
||||||
eventloop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_icon_path(espanso_dir: &Path) -> PathBuf {
|
|
||||||
espanso_dir.join("espanso.ico")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native bridge code
|
|
||||||
|
|
||||||
pub fn update_icon(enabled: bool) {
|
|
||||||
unsafe {
|
|
||||||
crate::bridge::windows::update_tray_icon(if enabled { 1 } else { 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn keypress_callback(
|
|
||||||
_self: *mut c_void,
|
|
||||||
raw_buffer: *const u16,
|
|
||||||
len: i32,
|
|
||||||
event_type: i32,
|
|
||||||
key_code: i32,
|
|
||||||
variant: i32,
|
|
||||||
is_key_down: i32,
|
|
||||||
) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut WindowsContext;
|
|
||||||
|
|
||||||
// If espanso is currently injecting text, we should avoid processing
|
|
||||||
// external events, as it could happen that espanso reinterpret its
|
|
||||||
// own input.
|
|
||||||
if (*_self).is_injecting.load(Acquire) {
|
|
||||||
debug!("Input ignored while espanso is injecting text...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if event_type == 0 {
|
|
||||||
// Char event
|
|
||||||
if is_key_down != 0 {
|
|
||||||
// KEY DOWN EVENT
|
|
||||||
// Convert the received buffer to a string
|
|
||||||
let buffer = std::slice::from_raw_parts(raw_buffer, len as usize);
|
|
||||||
let c_string = U16CStr::from_slice_with_nul(buffer);
|
|
||||||
|
|
||||||
if let Ok(c_string) = c_string {
|
|
||||||
let string = c_string.to_string();
|
|
||||||
|
|
||||||
// Send the char through the channel
|
|
||||||
match string {
|
|
||||||
Ok(string) => {
|
|
||||||
let event = Event::Key(KeyEvent::Char(string));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to receive char: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("unable to decode widechar");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if event_type == 1 {
|
|
||||||
// Modifier event
|
|
||||||
if is_key_down == 0 {
|
|
||||||
let modifier: Option<KeyModifier> = match (key_code, variant) {
|
|
||||||
(0x5B, _) => Some(LEFT_META),
|
|
||||||
(0x5C, _) => Some(RIGHT_META),
|
|
||||||
(0x10, 1) => Some(LEFT_SHIFT),
|
|
||||||
(0x10, 2) => Some(RIGHT_SHIFT),
|
|
||||||
(0x12, 1) => Some(LEFT_ALT),
|
|
||||||
(0x12, 2) => Some(RIGHT_ALT),
|
|
||||||
(0x11, 1) => Some(LEFT_CTRL),
|
|
||||||
(0x11, 2) => Some(RIGHT_CTRL),
|
|
||||||
(0x08, _) => Some(BACKSPACE),
|
|
||||||
(0x14, _) => Some(CAPS_LOCK),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(modifier) = modifier {
|
|
||||||
let event = Event::Key(KeyEvent::Modifier(modifier));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
} else {
|
|
||||||
// Not one of the default modifiers, send an "other" event
|
|
||||||
let event = Event::Key(KeyEvent::Other);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other type of event
|
|
||||||
let event = Event::Key(KeyEvent::Other);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn icon_click_callback(_self: *mut c_void) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut WindowsContext;
|
|
||||||
|
|
||||||
let event = Event::Action(ActionType::IconClick);
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn context_menu_click_callback(_self: *mut c_void, id: i32) {
|
|
||||||
unsafe {
|
|
||||||
let _self = _self as *mut WindowsContext;
|
|
||||||
|
|
||||||
let event = Event::Action(ActionType::from(id));
|
|
||||||
(*_self).send_channel.send(event).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
76
src/edit.rs
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn default_editor() -> String {
|
|
||||||
"/bin/nano".to_owned()
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn default_editor() -> String {
|
|
||||||
"/usr/bin/nano".to_owned()
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn default_editor() -> String {
|
|
||||||
"C:\\Windows\\System32\\notepad.exe".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open_editor(file_path: &Path) -> bool {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
// Check if another editor is defined in the environment variables
|
|
||||||
let editor_var = std::env::var_os("EDITOR");
|
|
||||||
let visual_var = std::env::var_os("VISUAL");
|
|
||||||
|
|
||||||
// Prioritize the editors specified by the environment variable, use the default one
|
|
||||||
let editor: String = if let Some(editor_var) = editor_var {
|
|
||||||
editor_var.to_string_lossy().to_string()
|
|
||||||
} else if let Some(visual_var) = visual_var {
|
|
||||||
visual_var.to_string_lossy().to_string()
|
|
||||||
} else {
|
|
||||||
default_editor()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start the editor and wait for its termination
|
|
||||||
let status = if cfg!(target_os = "windows") {
|
|
||||||
Command::new(&editor).arg(file_path).spawn()
|
|
||||||
} else {
|
|
||||||
// On Unix, spawn the editor using the shell so that it can
|
|
||||||
// accept parameters. See issue #245
|
|
||||||
Command::new("/bin/bash")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(format!("{} '{}'", editor, file_path.to_string_lossy()))
|
|
||||||
.spawn()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(mut child) = status {
|
|
||||||
// Wait for the user to edit the configuration
|
|
||||||
let result = child.wait();
|
|
||||||
|
|
||||||
if let Ok(exit_status) = result {
|
|
||||||
exit_status.success()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("Error: could not start editor at: {}", &editor);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
545
src/engine.rs
|
@ -1,545 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::clipboard::ClipboardManager;
|
|
||||||
use crate::config::BackendType;
|
|
||||||
use crate::config::{ConfigManager, Configs};
|
|
||||||
use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
|
|
||||||
use crate::keyboard::KeyboardManager;
|
|
||||||
use crate::matcher::{Match, MatchReceiver};
|
|
||||||
use crate::protocol::{send_command_or_warn, IPCCommand, Service};
|
|
||||||
use crate::render::{RenderResult, Renderer};
|
|
||||||
use crate::{
|
|
||||||
guard::InjectGuard,
|
|
||||||
ui::{MenuItem, MenuItemType, UIManager},
|
|
||||||
};
|
|
||||||
use log::{debug, error, info, warn};
|
|
||||||
use regex::Regex;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::atomic::Ordering::Release;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub struct Engine<
|
|
||||||
'a,
|
|
||||||
S: KeyboardManager,
|
|
||||||
C: ClipboardManager,
|
|
||||||
M: ConfigManager<'a>,
|
|
||||||
U: UIManager,
|
|
||||||
R: Renderer,
|
|
||||||
> {
|
|
||||||
keyboard_manager: &'a S,
|
|
||||||
clipboard_manager: &'a C,
|
|
||||||
config_manager: &'a M,
|
|
||||||
ui_manager: &'a U,
|
|
||||||
renderer: &'a R,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
|
|
||||||
enabled: RefCell<bool>,
|
|
||||||
// Trigger string and injected text len pair
|
|
||||||
last_expansion_data: RefCell<Option<(String, i32)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<
|
|
||||||
'a,
|
|
||||||
S: KeyboardManager,
|
|
||||||
C: ClipboardManager,
|
|
||||||
M: ConfigManager<'a>,
|
|
||||||
U: UIManager,
|
|
||||||
R: Renderer,
|
|
||||||
> Engine<'a, S, C, M, U, R>
|
|
||||||
{
|
|
||||||
pub fn new(
|
|
||||||
keyboard_manager: &'a S,
|
|
||||||
clipboard_manager: &'a C,
|
|
||||||
config_manager: &'a M,
|
|
||||||
ui_manager: &'a U,
|
|
||||||
renderer: &'a R,
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
) -> Engine<'a, S, C, M, U, R> {
|
|
||||||
let enabled = RefCell::new(true);
|
|
||||||
let last_expansion_data = RefCell::new(None);
|
|
||||||
|
|
||||||
Engine {
|
|
||||||
keyboard_manager,
|
|
||||||
clipboard_manager,
|
|
||||||
config_manager,
|
|
||||||
ui_manager,
|
|
||||||
renderer,
|
|
||||||
is_injecting,
|
|
||||||
enabled,
|
|
||||||
last_expansion_data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_menu(&self) -> Vec<MenuItem> {
|
|
||||||
let mut menu = Vec::new();
|
|
||||||
|
|
||||||
let enabled = self.enabled.borrow();
|
|
||||||
let toggle_text = if *enabled { "Disable" } else { "Enable" }.to_owned();
|
|
||||||
menu.push(MenuItem {
|
|
||||||
item_type: MenuItemType::Button,
|
|
||||||
item_name: toggle_text,
|
|
||||||
item_id: ActionType::Toggle as i32,
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.push(MenuItem {
|
|
||||||
item_type: MenuItemType::Separator,
|
|
||||||
item_name: "".to_owned(),
|
|
||||||
item_id: 998,
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.push(MenuItem {
|
|
||||||
item_type: MenuItemType::Button,
|
|
||||||
item_name: "Reload configs".to_owned(),
|
|
||||||
item_id: ActionType::RestartWorker as i32,
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.push(MenuItem {
|
|
||||||
item_type: MenuItemType::Separator,
|
|
||||||
item_name: "".to_owned(),
|
|
||||||
item_id: 999,
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.push(MenuItem {
|
|
||||||
item_type: MenuItemType::Button,
|
|
||||||
item_name: "Exit espanso".to_owned(),
|
|
||||||
item_id: ActionType::Exit as i32,
|
|
||||||
});
|
|
||||||
|
|
||||||
menu
|
|
||||||
}
|
|
||||||
|
|
||||||
fn return_content_if_preserve_clipboard_is_enabled(&self) -> Option<String> {
|
|
||||||
// If the preserve_clipboard option is enabled, first save the current
|
|
||||||
// clipboard content in order to restore it later.
|
|
||||||
if self.config_manager.default_config().preserve_clipboard {
|
|
||||||
match self.clipboard_manager.get_clipboard() {
|
|
||||||
Some(clipboard) => Some(clipboard),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_match_by_trigger(&self, trigger: &str) -> Option<Match> {
|
|
||||||
let config = self.config_manager.active_config();
|
|
||||||
|
|
||||||
if let Some(m) = config
|
|
||||||
.matches
|
|
||||||
.iter()
|
|
||||||
.find(|m| m.triggers.iter().any(|t| t == trigger))
|
|
||||||
{
|
|
||||||
Some(m.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inject_text(
|
|
||||||
&self,
|
|
||||||
config: &Configs,
|
|
||||||
target_string: &str,
|
|
||||||
force_clipboard: bool,
|
|
||||||
is_html: bool,
|
|
||||||
) {
|
|
||||||
let backend = if force_clipboard || is_html {
|
|
||||||
&BackendType::Clipboard
|
|
||||||
} else if config.backend == BackendType::Auto {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
|
||||||
if all_ascii {
|
|
||||||
debug!("All elements of the replacement are ascii, using Inject backend");
|
|
||||||
&BackendType::Inject
|
|
||||||
} else {
|
|
||||||
debug!("There are non-ascii characters, using Clipboard backend");
|
|
||||||
&BackendType::Clipboard
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&BackendType::Inject
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&config.backend
|
|
||||||
};
|
|
||||||
|
|
||||||
match backend {
|
|
||||||
BackendType::Inject => {
|
|
||||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
|
||||||
let splits = target_string.split('\n');
|
|
||||||
|
|
||||||
for (i, split) in splits.enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
self.keyboard_manager.send_enter(&config);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.keyboard_manager.send_string(&config, split);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackendType::Clipboard => {
|
|
||||||
if !is_html {
|
|
||||||
self.clipboard_manager.set_clipboard(&target_string);
|
|
||||||
} else {
|
|
||||||
self.clipboard_manager.set_clipboard_html(&target_string);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("Unsupported backend type evaluation.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inject_match(
|
|
||||||
&self,
|
|
||||||
m: &Match,
|
|
||||||
trailing_separator: Option<char>,
|
|
||||||
trigger_offset: usize,
|
|
||||||
skip_delete: bool,
|
|
||||||
) -> Option<(String, i32)> {
|
|
||||||
let config = self.config_manager.active_config();
|
|
||||||
|
|
||||||
if !config.enable_active {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block espanso from reinterpreting its own actions
|
|
||||||
let _inject_guard = InjectGuard::new(self.is_injecting.clone(), &config);
|
|
||||||
|
|
||||||
let char_count = if trailing_separator.is_none() {
|
|
||||||
m.triggers[trigger_offset].chars().count() as i32
|
|
||||||
} else {
|
|
||||||
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
|
|
||||||
};
|
|
||||||
|
|
||||||
// If configured to do so, wait until the modifier keys are released (or timeout) so
|
|
||||||
// that we avoid unwanted interactions. As an example, see:
|
|
||||||
// https://github.com/federico-terzi/espanso/issues/470
|
|
||||||
if config.wait_for_modifiers_release {
|
|
||||||
crate::keyboard::wait_for_modifiers_release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !skip_delete {
|
|
||||||
self.keyboard_manager.delete_string(&config, char_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut previous_clipboard_content: Option<String> = None;
|
|
||||||
|
|
||||||
let rendered = self
|
|
||||||
.renderer
|
|
||||||
.render_match(m, trigger_offset, config, vec![]);
|
|
||||||
|
|
||||||
let mut expansion_data: Option<(String, i32)> = None;
|
|
||||||
|
|
||||||
match rendered {
|
|
||||||
RenderResult::Text(mut target_string) => {
|
|
||||||
// If a trailing separator was counted in the match, add it back to the target string
|
|
||||||
if let Some(trailing_separator) = trailing_separator {
|
|
||||||
if trailing_separator == '\r' {
|
|
||||||
// If the trailing separator is a carriage return,
|
|
||||||
target_string.push('\n'); // convert it to new line
|
|
||||||
} else {
|
|
||||||
target_string.push(trailing_separator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert Windows style newlines into unix styles
|
|
||||||
target_string = target_string.replace("\r\n", "\n");
|
|
||||||
|
|
||||||
// Calculate cursor rewind moves if a Cursor Hint is present
|
|
||||||
let index = target_string.find("$|$");
|
|
||||||
let cursor_rewind = if let Some(index) = index {
|
|
||||||
// Convert the byte index to a char index
|
|
||||||
let char_str = &target_string[0..index];
|
|
||||||
let char_index = char_str.chars().count();
|
|
||||||
let total_size = target_string.chars().count();
|
|
||||||
|
|
||||||
// Remove the $|$ placeholder
|
|
||||||
target_string = target_string.replace("$|$", "");
|
|
||||||
|
|
||||||
// Calculate the amount of rewind moves needed (LEFT ARROW).
|
|
||||||
// Subtract also 3, equal to the number of chars of the placeholder "$|$"
|
|
||||||
let moves = (total_size - char_index - 3) as i32;
|
|
||||||
Some(moves)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the preserve_clipboard option is enabled, save the current
|
|
||||||
// clipboard content to restore it later.
|
|
||||||
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
|
|
||||||
|
|
||||||
self.inject_text(&config, &target_string, m.force_clipboard, m.is_html);
|
|
||||||
|
|
||||||
// Disallow undo backspace if cursor positioning is used or text is HTML
|
|
||||||
if cursor_rewind.is_none() && !m.is_html {
|
|
||||||
expansion_data = Some((
|
|
||||||
m.triggers[trigger_offset].clone(),
|
|
||||||
target_string.chars().count() as i32,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(moves) = cursor_rewind {
|
|
||||||
// Simulate left arrow key presses to bring the cursor into the desired position
|
|
||||||
self.keyboard_manager.move_cursor_left(&config, moves);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RenderResult::Image(image_path) => {
|
|
||||||
// If the preserve_clipboard option is enabled, save the current
|
|
||||||
// clipboard content to restore it later.
|
|
||||||
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
|
|
||||||
|
|
||||||
self.clipboard_manager.set_clipboard_image(&image_path);
|
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
|
||||||
}
|
|
||||||
RenderResult::Error => {
|
|
||||||
error!("Could not render match: {}", m.triggers[trigger_offset]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore previous clipboard content
|
|
||||||
if let Some(previous_clipboard_content) = previous_clipboard_content {
|
|
||||||
// Sometimes an expansion gets overwritten before pasting by the previous content
|
|
||||||
// A delay is needed to mitigate the problem
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(
|
|
||||||
config.restore_clipboard_delay as u64,
|
|
||||||
));
|
|
||||||
|
|
||||||
self.clipboard_manager
|
|
||||||
.set_clipboard(&previous_clipboard_content);
|
|
||||||
}
|
|
||||||
|
|
||||||
expansion_data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<
|
|
||||||
'a,
|
|
||||||
S: KeyboardManager,
|
|
||||||
C: ClipboardManager,
|
|
||||||
M: ConfigManager<'a>,
|
|
||||||
U: UIManager,
|
|
||||||
R: Renderer,
|
|
||||||
> MatchReceiver for Engine<'a, S, C, M, U, R>
|
|
||||||
{
|
|
||||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
|
|
||||||
let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false);
|
|
||||||
let mut last_expansion_data = self.last_expansion_data.borrow_mut();
|
|
||||||
(*last_expansion_data) = expansion_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_undo(&self) {
|
|
||||||
let config = self.config_manager.active_config();
|
|
||||||
|
|
||||||
if !config.undo_backspace {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block espanso from reinterpreting its own actions
|
|
||||||
let _inject_guard = InjectGuard::new(self.is_injecting.clone(), &config);
|
|
||||||
|
|
||||||
let last_expansion_data = self.last_expansion_data.borrow();
|
|
||||||
if let Some(ref last_expansion_data) = *last_expansion_data {
|
|
||||||
let (trigger_string, injected_text_len) = last_expansion_data;
|
|
||||||
// Delete the previously injected text, minus one character as it has been consumed by the backspace
|
|
||||||
self.keyboard_manager
|
|
||||||
.delete_string(&config, *injected_text_len - 1);
|
|
||||||
// Restore previous text
|
|
||||||
self.inject_text(&config, trigger_string, false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_enable_update(&self, status: bool) {
|
|
||||||
let message = if status {
|
|
||||||
"espanso enabled"
|
|
||||||
} else {
|
|
||||||
"espanso disabled"
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Toggled: {}", message);
|
|
||||||
|
|
||||||
let mut enabled_ref = self.enabled.borrow_mut();
|
|
||||||
*enabled_ref = status;
|
|
||||||
|
|
||||||
let config = self.config_manager.default_config();
|
|
||||||
|
|
||||||
if config.show_notifications {
|
|
||||||
self.ui_manager.notify(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the icon on supported OSes.
|
|
||||||
crate::context::update_icon(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_passive(&self) {
|
|
||||||
let config = self.config_manager.active_config();
|
|
||||||
|
|
||||||
if !config.enable_passive {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block espanso from reinterpreting its own actions
|
|
||||||
self.is_injecting.store(true, Release);
|
|
||||||
|
|
||||||
// In order to avoid pasting previous clipboard contents, we need to check if
|
|
||||||
// a new clipboard was effectively copied.
|
|
||||||
// See issue: https://github.com/federico-terzi/espanso/issues/213
|
|
||||||
let previous_clipboard = self.clipboard_manager.get_clipboard().unwrap_or_default();
|
|
||||||
|
|
||||||
// Sleep for a while, giving time to effectively copy the text
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
|
||||||
|
|
||||||
// Clear the clipboard, for new-content detection later
|
|
||||||
self.clipboard_manager.set_clipboard("");
|
|
||||||
|
|
||||||
// Sleep for a while, giving time to effectively copy the text
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
|
||||||
|
|
||||||
// Trigger a copy shortcut to transfer the content of the selection to the clipboard
|
|
||||||
self.keyboard_manager.trigger_copy(&config);
|
|
||||||
|
|
||||||
// Sleep for a while, giving time to effectively copy the text
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
|
||||||
|
|
||||||
// Then get the text from the clipboard and render the match output
|
|
||||||
let clipboard = self.clipboard_manager.get_clipboard();
|
|
||||||
|
|
||||||
if let Some(clipboard) = clipboard {
|
|
||||||
// Don't expand empty clipboards, as usually they are the result of an empty passive selection
|
|
||||||
if clipboard.trim().is_empty() {
|
|
||||||
info!("Avoiding passive expansion, as the user didn't select anything");
|
|
||||||
} else {
|
|
||||||
info!("Passive mode activated");
|
|
||||||
|
|
||||||
// Restore original clipboard in case it's used during render
|
|
||||||
self.clipboard_manager.set_clipboard(&previous_clipboard);
|
|
||||||
|
|
||||||
let rendered = self.renderer.render_passive(&clipboard, &config);
|
|
||||||
|
|
||||||
match rendered {
|
|
||||||
RenderResult::Text(payload) => {
|
|
||||||
// Paste back the result in the field
|
|
||||||
self.clipboard_manager.set_clipboard(&payload);
|
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
|
||||||
}
|
|
||||||
_ => warn!("Cannot expand passive match"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(config.passive_delay));
|
|
||||||
|
|
||||||
// Restore original clipboard
|
|
||||||
self.clipboard_manager.set_clipboard(&previous_clipboard);
|
|
||||||
|
|
||||||
// Re-allow espanso to interpret actions
|
|
||||||
self.is_injecting.store(false, Release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<
|
|
||||||
'a,
|
|
||||||
S: KeyboardManager,
|
|
||||||
C: ClipboardManager,
|
|
||||||
M: ConfigManager<'a>,
|
|
||||||
U: UIManager,
|
|
||||||
R: Renderer,
|
|
||||||
> ActionEventReceiver for Engine<'a, S, C, M, U, R>
|
|
||||||
{
|
|
||||||
fn on_action_event(&self, e: ActionType) {
|
|
||||||
let config = self.config_manager.default_config();
|
|
||||||
match e {
|
|
||||||
ActionType::IconClick => {
|
|
||||||
self.ui_manager.show_menu(self.build_menu());
|
|
||||||
}
|
|
||||||
ActionType::ExitWorker => {
|
|
||||||
info!("terminating worker process");
|
|
||||||
self.ui_manager.cleanup();
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
ActionType::Exit => {
|
|
||||||
send_command_or_warn(Service::Daemon, config.clone(), IPCCommand::exit());
|
|
||||||
}
|
|
||||||
ActionType::RestartWorker => {
|
|
||||||
send_command_or_warn(
|
|
||||||
Service::Daemon,
|
|
||||||
config.clone(),
|
|
||||||
IPCCommand::restart_worker(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<
|
|
||||||
'a,
|
|
||||||
S: KeyboardManager,
|
|
||||||
C: ClipboardManager,
|
|
||||||
M: ConfigManager<'a>,
|
|
||||||
U: UIManager,
|
|
||||||
R: Renderer,
|
|
||||||
> SystemEventReceiver for Engine<'a, S, C, M, U, R>
|
|
||||||
{
|
|
||||||
fn on_system_event(&self, e: SystemEvent) {
|
|
||||||
match e {
|
|
||||||
// MacOS specific
|
|
||||||
SystemEvent::SecureInputEnabled(app_name, path) => {
|
|
||||||
info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path);
|
|
||||||
|
|
||||||
let config = self.config_manager.default_config();
|
|
||||||
if config.secure_input_notification && config.show_notifications {
|
|
||||||
self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::context::update_icon(false);
|
|
||||||
}
|
|
||||||
SystemEvent::SecureInputDisabled => {
|
|
||||||
info!("SecureInput has been disabled.");
|
|
||||||
|
|
||||||
let is_enabled = self.enabled.borrow();
|
|
||||||
crate::context::update_icon(*is_enabled);
|
|
||||||
}
|
|
||||||
SystemEvent::NotifyRequest(message) => {
|
|
||||||
let config = self.config_manager.default_config();
|
|
||||||
if config.show_notifications {
|
|
||||||
self.ui_manager.notify(&message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SystemEvent::Trigger(trigger) => {
|
|
||||||
let m = self.find_match_by_trigger(&trigger);
|
|
||||||
match m {
|
|
||||||
Some(m) => {
|
|
||||||
self.inject_match(&m, None, 0, true);
|
|
||||||
}
|
|
||||||
None => warn!("No match found with trigger: {}", trigger),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::event::{ActionEventReceiver, Event, KeyEventReceiver, SystemEventReceiver};
|
|
||||||
use std::sync::mpsc::Receiver;
|
|
||||||
|
|
||||||
pub trait EventManager {
|
|
||||||
fn eventloop(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DefaultEventManager<'a> {
|
|
||||||
receive_channel: Receiver<Event>,
|
|
||||||
key_receivers: Vec<&'a dyn KeyEventReceiver>,
|
|
||||||
action_receivers: Vec<&'a dyn ActionEventReceiver>,
|
|
||||||
system_receivers: Vec<&'a dyn SystemEventReceiver>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DefaultEventManager<'a> {
|
|
||||||
pub fn new(
|
|
||||||
receive_channel: Receiver<Event>,
|
|
||||||
key_receivers: Vec<&'a dyn KeyEventReceiver>,
|
|
||||||
action_receivers: Vec<&'a dyn ActionEventReceiver>,
|
|
||||||
system_receivers: Vec<&'a dyn SystemEventReceiver>,
|
|
||||||
) -> DefaultEventManager<'a> {
|
|
||||||
DefaultEventManager {
|
|
||||||
receive_channel,
|
|
||||||
key_receivers,
|
|
||||||
action_receivers,
|
|
||||||
system_receivers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> EventManager for DefaultEventManager<'a> {
|
|
||||||
fn eventloop(&self) {
|
|
||||||
loop {
|
|
||||||
match self.receive_channel.recv() {
|
|
||||||
Ok(event) => match event {
|
|
||||||
Event::Key(key_event) => {
|
|
||||||
self.key_receivers
|
|
||||||
.iter()
|
|
||||||
.for_each(move |&receiver| receiver.on_key_event(key_event.clone()));
|
|
||||||
}
|
|
||||||
Event::Action(action_event) => {
|
|
||||||
self.action_receivers
|
|
||||||
.iter()
|
|
||||||
.for_each(|&receiver| receiver.on_action_event(action_event.clone()));
|
|
||||||
}
|
|
||||||
Event::System(system_event) => {
|
|
||||||
self.system_receivers.iter().for_each(move |&receiver| {
|
|
||||||
receiver.on_system_event(system_event.clone())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => panic!("Broken event channel {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
211
src/event/mod.rs
|
@ -1,211 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub(crate) mod manager;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Event {
|
|
||||||
Action(ActionType),
|
|
||||||
Key(KeyEvent),
|
|
||||||
System(SystemEvent),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ActionType {
|
|
||||||
Noop = 0,
|
|
||||||
Toggle = 1,
|
|
||||||
Exit = 2,
|
|
||||||
IconClick = 3,
|
|
||||||
Enable = 4,
|
|
||||||
Disable = 5,
|
|
||||||
RestartWorker = 6,
|
|
||||||
ExitWorker = 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<i32> for ActionType {
|
|
||||||
fn from(id: i32) -> Self {
|
|
||||||
match id {
|
|
||||||
1 => ActionType::Toggle,
|
|
||||||
2 => ActionType::Exit,
|
|
||||||
3 => ActionType::IconClick,
|
|
||||||
4 => ActionType::Enable,
|
|
||||||
5 => ActionType::Disable,
|
|
||||||
6 => ActionType::RestartWorker,
|
|
||||||
7 => ActionType::ExitWorker,
|
|
||||||
_ => ActionType::Noop,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum KeyEvent {
|
|
||||||
Char(String),
|
|
||||||
Modifier(KeyModifier),
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum KeyModifier {
|
|
||||||
CTRL,
|
|
||||||
SHIFT,
|
|
||||||
ALT,
|
|
||||||
META,
|
|
||||||
BACKSPACE,
|
|
||||||
OFF,
|
|
||||||
|
|
||||||
// These are specific variants of the ones above. See issue: #117
|
|
||||||
// https://github.com/federico-terzi/espanso/issues/117
|
|
||||||
LEFT_CTRL,
|
|
||||||
RIGHT_CTRL,
|
|
||||||
LEFT_ALT,
|
|
||||||
RIGHT_ALT,
|
|
||||||
LEFT_META,
|
|
||||||
RIGHT_META,
|
|
||||||
LEFT_SHIFT,
|
|
||||||
RIGHT_SHIFT,
|
|
||||||
|
|
||||||
// Special cases, should not be used in config
|
|
||||||
CAPS_LOCK,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyModifier {
|
|
||||||
/// This function is used to compare KeyModifiers, considering the relations between
|
|
||||||
/// the generic modifier and the specific left/right variant
|
|
||||||
/// For example, CTRL will match with CTRL, LEFT_CTRL and RIGHT_CTRL;
|
|
||||||
/// but LEFT_CTRL will only match will LEFT_CTRL
|
|
||||||
pub fn shallow_equals(current: &KeyModifier, config: &KeyModifier) -> bool {
|
|
||||||
use KeyModifier::*;
|
|
||||||
|
|
||||||
match config {
|
|
||||||
KeyModifier::CTRL => {
|
|
||||||
current == &LEFT_CTRL || current == &RIGHT_CTRL || current == &CTRL
|
|
||||||
}
|
|
||||||
KeyModifier::SHIFT => {
|
|
||||||
current == &LEFT_SHIFT || current == &RIGHT_SHIFT || current == &SHIFT
|
|
||||||
}
|
|
||||||
KeyModifier::ALT => current == &LEFT_ALT || current == &RIGHT_ALT || current == &ALT,
|
|
||||||
KeyModifier::META => {
|
|
||||||
current == &LEFT_META || current == &RIGHT_META || current == &META
|
|
||||||
}
|
|
||||||
KeyModifier::BACKSPACE => current == &BACKSPACE,
|
|
||||||
KeyModifier::LEFT_CTRL => current == &LEFT_CTRL,
|
|
||||||
KeyModifier::RIGHT_CTRL => current == &RIGHT_CTRL,
|
|
||||||
KeyModifier::LEFT_ALT => current == &LEFT_ALT,
|
|
||||||
KeyModifier::RIGHT_ALT => current == &RIGHT_ALT,
|
|
||||||
KeyModifier::LEFT_META => current == &LEFT_META,
|
|
||||||
KeyModifier::RIGHT_META => current == &RIGHT_META,
|
|
||||||
KeyModifier::LEFT_SHIFT => current == &LEFT_SHIFT,
|
|
||||||
KeyModifier::RIGHT_SHIFT => current == &RIGHT_SHIFT,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum SystemEvent {
|
|
||||||
// MacOS specific
|
|
||||||
SecureInputEnabled(String, String), // AppName, App Path
|
|
||||||
SecureInputDisabled,
|
|
||||||
|
|
||||||
// Notification
|
|
||||||
NotifyRequest(String),
|
|
||||||
|
|
||||||
// Trigger an expansion from IPC
|
|
||||||
Trigger(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receivers
|
|
||||||
|
|
||||||
pub trait KeyEventReceiver {
|
|
||||||
fn on_key_event(&self, e: KeyEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ActionEventReceiver {
|
|
||||||
fn on_action_event(&self, e: ActionType);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SystemEventReceiver {
|
|
||||||
fn on_system_event(&self, e: SystemEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTS
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::KeyModifier::*;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shallow_equals_ctrl() {
|
|
||||||
assert!(KeyModifier::shallow_equals(&CTRL, &CTRL));
|
|
||||||
assert!(KeyModifier::shallow_equals(&LEFT_CTRL, &CTRL));
|
|
||||||
assert!(KeyModifier::shallow_equals(&RIGHT_CTRL, &CTRL));
|
|
||||||
|
|
||||||
assert!(!KeyModifier::shallow_equals(&CTRL, &LEFT_CTRL));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&CTRL, &RIGHT_CTRL));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shallow_equals_shift() {
|
|
||||||
assert!(KeyModifier::shallow_equals(&SHIFT, &SHIFT));
|
|
||||||
assert!(KeyModifier::shallow_equals(&LEFT_SHIFT, &SHIFT));
|
|
||||||
assert!(KeyModifier::shallow_equals(&RIGHT_SHIFT, &SHIFT));
|
|
||||||
|
|
||||||
assert!(!KeyModifier::shallow_equals(&SHIFT, &LEFT_SHIFT));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&SHIFT, &RIGHT_SHIFT));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shallow_equals_alt() {
|
|
||||||
assert!(KeyModifier::shallow_equals(&ALT, &ALT));
|
|
||||||
assert!(KeyModifier::shallow_equals(&LEFT_ALT, &ALT));
|
|
||||||
assert!(KeyModifier::shallow_equals(&RIGHT_ALT, &ALT));
|
|
||||||
|
|
||||||
assert!(!KeyModifier::shallow_equals(&ALT, &LEFT_ALT));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&ALT, &RIGHT_ALT));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shallow_equals_meta() {
|
|
||||||
assert!(KeyModifier::shallow_equals(&META, &META));
|
|
||||||
assert!(KeyModifier::shallow_equals(&LEFT_META, &META));
|
|
||||||
assert!(KeyModifier::shallow_equals(&RIGHT_META, &META));
|
|
||||||
|
|
||||||
assert!(!KeyModifier::shallow_equals(&META, &LEFT_META));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&META, &RIGHT_META));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shallow_equals_backspace() {
|
|
||||||
assert!(KeyModifier::shallow_equals(&BACKSPACE, &BACKSPACE));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shallow_equals_off() {
|
|
||||||
assert!(!KeyModifier::shallow_equals(&OFF, &CTRL));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&OFF, &ALT));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&OFF, &META));
|
|
||||||
assert!(!KeyModifier::shallow_equals(&OFF, &SHIFT));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::clipboard::ClipboardManager;
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use super::ExtensionOut;
|
|
||||||
|
|
||||||
pub struct ClipboardExtension {
|
|
||||||
clipboard_manager: Box<dyn ClipboardManager>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClipboardExtension {
|
|
||||||
pub fn new(clipboard_manager: Box<dyn ClipboardManager>) -> ClipboardExtension {
|
|
||||||
ClipboardExtension { clipboard_manager }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for ClipboardExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
String::from("clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
_: &Mapping,
|
|
||||||
_: &Vec<String>,
|
|
||||||
_: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> ExtensionOut {
|
|
||||||
if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
|
|
||||||
Ok(Some(ExtensionResult::Single(clipboard)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019-2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use chrono::{DateTime, Duration, Local};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use super::ExtensionOut;
|
|
||||||
|
|
||||||
pub struct DateExtension {}
|
|
||||||
|
|
||||||
impl DateExtension {
|
|
||||||
pub fn new() -> DateExtension {
|
|
||||||
DateExtension {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for DateExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
String::from("date")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
_: &Vec<String>,
|
|
||||||
_: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> ExtensionOut {
|
|
||||||
let mut now: DateTime<Local> = Local::now();
|
|
||||||
|
|
||||||
// Compute the given offset
|
|
||||||
let offset = params.get(&Value::from("offset"));
|
|
||||||
if let Some(offset) = offset {
|
|
||||||
let seconds = offset.as_i64().unwrap_or_else(|| 0);
|
|
||||||
let offset = Duration::seconds(seconds);
|
|
||||||
now = now + offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
let format = params.get(&Value::from("format"));
|
|
||||||
|
|
||||||
let date = if let Some(format) = format {
|
|
||||||
now.format(format.as_str().unwrap()).to_string()
|
|
||||||
} else {
|
|
||||||
now.to_rfc2822()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(ExtensionResult::Single(date)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct DummyExtension {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DummyExtension {
|
|
||||||
pub fn new(name: &str) -> DummyExtension {
|
|
||||||
DummyExtension {
|
|
||||||
name: name.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for DummyExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
_: &Vec<String>,
|
|
||||||
_: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let echo = params.get(&Value::from("echo"));
|
|
||||||
|
|
||||||
if let Some(echo) = echo {
|
|
||||||
Ok(Some(ExtensionResult::Single(
|
|
||||||
echo.as_str().unwrap_or_default().to_owned(),
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::{config::Configs, extension::ExtensionResult, ui::modulo::ModuloManager};
|
|
||||||
use log::{error, warn};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct FormExtension {
|
|
||||||
manager: ModuloManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FormExtension {
|
|
||||||
pub fn new(config: &Configs) -> FormExtension {
|
|
||||||
let manager = ModuloManager::new(config);
|
|
||||||
FormExtension { manager }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for FormExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"form".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
_: &Vec<String>,
|
|
||||||
_: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let layout = params.get(&Value::from("layout"));
|
|
||||||
let layout = if let Some(value) = layout {
|
|
||||||
value.as_str().unwrap_or_default().to_string()
|
|
||||||
} else {
|
|
||||||
error!("invoking form extension without specifying a layout");
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut form_config = Mapping::new();
|
|
||||||
form_config.insert(Value::from("title"), Value::from("espanso"));
|
|
||||||
form_config.insert(Value::from("layout"), Value::from(layout));
|
|
||||||
|
|
||||||
if let Some(fields) = params.get(&Value::from("fields")) {
|
|
||||||
form_config.insert(Value::from("fields"), fields.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(icon_path) = crate::context::get_icon_path() {
|
|
||||||
form_config.insert(
|
|
||||||
Value::from("icon"),
|
|
||||||
Value::from(icon_path.to_string_lossy().to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let serialized_config: String =
|
|
||||||
serde_yaml::to_string(&form_config).expect("unable to serialize form config");
|
|
||||||
|
|
||||||
let output = self
|
|
||||||
.manager
|
|
||||||
.invoke(&["form", "-i", "-"], &serialized_config);
|
|
||||||
|
|
||||||
// On macOS and Windows, after the form closes we have to wait until the user releases the modifier keys
|
|
||||||
on_form_close();
|
|
||||||
|
|
||||||
if let Some(output) = output {
|
|
||||||
let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
|
|
||||||
match json {
|
|
||||||
Ok(json) => {
|
|
||||||
// Check if the JSON is empty. In those cases, it means the user exited
|
|
||||||
// the form before submitting it, therefore the expansion should stop
|
|
||||||
if json.is_empty() {
|
|
||||||
return Err(super::ExtensionError::Aborted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Some(ExtensionResult::Multiple(json)));
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
error!("modulo json parsing error: {}", error);
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("modulo form didn't return any output");
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn on_form_close() {
|
|
||||||
// NOOP on Linux
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn on_form_close() {
|
|
||||||
let released = crate::keyboard::windows::wait_for_modifiers_release();
|
|
||||||
if !released {
|
|
||||||
warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn on_form_close() {
|
|
||||||
let released = crate::keyboard::macos::wait_for_modifiers_release();
|
|
||||||
if !released {
|
|
||||||
warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::{clipboard::ClipboardManager, config::Configs};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
mod clipboard;
|
|
||||||
mod date;
|
|
||||||
pub mod dummy;
|
|
||||||
mod form;
|
|
||||||
pub mod multiecho;
|
|
||||||
mod random;
|
|
||||||
mod script;
|
|
||||||
mod shell;
|
|
||||||
mod utils;
|
|
||||||
pub mod vardummy;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum ExtensionResult {
|
|
||||||
Single(String),
|
|
||||||
Multiple(HashMap<String, String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum ExtensionError {
|
|
||||||
// Returned by an extension if an internal process occurred
|
|
||||||
Internal,
|
|
||||||
// Returned by an extension if the user aborted the expansion
|
|
||||||
// for example when pressing ESC inside a FormExtension.
|
|
||||||
Aborted,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type ExtensionOut = Result<Option<ExtensionResult>, ExtensionError>;
|
|
||||||
|
|
||||||
pub trait Extension {
|
|
||||||
fn name(&self) -> String;
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
args: &Vec<String>,
|
|
||||||
current_vars: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> ExtensionOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_extensions(
|
|
||||||
config: &Configs,
|
|
||||||
clipboard_manager: Box<dyn ClipboardManager>,
|
|
||||||
) -> Vec<Box<dyn Extension>> {
|
|
||||||
vec![
|
|
||||||
Box::new(date::DateExtension::new()),
|
|
||||||
Box::new(shell::ShellExtension::new()),
|
|
||||||
Box::new(script::ScriptExtension::new()),
|
|
||||||
Box::new(random::RandomExtension::new()),
|
|
||||||
Box::new(multiecho::MultiEchoExtension::new()),
|
|
||||||
Box::new(dummy::DummyExtension::new("dummy")),
|
|
||||||
Box::new(dummy::DummyExtension::new("echo")),
|
|
||||||
Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
|
|
||||||
Box::new(form::FormExtension::new(config)),
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct MultiEchoExtension {}
|
|
||||||
|
|
||||||
impl MultiEchoExtension {
|
|
||||||
pub fn new() -> MultiEchoExtension {
|
|
||||||
MultiEchoExtension {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for MultiEchoExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"multiecho".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
_: &Vec<String>,
|
|
||||||
_: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let mut output: HashMap<String, String> = HashMap::new();
|
|
||||||
for (key, value) in params.iter() {
|
|
||||||
if let Some(key) = key.as_str() {
|
|
||||||
if let Some(value) = value.as_str() {
|
|
||||||
output.insert(key.to_owned(), value.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(ExtensionResult::Multiple(output)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use log::{error, warn};
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct RandomExtension {}
|
|
||||||
|
|
||||||
impl RandomExtension {
|
|
||||||
pub fn new() -> RandomExtension {
|
|
||||||
RandomExtension {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for RandomExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
String::from("random")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
args: &Vec<String>,
|
|
||||||
_: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let choices = params.get(&Value::from("choices"));
|
|
||||||
if choices.is_none() {
|
|
||||||
warn!("No 'choices' parameter specified for random variable");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let choices = choices.unwrap().as_sequence();
|
|
||||||
if let Some(choices) = choices {
|
|
||||||
let str_choices = choices
|
|
||||||
.iter()
|
|
||||||
.map(|arg| arg.as_str().unwrap_or_default().to_string())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
// Select a random choice between the possibilities
|
|
||||||
let choice = str_choices.choose(&mut rand::thread_rng());
|
|
||||||
|
|
||||||
match choice {
|
|
||||||
Some(output) => {
|
|
||||||
// Render arguments
|
|
||||||
let output = crate::render::utils::render_args(output, args);
|
|
||||||
|
|
||||||
return Ok(Some(ExtensionResult::Single(output)));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!("Could not select a random choice.");
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error!("choices array have an invalid format '{:?}'", choices);
|
|
||||||
Err(super::ExtensionError::Internal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::extension::Extension;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_basic() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
let choices = vec!["first", "second", "third"];
|
|
||||||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
|
||||||
|
|
||||||
let extension = RandomExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec![], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
|
|
||||||
let output = output.unwrap();
|
|
||||||
|
|
||||||
assert!(choices
|
|
||||||
.into_iter()
|
|
||||||
.any(|x| ExtensionResult::Single(x.to_owned()) == output));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_random_with_args() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
let choices = vec!["first $0$", "second $0$", "$0$ third"];
|
|
||||||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
|
||||||
|
|
||||||
let extension = RandomExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec!["test".to_owned()], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
|
|
||||||
let output = output.unwrap();
|
|
||||||
|
|
||||||
let rendered_choices = vec!["first test", "second test", "test third"];
|
|
||||||
|
|
||||||
assert!(rendered_choices
|
|
||||||
.into_iter()
|
|
||||||
.any(|x| ExtensionResult::Single(x.to_owned()) == output));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,272 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use log::{error, warn};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
pub struct ScriptExtension {}
|
|
||||||
|
|
||||||
impl ScriptExtension {
|
|
||||||
pub fn new() -> ScriptExtension {
|
|
||||||
ScriptExtension {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for ScriptExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
String::from("script")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
user_args: &Vec<String>,
|
|
||||||
vars: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let args = params.get(&Value::from("args"));
|
|
||||||
if args.is_none() {
|
|
||||||
warn!("No 'args' parameter specified for script variable");
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
let args = args.unwrap().as_sequence();
|
|
||||||
if let Some(args) = args {
|
|
||||||
let mut str_args = args
|
|
||||||
.iter()
|
|
||||||
.map(|arg| arg.as_str().unwrap_or_default().to_string())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
// The user has to enable argument concatenation explicitly
|
|
||||||
let inject_args = params
|
|
||||||
.get(&Value::from("inject_args"))
|
|
||||||
.unwrap_or(&Value::from(false))
|
|
||||||
.as_bool()
|
|
||||||
.unwrap_or(false);
|
|
||||||
if inject_args {
|
|
||||||
str_args.extend(user_args.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace %HOME% with current user home directory to
|
|
||||||
// create cross-platform paths. See issue #265
|
|
||||||
// Also replace %CONFIG% and %PACKAGES% path. See issue #380
|
|
||||||
let home_dir = dirs::home_dir().unwrap_or_default();
|
|
||||||
str_args.iter_mut().for_each(|arg| {
|
|
||||||
if arg.contains("%HOME%") {
|
|
||||||
*arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string());
|
|
||||||
}
|
|
||||||
if arg.contains("%CONFIG%") {
|
|
||||||
*arg = arg.replace(
|
|
||||||
"%CONFIG%",
|
|
||||||
&crate::context::get_config_dir()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if arg.contains("%PACKAGES%") {
|
|
||||||
*arg = arg.replace(
|
|
||||||
"%PACKAGES%",
|
|
||||||
&crate::context::get_package_dir()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Windows, correct paths separators
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
let path = PathBuf::from(&arg);
|
|
||||||
if path.exists() {
|
|
||||||
*arg = path.to_string_lossy().to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut command = Command::new(&str_args[0]);
|
|
||||||
|
|
||||||
// Set the OS-specific flags
|
|
||||||
crate::utils::set_command_flags(&mut command);
|
|
||||||
|
|
||||||
// Inject the $CONFIG variable
|
|
||||||
command.env("CONFIG", crate::context::get_config_dir());
|
|
||||||
|
|
||||||
// Inject all the env variables
|
|
||||||
let env_variables = super::utils::convert_to_env_variables(&vars);
|
|
||||||
for (key, value) in env_variables.iter() {
|
|
||||||
command.env(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = if str_args.len() > 1 {
|
|
||||||
command.args(&str_args[1..]).output()
|
|
||||||
} else {
|
|
||||||
command.output()
|
|
||||||
};
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(output) => {
|
|
||||||
let mut output_str =
|
|
||||||
String::from_utf8_lossy(output.stdout.as_slice()).to_string();
|
|
||||||
let error_str = String::from_utf8_lossy(output.stderr.as_slice());
|
|
||||||
let error_str = error_str.to_string();
|
|
||||||
let error_str = error_str.trim();
|
|
||||||
|
|
||||||
// Print stderror if present
|
|
||||||
if !error_str.is_empty() {
|
|
||||||
warn!("Script command reported error: \n{}", error_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If specified, trim the output
|
|
||||||
let trim_opt = params.get(&Value::from("trim"));
|
|
||||||
let should_trim = if let Some(value) = trim_opt {
|
|
||||||
let val = value.as_bool();
|
|
||||||
val.unwrap_or(true)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_trim {
|
|
||||||
output_str = output_str.trim().to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Some(ExtensionResult::Single(output_str)));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not execute script '{:?}', error: {}", args, e);
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error!("Could not execute script with args '{:?}'", args);
|
|
||||||
Err(super::ExtensionError::Internal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::extension::Extension;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_script_basic() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("args"),
|
|
||||||
Value::from(vec!["echo", "hello world"]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new()).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_script_basic_no_trim() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("args"),
|
|
||||||
Value::from(vec!["echo", "hello world"]),
|
|
||||||
);
|
|
||||||
params.insert(Value::from("trim"), Value::from(false));
|
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new()).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world\n".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_script_inject_args_off() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("args"),
|
|
||||||
Value::from(vec!["echo", "hello world"]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
|
||||||
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_script_inject_args_on() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("args"),
|
|
||||||
Value::from(vec!["echo", "hello world"]),
|
|
||||||
);
|
|
||||||
params.insert(Value::from("inject_args"), Value::from(true));
|
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
|
||||||
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world jon".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_script_var_injection() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("args"),
|
|
||||||
Value::from(vec!["bash", "-c", "echo $ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
|
||||||
let mut subvars = HashMap::new();
|
|
||||||
subvars.insert("name".to_owned(), "John".to_owned());
|
|
||||||
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
|
||||||
vars.insert(
|
|
||||||
"var1".to_owned(),
|
|
||||||
ExtensionResult::Single("hello".to_owned()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
|
||||||
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello John".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,470 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use regex::{Captures, Regex};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::process::{Command, Output};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref UNIX_POS_ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)").unwrap();
|
|
||||||
static ref WIN_POS_ARG_REGEX: Regex = Regex::new("%(?P<pos>\\d+)").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Shell {
|
|
||||||
Cmd,
|
|
||||||
Powershell,
|
|
||||||
WSL,
|
|
||||||
WSL2,
|
|
||||||
Bash,
|
|
||||||
Sh,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shell {
|
|
||||||
fn execute_cmd(&self, cmd: &str, vars: &HashMap<String, String>) -> std::io::Result<Output> {
|
|
||||||
let mut is_wsl = false;
|
|
||||||
|
|
||||||
let mut command = match self {
|
|
||||||
Shell::Cmd => {
|
|
||||||
let mut command = Command::new("cmd");
|
|
||||||
command.args(&["/C", &cmd]);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
Shell::Powershell => {
|
|
||||||
let mut command = Command::new("powershell");
|
|
||||||
command.args(&["-Command", &cmd]);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
Shell::WSL => {
|
|
||||||
is_wsl = true;
|
|
||||||
let mut command = Command::new("bash");
|
|
||||||
command.args(&["-c", &cmd]);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
Shell::WSL2 => {
|
|
||||||
is_wsl = true;
|
|
||||||
let mut command = Command::new("wsl");
|
|
||||||
command.args(&["bash", "-c", &cmd]);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
Shell::Bash => {
|
|
||||||
let mut command = Command::new("bash");
|
|
||||||
command.args(&["-c", &cmd]);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
Shell::Sh => {
|
|
||||||
let mut command = Command::new("sh");
|
|
||||||
command.args(&["-c", &cmd]);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set the OS-specific flags
|
|
||||||
crate::utils::set_command_flags(&mut command);
|
|
||||||
|
|
||||||
// Inject the $CONFIG variable
|
|
||||||
command.env("CONFIG", crate::context::get_config_dir());
|
|
||||||
|
|
||||||
// Inject all the previous variables
|
|
||||||
for (key, value) in vars.iter() {
|
|
||||||
command.env(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In WSL environment, we have to specify which ENV variables
|
|
||||||
// should be passed to linux.
|
|
||||||
// For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
|
|
||||||
if is_wsl {
|
|
||||||
let mut tokens: Vec<&str> = Vec::new();
|
|
||||||
tokens.push("CONFIG/p");
|
|
||||||
|
|
||||||
// Add all the previous variables
|
|
||||||
for (key, _) in vars.iter() {
|
|
||||||
tokens.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let wsl_env = tokens.join(":");
|
|
||||||
command.env("WSLENV", wsl_env);
|
|
||||||
}
|
|
||||||
|
|
||||||
command.output()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_string(shell: &str) -> Option<Shell> {
|
|
||||||
match shell {
|
|
||||||
"cmd" => Some(Shell::Cmd),
|
|
||||||
"powershell" => Some(Shell::Powershell),
|
|
||||||
"wsl" => Some(Shell::WSL),
|
|
||||||
"wsl2" => Some(Shell::WSL2),
|
|
||||||
"bash" => Some(Shell::Bash),
|
|
||||||
"sh" => Some(Shell::Sh),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_arg_regex(&self) -> &Regex {
|
|
||||||
let regex = match self {
|
|
||||||
Shell::Cmd | Shell::Powershell => &*WIN_POS_ARG_REGEX,
|
|
||||||
_ => &*UNIX_POS_ARG_REGEX,
|
|
||||||
};
|
|
||||||
regex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Shell {
|
|
||||||
fn default() -> Shell {
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
Shell::Powershell
|
|
||||||
} else if cfg!(target_os = "macos") {
|
|
||||||
Shell::Sh
|
|
||||||
} else if cfg!(target_os = "linux") {
|
|
||||||
Shell::Bash
|
|
||||||
} else {
|
|
||||||
panic!("invalid target os for shell")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ShellExtension {}
|
|
||||||
|
|
||||||
impl ShellExtension {
|
|
||||||
pub fn new() -> ShellExtension {
|
|
||||||
ShellExtension {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for ShellExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
String::from("shell")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
args: &Vec<String>,
|
|
||||||
vars: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let cmd = params.get(&Value::from("cmd"));
|
|
||||||
if cmd.is_none() {
|
|
||||||
warn!("No 'cmd' parameter specified for shell variable");
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
|
|
||||||
let inject_args = params
|
|
||||||
.get(&Value::from("inject_args"))
|
|
||||||
.unwrap_or(&Value::from(false))
|
|
||||||
.as_bool()
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
let original_cmd = cmd.unwrap().as_str().unwrap();
|
|
||||||
|
|
||||||
let shell_param = params.get(&Value::from("shell"));
|
|
||||||
let shell = if let Some(shell_param) = shell_param {
|
|
||||||
let shell_param = shell_param.as_str().expect("invalid shell parameter");
|
|
||||||
let shell = Shell::from_string(shell_param);
|
|
||||||
|
|
||||||
if shell.is_none() {
|
|
||||||
error!("Invalid shell parameter, please select a valid one.");
|
|
||||||
return Err(super::ExtensionError::Internal);
|
|
||||||
}
|
|
||||||
|
|
||||||
shell.unwrap()
|
|
||||||
} else {
|
|
||||||
Shell::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render positional parameters in args
|
|
||||||
let cmd = if inject_args {
|
|
||||||
shell
|
|
||||||
.get_arg_regex()
|
|
||||||
.replace_all(&original_cmd, |caps: &Captures| {
|
|
||||||
let position_str = caps.name("pos").unwrap().as_str();
|
|
||||||
let position = position_str.parse::<i32>().unwrap_or(-1);
|
|
||||||
if position >= 0 && position < args.len() as i32 {
|
|
||||||
args[position as usize].to_owned()
|
|
||||||
} else {
|
|
||||||
"".to_owned()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
original_cmd.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let env_variables = super::utils::convert_to_env_variables(&vars);
|
|
||||||
|
|
||||||
let output = shell.execute_cmd(&cmd, &env_variables);
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(output) => {
|
|
||||||
let output_str = String::from_utf8_lossy(output.stdout.as_slice());
|
|
||||||
let mut output_str = output_str.into_owned();
|
|
||||||
let error_str = String::from_utf8_lossy(output.stderr.as_slice());
|
|
||||||
let error_str = error_str.to_string();
|
|
||||||
let error_str = error_str.trim();
|
|
||||||
|
|
||||||
// Print stderror if present
|
|
||||||
if !error_str.is_empty() {
|
|
||||||
warn!("Shell command reported error: \n{}", error_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if debug flag set, provide additional context when an error occurs.
|
|
||||||
let debug_opt = params.get(&Value::from("debug"));
|
|
||||||
let with_debug = if let Some(value) = debug_opt {
|
|
||||||
let val = value.as_bool();
|
|
||||||
val.unwrap_or(false)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if with_debug {
|
|
||||||
info!(
|
|
||||||
"debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'",
|
|
||||||
original_cmd, output.status, output_str, error_str
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If specified, trim the output
|
|
||||||
let trim_opt = params.get(&Value::from("trim"));
|
|
||||||
let should_trim = if let Some(value) = trim_opt {
|
|
||||||
let val = value.as_bool();
|
|
||||||
val.unwrap_or(true)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_trim {
|
|
||||||
output_str = output_str.trim().to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(ExtensionResult::Single(output_str)))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not execute cmd '{}', error: {}", cmd, e);
|
|
||||||
Err(super::ExtensionError::Internal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::extension::Extension;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shell_not_trimmed() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
|
||||||
params.insert(Value::from("trim"), Value::from(false));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec![], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world\r\n".to_owned())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world\n".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shell_basic() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec![], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shell_trimmed_2() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("cmd"),
|
|
||||||
Value::from("echo \" hello world \""),
|
|
||||||
);
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec![], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shell_trimmed_malformed() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
|
||||||
params.insert(Value::from("trim"), Value::from("error"));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec![], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_shell_pipes() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo hello world | cat"));
|
|
||||||
params.insert(Value::from("trim"), Value::from(true));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec![], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("hello world".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_shell_args_unix() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo $0"));
|
|
||||||
params.insert(Value::from("inject_args"), Value::from(true));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn test_shell_no_default_inject_args_unix() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(
|
|
||||||
Value::from("cmd"),
|
|
||||||
Value::from("echo 'hey friend' | awk '{ print $2 }'"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
output.unwrap(),
|
|
||||||
ExtensionResult::Single("friend".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn test_shell_args_windows() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo %0"));
|
|
||||||
params.insert(Value::from("inject_args"), Value::from(true));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let output = extension
|
|
||||||
.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shell_vars_single_injection() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_VAR1%"));
|
|
||||||
params.insert(Value::from("shell"), Value::from("cmd"));
|
|
||||||
} else {
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_VAR1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
|
||||||
vars.insert(
|
|
||||||
"var1".to_owned(),
|
|
||||||
ExtensionResult::Single("hello".to_owned()),
|
|
||||||
);
|
|
||||||
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shell_vars_multiple_injection() {
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_FORM1_NAME%"));
|
|
||||||
params.insert(Value::from("shell"), Value::from("cmd"));
|
|
||||||
} else {
|
|
||||||
params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_FORM1_NAME"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
|
||||||
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
|
||||||
let mut subvars = HashMap::new();
|
|
||||||
subvars.insert("name".to_owned(), "John".to_owned());
|
|
||||||
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
|
||||||
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
|
||||||
|
|
||||||
assert!(output.is_some());
|
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub fn convert_to_env_variables(
|
|
||||||
original_vars: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> HashMap<String, String> {
|
|
||||||
let mut output = HashMap::new();
|
|
||||||
|
|
||||||
for (key, result) in original_vars.iter() {
|
|
||||||
match result {
|
|
||||||
ExtensionResult::Single(value) => {
|
|
||||||
let name = format!("ESPANSO_{}", key.to_uppercase());
|
|
||||||
output.insert(name, value.clone());
|
|
||||||
}
|
|
||||||
ExtensionResult::Multiple(values) => {
|
|
||||||
for (sub_key, sub_value) in values.iter() {
|
|
||||||
let name = format!("ESPANSO_{}_{}", key.to_uppercase(), sub_key.to_uppercase());
|
|
||||||
output.insert(name, sub_value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::extension::Extension;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_convert_to_env_variables() {
|
|
||||||
let mut vars: HashMap<String, ExtensionResult> = HashMap::new();
|
|
||||||
let mut subvars = HashMap::new();
|
|
||||||
subvars.insert("name".to_owned(), "John".to_owned());
|
|
||||||
subvars.insert("lastname".to_owned(), "Snow".to_owned());
|
|
||||||
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
|
||||||
vars.insert(
|
|
||||||
"var1".to_owned(),
|
|
||||||
ExtensionResult::Single("test".to_owned()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = convert_to_env_variables(&vars);
|
|
||||||
assert_eq!(output.get("ESPANSO_FORM1_NAME").unwrap(), "John");
|
|
||||||
assert_eq!(output.get("ESPANSO_FORM1_LASTNAME").unwrap(), "Snow");
|
|
||||||
assert_eq!(output.get("ESPANSO_VAR1").unwrap(), "test");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::extension::ExtensionResult;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct VarDummyExtension {}
|
|
||||||
|
|
||||||
impl VarDummyExtension {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Extension for VarDummyExtension {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"vardummy".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate(
|
|
||||||
&self,
|
|
||||||
params: &Mapping,
|
|
||||||
_: &Vec<String>,
|
|
||||||
vars: &HashMap<String, ExtensionResult>,
|
|
||||||
) -> super::ExtensionOut {
|
|
||||||
let target = params.get(&Value::from("target"));
|
|
||||||
|
|
||||||
if let Some(target) = target {
|
|
||||||
let value = vars.get(target.as_str().unwrap_or_default());
|
|
||||||
Ok(Some(value.unwrap().clone()))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
56
src/guard.rs
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::config::Configs;
|
|
||||||
use log::debug;
|
|
||||||
use std::sync::atomic::Ordering::Release;
|
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
|
||||||
|
|
||||||
pub struct InjectGuard {
|
|
||||||
is_injecting: Arc<AtomicBool>,
|
|
||||||
post_inject_delay: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InjectGuard {
|
|
||||||
pub fn new(is_injecting: Arc<AtomicBool>, config: &Configs) -> Self {
|
|
||||||
debug!("enabling inject guard");
|
|
||||||
|
|
||||||
// Enable the injecting block
|
|
||||||
is_injecting.store(true, Release);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
is_injecting,
|
|
||||||
post_inject_delay: config.post_inject_delay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for InjectGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// Because the keyinjection is async, we need to wait a bit before
|
|
||||||
// giving back the control. Otherwise, the injected actions will be handled back
|
|
||||||
// by espanso itself.
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(self.post_inject_delay));
|
|
||||||
|
|
||||||
debug!("releasing inject guard");
|
|
||||||
|
|
||||||
// Re-allow espanso to interpret actions
|
|
||||||
self.is_injecting.store(false, Release);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::PasteShortcut;
|
|
||||||
use crate::bridge::linux::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use log::error;
|
|
||||||
use std::ffi::CString;
|
|
||||||
|
|
||||||
pub struct LinuxKeyboardManager {}
|
|
||||||
|
|
||||||
impl super::KeyboardManager for LinuxKeyboardManager {
|
|
||||||
fn send_string(&self, active_config: &Configs, s: &str) {
|
|
||||||
let res = CString::new(s);
|
|
||||||
match res {
|
|
||||||
Ok(cstr) => unsafe {
|
|
||||||
if active_config.fast_inject {
|
|
||||||
fast_send_string(cstr.as_ptr(), active_config.inject_delay);
|
|
||||||
} else {
|
|
||||||
send_string(cstr.as_ptr());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => panic!(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_enter(&self, active_config: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
if active_config.fast_inject {
|
|
||||||
fast_send_enter();
|
|
||||||
} else {
|
|
||||||
send_enter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_paste(&self, active_config: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
match active_config.paste_shortcut {
|
|
||||||
PasteShortcut::Default => {
|
|
||||||
let is_special = is_current_window_special();
|
|
||||||
|
|
||||||
// Terminals use a different keyboard combination to paste from clipboard,
|
|
||||||
// so we need to check the correct situation.
|
|
||||||
if is_special == 0 {
|
|
||||||
trigger_paste();
|
|
||||||
}else if is_special == 2 { // Special case for stterm
|
|
||||||
trigger_alt_shift_ins_paste();
|
|
||||||
}else if is_special == 3 { // Special case for Emacs
|
|
||||||
trigger_shift_ins_paste();
|
|
||||||
}else if is_special == 4 { // CTRL+ALT+V used in some terminals (urxvt)
|
|
||||||
trigger_ctrl_alt_paste();
|
|
||||||
}else{
|
|
||||||
trigger_terminal_paste();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PasteShortcut::CtrlV => {
|
|
||||||
trigger_paste();
|
|
||||||
},
|
|
||||||
PasteShortcut::CtrlShiftV => {
|
|
||||||
trigger_terminal_paste();
|
|
||||||
},
|
|
||||||
PasteShortcut::ShiftInsert=> {
|
|
||||||
trigger_shift_ins_paste();
|
|
||||||
},
|
|
||||||
PasteShortcut::CtrlAltV => {
|
|
||||||
trigger_ctrl_alt_paste();
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_string(&self, active_config: &Configs, count: i32) {
|
|
||||||
unsafe {
|
|
||||||
if active_config.fast_inject {
|
|
||||||
fast_delete_string(count, active_config.backspace_delay);
|
|
||||||
} else {
|
|
||||||
delete_string(count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_left(&self, active_config: &Configs, count: i32) {
|
|
||||||
unsafe {
|
|
||||||
if active_config.fast_inject {
|
|
||||||
fast_left_arrow(count);
|
|
||||||
} else {
|
|
||||||
left_arrow(count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_copy(&self, _: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
trigger_copy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::PasteShortcut;
|
|
||||||
use crate::bridge::macos::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use log::error;
|
|
||||||
use std::ffi::CString;
|
|
||||||
|
|
||||||
pub struct MacKeyboardManager {}
|
|
||||||
|
|
||||||
impl super::KeyboardManager for MacKeyboardManager {
|
|
||||||
fn send_string(&self, _: &Configs, s: &str) {
|
|
||||||
let res = CString::new(s);
|
|
||||||
match res {
|
|
||||||
Ok(cstr) => unsafe {
|
|
||||||
send_string(cstr.as_ptr());
|
|
||||||
},
|
|
||||||
Err(e) => panic!(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_enter(&self, _: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
// Send the kVK_Return key press
|
|
||||||
send_vkey(0x24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_paste(&self, active_config: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
match active_config.paste_shortcut {
|
|
||||||
PasteShortcut::Default => {
|
|
||||||
unsafe {
|
|
||||||
trigger_paste();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
error!("MacOS backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_copy(&self, _: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
trigger_copy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_string(&self, _: &Configs, count: i32) {
|
|
||||||
unsafe { delete_string(count) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_left(&self, _: &Configs, count: i32) {
|
|
||||||
unsafe {
|
|
||||||
// Simulate the Left arrow count times
|
|
||||||
send_multi_vkey(0x7B, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wait_for_modifiers_release() -> bool {
|
|
||||||
let start = std::time::SystemTime::now();
|
|
||||||
while start.elapsed().unwrap_or_default().as_millis() < 3000 {
|
|
||||||
let pressed = unsafe { crate::bridge::macos::are_modifiers_pressed() };
|
|
||||||
if pressed == 0 {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::config::Configs;
|
|
||||||
use log::warn;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub mod windows;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
mod linux;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub mod macos;
|
|
||||||
|
|
||||||
pub trait KeyboardManager {
|
|
||||||
fn send_string(&self, active_config: &Configs, s: &str);
|
|
||||||
fn send_enter(&self, active_config: &Configs);
|
|
||||||
fn trigger_paste(&self, active_config: &Configs);
|
|
||||||
fn delete_string(&self, active_config: &Configs, count: i32);
|
|
||||||
fn move_cursor_left(&self, active_config: &Configs, count: i32);
|
|
||||||
fn trigger_copy(&self, active_config: &Configs);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub enum PasteShortcut {
|
|
||||||
Default, // Default one for the current system
|
|
||||||
CtrlV, // Classic Ctrl+V shortcut
|
|
||||||
CtrlShiftV, // Could be used to paste without formatting in many applications
|
|
||||||
ShiftInsert, // Often used in Linux systems
|
|
||||||
CtrlAltV, // Used in some Linux terminals (urxvt)
|
|
||||||
MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PasteShortcut {
|
|
||||||
fn default() -> Self {
|
|
||||||
PasteShortcut::Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn get_manager() -> impl KeyboardManager {
|
|
||||||
windows::WindowsKeyboardManager {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LINUX IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn get_manager() -> impl KeyboardManager {
|
|
||||||
linux::LinuxKeyboardManager {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAC IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn get_manager() -> impl KeyboardManager {
|
|
||||||
macos::MacKeyboardManager {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// These methods are used to wait until all modifiers are released (or timeout occurs)
|
|
||||||
pub fn wait_for_modifiers_release() {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let released = crate::keyboard::windows::wait_for_modifiers_release();
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let released = crate::keyboard::macos::wait_for_modifiers_release();
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let released = true; // NOOP on linux (at least for now)
|
|
||||||
|
|
||||||
if !released {
|
|
||||||
warn!("Wait for modifiers release timed out! Please release your modifiers keys (CTRL, CMD, ALT, SHIFT) after typing the trigger");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::PasteShortcut;
|
|
||||||
use crate::bridge::windows::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use log::error;
|
|
||||||
use widestring::U16CString;
|
|
||||||
|
|
||||||
pub struct WindowsKeyboardManager {}
|
|
||||||
|
|
||||||
impl super::KeyboardManager for WindowsKeyboardManager {
|
|
||||||
fn send_string(&self, _: &Configs, s: &str) {
|
|
||||||
let res = U16CString::from_str(s);
|
|
||||||
match res {
|
|
||||||
Ok(s) => unsafe {
|
|
||||||
send_string(s.as_ptr());
|
|
||||||
},
|
|
||||||
Err(e) => println!("Error while sending string: {}", e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_enter(&self, _: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
// Send the VK_RETURN key press
|
|
||||||
send_vkey(0x0D);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_paste(&self, active_config: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
match active_config.paste_shortcut {
|
|
||||||
PasteShortcut::Default => {
|
|
||||||
unsafe {
|
|
||||||
trigger_paste();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PasteShortcut::CtrlShiftV => {
|
|
||||||
trigger_shift_paste();
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_string(&self, config: &Configs, count: i32) {
|
|
||||||
unsafe { delete_string(count, config.backspace_delay) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_left(&self, _: &Configs, count: i32) {
|
|
||||||
unsafe {
|
|
||||||
// Send the left arrow key multiple times
|
|
||||||
send_multi_vkey(0x25, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_copy(&self, _: &Configs) {
|
|
||||||
unsafe {
|
|
||||||
trigger_copy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wait_for_modifiers_release() -> bool {
|
|
||||||
let start = std::time::SystemTime::now();
|
|
||||||
while start.elapsed().unwrap_or_default().as_millis() < 3000 {
|
|
||||||
let pressed = unsafe { crate::bridge::windows::are_modifiers_pressed() };
|
|
||||||
if pressed == 0 {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
1449
src/main.rs
|
@ -1,801 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espans{ name: (), var_type: (), params: ()}
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::event::KeyEventReceiver;
|
|
||||||
use crate::event::{KeyEvent, KeyModifier};
|
|
||||||
use regex::{Captures, Regex};
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
|
||||||
|
|
||||||
pub(crate) mod scrolling;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
|
||||||
pub struct Match {
|
|
||||||
pub triggers: Vec<String>,
|
|
||||||
pub content: MatchContentType,
|
|
||||||
pub word: bool,
|
|
||||||
pub passive_only: bool,
|
|
||||||
pub propagate_case: bool,
|
|
||||||
pub force_clipboard: bool,
|
|
||||||
pub is_html: bool,
|
|
||||||
|
|
||||||
// Automatically calculated from the triggers, used by the matcher to check for correspondences.
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub _trigger_sequences: Vec<Vec<TriggerEntry>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
|
||||||
pub enum MatchContentType {
|
|
||||||
Text(TextContent),
|
|
||||||
Image(ImageContent),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone, PartialEq)]
|
|
||||||
pub struct TextContent {
|
|
||||||
pub replace: String,
|
|
||||||
pub vars: Vec<MatchVariable>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub _has_vars: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
|
||||||
pub struct ImageContent {
|
|
||||||
pub path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for Match {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let auto_match = AutoMatch::deserialize(deserializer)?;
|
|
||||||
Ok(Match::from(&auto_match))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a AutoMatch> for Match {
|
|
||||||
fn from(other: &'a AutoMatch) -> Self {
|
|
||||||
lazy_static! {
|
|
||||||
static ref VAR_REGEX: Regex =
|
|
||||||
Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut triggers = if !other.triggers.is_empty() {
|
|
||||||
other.triggers.clone()
|
|
||||||
} else if !other.trigger.is_empty() {
|
|
||||||
vec![other.trigger.clone()]
|
|
||||||
} else {
|
|
||||||
panic!("Match does not have any trigger defined: {:?}", other)
|
|
||||||
};
|
|
||||||
|
|
||||||
// If propagate_case is true, we need to generate all the possible triggers
|
|
||||||
// For example, specifying "hello" as a trigger, we need to have:
|
|
||||||
// "hello", "Hello", "HELLO"
|
|
||||||
if other.propagate_case {
|
|
||||||
// List with first letter capitalized
|
|
||||||
let first_capitalized: Vec<String> = triggers
|
|
||||||
.iter()
|
|
||||||
.map(|trigger| {
|
|
||||||
let capitalized = trigger.clone();
|
|
||||||
let mut v: Vec<char> = capitalized.chars().collect();
|
|
||||||
|
|
||||||
// Capitalize the first alphabetic letter
|
|
||||||
// See issue #244
|
|
||||||
let first_alphabetic = v.iter().position(|c| c.is_alphabetic()).unwrap_or(0);
|
|
||||||
|
|
||||||
v[first_alphabetic] = v[first_alphabetic].to_uppercase().nth(0).unwrap();
|
|
||||||
v.into_iter().collect()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let all_capitalized: Vec<String> = triggers
|
|
||||||
.iter()
|
|
||||||
.map(|trigger| trigger.to_uppercase())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
triggers.extend(first_capitalized);
|
|
||||||
triggers.extend(all_capitalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
let trigger_sequences = triggers
|
|
||||||
.iter()
|
|
||||||
.map(|trigger| {
|
|
||||||
// Calculate the trigger sequence
|
|
||||||
let mut trigger_sequence = Vec::new();
|
|
||||||
let trigger_chars: Vec<char> = trigger.chars().collect();
|
|
||||||
trigger_sequence.extend(trigger_chars.into_iter().map(|c| TriggerEntry::Char(c)));
|
|
||||||
if other.word {
|
|
||||||
// If it's a word match, end with a word separator
|
|
||||||
trigger_sequence.push(TriggerEntry::WordSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger_sequence
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let (text_content, is_html) = if let Some(replace) = &other.replace {
|
|
||||||
(Some(Cow::from(replace)), false)
|
|
||||||
} else if let Some(markdown_str) = &other.markdown {
|
|
||||||
// Render the markdown into HTML
|
|
||||||
let mut html = markdown::to_html(markdown_str);
|
|
||||||
html = html.trim().to_owned();
|
|
||||||
|
|
||||||
if !other.paragraph {
|
|
||||||
// Remove the surrounding paragraph
|
|
||||||
if html.starts_with("<p>") {
|
|
||||||
html = html.trim_start_matches("<p>").to_owned();
|
|
||||||
}
|
|
||||||
if html.ends_with("</p>") {
|
|
||||||
html = html.trim_end_matches("</p>").to_owned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Some(Cow::from(html)), true)
|
|
||||||
} else if let Some(html) = &other.html {
|
|
||||||
(Some(Cow::from(html)), true)
|
|
||||||
} else {
|
|
||||||
(None, false)
|
|
||||||
};
|
|
||||||
|
|
||||||
let content = if let Some(content) = text_content {
|
|
||||||
// Check if the match contains variables
|
|
||||||
let has_vars = VAR_REGEX.is_match(&content);
|
|
||||||
|
|
||||||
let content = TextContent {
|
|
||||||
replace: content.to_string(),
|
|
||||||
vars: other.vars.clone(),
|
|
||||||
_has_vars: has_vars,
|
|
||||||
};
|
|
||||||
|
|
||||||
MatchContentType::Text(content)
|
|
||||||
} else if let Some(form) = &other.form {
|
|
||||||
// Form shorthand
|
|
||||||
// Replace all the form fields with actual variables
|
|
||||||
let new_replace = VAR_REGEX.replace_all(&form, |caps: &Captures| {
|
|
||||||
let var_name = caps.get(1).unwrap().as_str();
|
|
||||||
format!("{{{{form1.{}}}}}", var_name)
|
|
||||||
});
|
|
||||||
let new_replace = new_replace.to_string();
|
|
||||||
|
|
||||||
// Convert escaped brakets in forms
|
|
||||||
let form = form.replace("\\{", "{ ").replace("\\}", " }");
|
|
||||||
|
|
||||||
// Convert the form data to valid variables
|
|
||||||
let mut params = Mapping::new();
|
|
||||||
if let Some(fields) = &other.form_fields {
|
|
||||||
let mut mapping_fields = Mapping::new();
|
|
||||||
fields.iter().for_each(|(key, value)| {
|
|
||||||
mapping_fields.insert(Value::from(key.to_owned()), Value::from(value.clone()));
|
|
||||||
});
|
|
||||||
params.insert(Value::from("fields"), Value::from(mapping_fields));
|
|
||||||
}
|
|
||||||
params.insert(Value::from("layout"), Value::from(form));
|
|
||||||
|
|
||||||
let vars = vec![MatchVariable {
|
|
||||||
name: "form1".to_owned(),
|
|
||||||
var_type: "form".to_owned(),
|
|
||||||
params,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let content = TextContent {
|
|
||||||
replace: new_replace,
|
|
||||||
vars,
|
|
||||||
_has_vars: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
MatchContentType::Text(content)
|
|
||||||
} else if let Some(image_path) = &other.image_path {
|
|
||||||
// Image match
|
|
||||||
// On Windows, we have to replace the forward / with the backslash \ in the path
|
|
||||||
let new_path = if cfg!(target_os = "windows") {
|
|
||||||
image_path.replace("/", "\\")
|
|
||||||
} else {
|
|
||||||
image_path.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate variables in path
|
|
||||||
let new_path = if new_path.contains("$CONFIG") {
|
|
||||||
let config_dir = crate::context::get_config_dir();
|
|
||||||
let config_path = fs::canonicalize(&config_dir);
|
|
||||||
let config_path = if let Ok(config_path) = config_path {
|
|
||||||
config_path.to_string_lossy().into_owned()
|
|
||||||
} else {
|
|
||||||
"".to_owned()
|
|
||||||
};
|
|
||||||
new_path.replace("$CONFIG", &config_path)
|
|
||||||
} else {
|
|
||||||
new_path.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let content = ImageContent {
|
|
||||||
path: PathBuf::from(new_path),
|
|
||||||
};
|
|
||||||
|
|
||||||
MatchContentType::Image(content)
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: no action specified for match {}, please specify either 'replace', 'markdown', 'html', image_path' or 'form'", other.trigger);
|
|
||||||
std::process::exit(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
triggers,
|
|
||||||
content,
|
|
||||||
word: other.word,
|
|
||||||
passive_only: other.passive_only,
|
|
||||||
_trigger_sequences: trigger_sequences,
|
|
||||||
propagate_case: other.propagate_case,
|
|
||||||
force_clipboard: other.force_clipboard,
|
|
||||||
is_html,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used to deserialize the Match struct before applying some custom elaboration.
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
struct AutoMatch {
|
|
||||||
#[serde(default = "default_trigger")]
|
|
||||||
pub trigger: String,
|
|
||||||
|
|
||||||
#[serde(default = "default_triggers")]
|
|
||||||
pub triggers: Vec<String>,
|
|
||||||
|
|
||||||
#[serde(default = "default_replace")]
|
|
||||||
pub replace: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default = "default_image_path")]
|
|
||||||
pub image_path: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default = "default_form")]
|
|
||||||
pub form: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default = "default_form_fields")]
|
|
||||||
pub form_fields: Option<HashMap<String, Value>>,
|
|
||||||
|
|
||||||
#[serde(default = "default_vars")]
|
|
||||||
pub vars: Vec<MatchVariable>,
|
|
||||||
|
|
||||||
#[serde(default = "default_word")]
|
|
||||||
pub word: bool,
|
|
||||||
|
|
||||||
#[serde(default = "default_passive_only")]
|
|
||||||
pub passive_only: bool,
|
|
||||||
|
|
||||||
#[serde(default = "default_propagate_case")]
|
|
||||||
pub propagate_case: bool,
|
|
||||||
|
|
||||||
#[serde(default = "default_force_clipboard")]
|
|
||||||
pub force_clipboard: bool,
|
|
||||||
|
|
||||||
#[serde(default = "default_markdown")]
|
|
||||||
pub markdown: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default = "default_paragraph")]
|
|
||||||
pub paragraph: bool,
|
|
||||||
|
|
||||||
#[serde(default = "default_html")]
|
|
||||||
pub html: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_trigger() -> String {
|
|
||||||
"".to_owned()
|
|
||||||
}
|
|
||||||
fn default_triggers() -> Vec<String> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
fn default_vars() -> Vec<MatchVariable> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
fn default_word() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_passive_only() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_replace() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn default_form() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn default_form_fields() -> Option<HashMap<String, Value>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn default_image_path() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn default_propagate_case() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_force_clipboard() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_markdown() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn default_paragraph() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_html() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub struct MatchVariable {
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub var_type: String,
|
|
||||||
|
|
||||||
#[serde(default = "default_params")]
|
|
||||||
pub params: Mapping,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_params() -> Mapping {
|
|
||||||
Mapping::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum TriggerEntry {
|
|
||||||
Char(char),
|
|
||||||
WordSeparator,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MatchReceiver {
|
|
||||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize);
|
|
||||||
fn on_enable_update(&self, status: bool);
|
|
||||||
fn on_passive(&self);
|
|
||||||
fn on_undo(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Matcher: KeyEventReceiver {
|
|
||||||
fn handle_char(&self, c: &str);
|
|
||||||
fn handle_modifier(&self, m: KeyModifier);
|
|
||||||
fn handle_other(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M: Matcher> KeyEventReceiver for M {
|
|
||||||
fn on_key_event(&self, e: KeyEvent) {
|
|
||||||
match e {
|
|
||||||
KeyEvent::Char(c) => {
|
|
||||||
self.handle_char(&c);
|
|
||||||
}
|
|
||||||
KeyEvent::Modifier(m) => {
|
|
||||||
self.handle_modifier(m);
|
|
||||||
}
|
|
||||||
KeyEvent::Other => {
|
|
||||||
self.handle_other();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTS
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_has_vars_should_be_false() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
replace: "There are no variables"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(content._has_vars, false);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_has_vars_should_be_true() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
replace: "There are {{one}} and {{two}} variables"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(content._has_vars, true);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_has_vars_with_spaces_should_be_true() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
replace: "There is {{ one }} variable"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(content._has_vars, true);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_trigger_sequence_without_word() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: "test"
|
|
||||||
replace: "This is a test"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_trigger_sequence_with_word() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: "test"
|
|
||||||
replace: "This is a test"
|
|
||||||
word: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_with_image_content() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: "test"
|
|
||||||
image_path: "/path/to/file"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Image(content) => {
|
|
||||||
assert_eq!(content.path, PathBuf::from("/path/to/file"));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_trigger_populates_triggers_vector() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
replace: "This is a test"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec![":test"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_triggers_are_correctly_parsed() {
|
|
||||||
let match_str = r###"
|
|
||||||
triggers:
|
|
||||||
- ":test1"
|
|
||||||
- :test2
|
|
||||||
replace: "This is a test"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec![":test1", ":test2"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_triggers_are_correctly_parsed_square_brackets() {
|
|
||||||
let match_str = r###"
|
|
||||||
triggers: [":test1", ":test2"]
|
|
||||||
replace: "This is a test"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec![":test1", ":test2"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_propagate_case() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: "hello"
|
|
||||||
replace: "This is a test"
|
|
||||||
propagate_case: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec!["hello", "Hello", "HELLO"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_propagate_case_multi_trigger() {
|
|
||||||
let match_str = r###"
|
|
||||||
triggers: ["hello", "hi"]
|
|
||||||
replace: "This is a test"
|
|
||||||
propagate_case: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
_match.triggers,
|
|
||||||
vec!["hello", "hi", "Hello", "Hi", "HELLO", "HI"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_trigger_sequence_with_word_propagate_case() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: "test"
|
|
||||||
replace: "This is a test"
|
|
||||||
word: true
|
|
||||||
propagate_case: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
|
|
||||||
assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
|
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequences[1][0], TriggerEntry::Char('T'));
|
|
||||||
assert_eq!(_match._trigger_sequences[1][1], TriggerEntry::Char('e'));
|
|
||||||
assert_eq!(_match._trigger_sequences[1][2], TriggerEntry::Char('s'));
|
|
||||||
assert_eq!(_match._trigger_sequences[1][3], TriggerEntry::Char('t'));
|
|
||||||
assert_eq!(_match._trigger_sequences[1][4], TriggerEntry::WordSeparator);
|
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequences[2][0], TriggerEntry::Char('T'));
|
|
||||||
assert_eq!(_match._trigger_sequences[2][1], TriggerEntry::Char('E'));
|
|
||||||
assert_eq!(_match._trigger_sequences[2][2], TriggerEntry::Char('S'));
|
|
||||||
assert_eq!(_match._trigger_sequences[2][3], TriggerEntry::Char('T'));
|
|
||||||
assert_eq!(_match._trigger_sequences[2][4], TriggerEntry::WordSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_empty_replace_doesnt_crash() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: "hello"
|
|
||||||
replace: ""
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_propagate_case_with_prefix_symbol() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":hello"
|
|
||||||
replace: "This is a test"
|
|
||||||
propagate_case: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec![":hello", ":Hello", ":HELLO"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_propagate_case_non_alphabetic_should_not_crash() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":.."
|
|
||||||
replace: "This is a test"
|
|
||||||
propagate_case: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(_match.triggers, vec![":..", ":..", ":.."])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_form_translated_correctly() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
form: "Hey {{name}}, how are you? {{greet}}"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
let mut mapping = Mapping::new();
|
|
||||||
mapping.insert(
|
|
||||||
Value::from("layout"),
|
|
||||||
Value::from("Hey {{name}}, how are you? {{greet}}"),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
content,
|
|
||||||
TextContent {
|
|
||||||
replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(),
|
|
||||||
_has_vars: true,
|
|
||||||
vars: vec![MatchVariable {
|
|
||||||
name: "form1".to_owned(),
|
|
||||||
var_type: "form".to_owned(),
|
|
||||||
params: mapping,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => panic!("wrong content"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_form_with_fields_translated_correctly() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
form: "Hey {{name}}, how are you? {{greet}}"
|
|
||||||
form_fields:
|
|
||||||
name:
|
|
||||||
multiline: true
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
let mut name_mapping = Mapping::new();
|
|
||||||
name_mapping.insert(Value::from("multiline"), Value::Bool(true));
|
|
||||||
let mut submapping = Mapping::new();
|
|
||||||
submapping.insert(Value::from("name"), Value::from(name_mapping));
|
|
||||||
let mut mapping = Mapping::new();
|
|
||||||
mapping.insert(Value::from("fields"), Value::from(submapping));
|
|
||||||
mapping.insert(
|
|
||||||
Value::from("layout"),
|
|
||||||
Value::from("Hey {{name}}, how are you? {{greet}}"),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
content,
|
|
||||||
TextContent {
|
|
||||||
replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(),
|
|
||||||
_has_vars: true,
|
|
||||||
vars: vec![MatchVariable {
|
|
||||||
name: "form1".to_owned(),
|
|
||||||
var_type: "form".to_owned(),
|
|
||||||
params: mapping,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => panic!("wrong content"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_markdown_loaded_correctly() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
markdown: "This *text* is **very bold**"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(
|
|
||||||
content.replace,
|
|
||||||
"This <em>text</em> is <strong>very bold</strong>"
|
|
||||||
);
|
|
||||||
assert_eq!(_match.is_html, true);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_markdown_keep_vars() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
markdown: "This *text* is {{variable}} **very bold**"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(
|
|
||||||
content.replace,
|
|
||||||
"This <em>text</em> is {{variable}} <strong>very bold</strong>"
|
|
||||||
);
|
|
||||||
assert_eq!(_match.is_html, true);
|
|
||||||
assert_eq!(content._has_vars, true);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_html_loaded_correctly() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
html: "This <i>text<i> is <b>very bold</b>"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(content.replace, "This <i>text<i> is <b>very bold</b>");
|
|
||||||
assert_eq!(_match.is_html, true);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_html_keep_vars() {
|
|
||||||
let match_str = r###"
|
|
||||||
trigger: ":test"
|
|
||||||
html: "This <i>text<i> is {{var}} <b>very bold</b>"
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
|
||||||
|
|
||||||
match _match.content {
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
assert_eq!(
|
|
||||||
content.replace,
|
|
||||||
"This <i>text<i> is {{var}} <b>very bold</b>"
|
|
||||||
);
|
|
||||||
assert_eq!(_match.is_html, true);
|
|
||||||
assert_eq!(content._has_vars, true);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,318 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::config::ConfigManager;
|
|
||||||
use crate::event::KeyModifier::{BACKSPACE, CAPS_LOCK, LEFT_SHIFT, RIGHT_SHIFT};
|
|
||||||
use crate::event::{ActionEventReceiver, ActionType, KeyModifier};
|
|
||||||
use crate::matcher::{Match, MatchReceiver, TriggerEntry};
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
|
||||||
config_manager: &'a M,
|
|
||||||
receiver: &'a R,
|
|
||||||
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
|
||||||
toggle_press_time: RefCell<SystemTime>,
|
|
||||||
passive_press_time: RefCell<SystemTime>,
|
|
||||||
is_enabled: RefCell<bool>,
|
|
||||||
was_previous_char_word_separator: RefCell<bool>,
|
|
||||||
was_previous_char_a_match: RefCell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MatchEntry<'a> {
|
|
||||||
start: usize,
|
|
||||||
count: usize,
|
|
||||||
trigger_offset: usize, // The index of the trigger in the Match that matched
|
|
||||||
_match: &'a Match,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
|
||||||
pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> {
|
|
||||||
let current_set_queue = RefCell::new(VecDeque::new());
|
|
||||||
let toggle_press_time = RefCell::new(SystemTime::now());
|
|
||||||
let passive_press_time = RefCell::new(SystemTime::now());
|
|
||||||
|
|
||||||
ScrollingMatcher {
|
|
||||||
config_manager,
|
|
||||||
receiver,
|
|
||||||
current_set_queue,
|
|
||||||
toggle_press_time,
|
|
||||||
passive_press_time,
|
|
||||||
is_enabled: RefCell::new(true),
|
|
||||||
was_previous_char_word_separator: RefCell::new(true),
|
|
||||||
was_previous_char_a_match: RefCell::new(true),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle(&self) {
|
|
||||||
let mut is_enabled = self.is_enabled.borrow_mut();
|
|
||||||
*is_enabled = !(*is_enabled);
|
|
||||||
|
|
||||||
self.receiver.on_enable_update(*is_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_enabled(&self, enabled: bool) {
|
|
||||||
let mut is_enabled = self.is_enabled.borrow_mut();
|
|
||||||
*is_enabled = enabled;
|
|
||||||
|
|
||||||
self.receiver.on_enable_update(*is_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_matching(
|
|
||||||
mtc: &Match,
|
|
||||||
current_char: &str,
|
|
||||||
start: usize,
|
|
||||||
trigger_offset: usize,
|
|
||||||
is_current_word_separator: bool,
|
|
||||||
) -> bool {
|
|
||||||
match mtc._trigger_sequences[trigger_offset][start] {
|
|
||||||
TriggerEntry::Char(c) => current_char.starts_with(c),
|
|
||||||
TriggerEntry::WordSeparator => is_current_word_separator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> {
|
|
||||||
fn handle_char(&self, c: &str) {
|
|
||||||
// if not enabled, avoid any processing
|
|
||||||
if !*(self.is_enabled.borrow()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain the configuration for the active application if present,
|
|
||||||
// otherwise get the default one
|
|
||||||
let active_config = self.config_manager.active_config();
|
|
||||||
|
|
||||||
// Check if the current char is a word separator
|
|
||||||
let mut is_current_word_separator = active_config
|
|
||||||
.word_separators
|
|
||||||
.contains(&c.chars().nth(0).unwrap_or_default());
|
|
||||||
|
|
||||||
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
|
||||||
(*was_previous_char_a_match) = false;
|
|
||||||
|
|
||||||
let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
|
|
||||||
|
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
|
||||||
|
|
||||||
let mut new_matches: Vec<MatchEntry> = Vec::new();
|
|
||||||
|
|
||||||
for m in active_config.matches.iter() {
|
|
||||||
// only active-enabled matches are considered
|
|
||||||
if m.passive_only {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for trigger_offset in 0..m._trigger_sequences.len() {
|
|
||||||
let mut result =
|
|
||||||
Self::is_matching(m, c, 0, trigger_offset, is_current_word_separator);
|
|
||||||
|
|
||||||
if m.word {
|
|
||||||
result = result && *was_previous_word_separator
|
|
||||||
}
|
|
||||||
|
|
||||||
if result {
|
|
||||||
new_matches.push(MatchEntry {
|
|
||||||
start: 1,
|
|
||||||
count: m._trigger_sequences[trigger_offset].len(),
|
|
||||||
trigger_offset,
|
|
||||||
_match: &m,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
|
|
||||||
|
|
||||||
let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
|
|
||||||
Some(last_matches) => {
|
|
||||||
let mut updated: Vec<MatchEntry> = last_matches
|
|
||||||
.iter()
|
|
||||||
.filter(|&x| {
|
|
||||||
Self::is_matching(
|
|
||||||
x._match,
|
|
||||||
c,
|
|
||||||
x.start,
|
|
||||||
x.trigger_offset,
|
|
||||||
is_current_word_separator,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|x| MatchEntry {
|
|
||||||
start: x.start + 1,
|
|
||||||
count: x.count,
|
|
||||||
trigger_offset: x.trigger_offset,
|
|
||||||
_match: &x._match,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
updated.extend(new_matches);
|
|
||||||
updated
|
|
||||||
}
|
|
||||||
None => new_matches,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut found_entry = None;
|
|
||||||
|
|
||||||
for entry in combined_matches.iter() {
|
|
||||||
if entry.start == entry.count {
|
|
||||||
found_entry = Some(entry.clone());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
current_set_queue.push_back(combined_matches);
|
|
||||||
|
|
||||||
if current_set_queue.len() as i32
|
|
||||||
> (self.config_manager.default_config().backspace_limit + 1)
|
|
||||||
{
|
|
||||||
current_set_queue.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
*was_previous_word_separator = is_current_word_separator;
|
|
||||||
|
|
||||||
if let Some(entry) = found_entry {
|
|
||||||
let mtc = entry._match;
|
|
||||||
|
|
||||||
current_set_queue.clear();
|
|
||||||
|
|
||||||
let trailing_separator = if !mtc.word {
|
|
||||||
// If it's not a word match, it cannot have a trailing separator
|
|
||||||
None
|
|
||||||
} else if !is_current_word_separator {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let as_char = c.chars().nth(0);
|
|
||||||
match as_char {
|
|
||||||
Some(c) => {
|
|
||||||
Some(c) // Current char is the trailing separator
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Force espanso to consider the last char as a separator
|
|
||||||
*was_previous_word_separator = true;
|
|
||||||
|
|
||||||
self.receiver
|
|
||||||
.on_match(mtc, trailing_separator, entry.trigger_offset);
|
|
||||||
|
|
||||||
(*was_previous_char_a_match) = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_modifier(&self, m: KeyModifier) {
|
|
||||||
let config = self.config_manager.default_config();
|
|
||||||
|
|
||||||
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
|
||||||
|
|
||||||
// TODO: at the moment, activating the passive key triggers the toggle key
|
|
||||||
// study a mechanism to avoid this problem
|
|
||||||
|
|
||||||
if KeyModifier::shallow_equals(&m, &config.toggle_key) {
|
|
||||||
check_interval(
|
|
||||||
&self.toggle_press_time,
|
|
||||||
u128::from(config.toggle_interval),
|
|
||||||
|| {
|
|
||||||
self.toggle();
|
|
||||||
|
|
||||||
let is_enabled = self.is_enabled.borrow();
|
|
||||||
|
|
||||||
if !*is_enabled {
|
|
||||||
self.current_set_queue.borrow_mut().clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if KeyModifier::shallow_equals(&m, &config.passive_key) {
|
|
||||||
check_interval(
|
|
||||||
&self.passive_press_time,
|
|
||||||
u128::from(config.toggle_interval),
|
|
||||||
|| {
|
|
||||||
self.receiver.on_passive();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backspace handling, basically "rewinding history"
|
|
||||||
if m == BACKSPACE {
|
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
|
||||||
current_set_queue.pop_back();
|
|
||||||
|
|
||||||
if (*was_previous_char_a_match) {
|
|
||||||
current_set_queue.clear();
|
|
||||||
self.receiver.on_undo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable the "backspace undo" feature
|
|
||||||
(*was_previous_char_a_match) = false;
|
|
||||||
|
|
||||||
// Consider modifiers as separators to improve word matches reliability
|
|
||||||
if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
|
|
||||||
let mut was_previous_char_word_separator =
|
|
||||||
self.was_previous_char_word_separator.borrow_mut();
|
|
||||||
*was_previous_char_word_separator = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_other(&self) {
|
|
||||||
// When receiving "other" type of events, we mark them as valid separators.
|
|
||||||
// This dramatically improves the reliability of word matches
|
|
||||||
let mut was_previous_char_word_separator =
|
|
||||||
self.was_previous_char_word_separator.borrow_mut();
|
|
||||||
*was_previous_char_word_separator = true;
|
|
||||||
|
|
||||||
// Disable the "backspace undo" feature
|
|
||||||
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
|
||||||
(*was_previous_char_a_match) = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver
|
|
||||||
for ScrollingMatcher<'a, R, M>
|
|
||||||
{
|
|
||||||
fn on_action_event(&self, e: ActionType) {
|
|
||||||
match e {
|
|
||||||
ActionType::Toggle => {
|
|
||||||
self.toggle();
|
|
||||||
}
|
|
||||||
ActionType::Enable => {
|
|
||||||
self.set_enabled(true);
|
|
||||||
}
|
|
||||||
ActionType::Disable => {
|
|
||||||
self.set_enabled(false);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_callback: F)
|
|
||||||
where
|
|
||||||
F: Fn(),
|
|
||||||
{
|
|
||||||
let mut press_time = state_var.borrow_mut();
|
|
||||||
if let Ok(elapsed) = press_time.elapsed() {
|
|
||||||
if elapsed.as_millis() < interval {
|
|
||||||
elapsed_callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(*press_time) = SystemTime::now();
|
|
||||||
}
|
|
|
@ -1,794 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::package::InstallResult::{AlreadyInstalled, BlockedExternalPackage, NotFoundInIndex};
|
|
||||||
use crate::package::RemoveResult::Removed;
|
|
||||||
use crate::package::UpdateResult::{NotOutdated, Updated};
|
|
||||||
use crate::package::{
|
|
||||||
InstallResult, Package, PackageIndex, PackageResolver, RemoveResult, UpdateResult,
|
|
||||||
};
|
|
||||||
use regex::Regex;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fs;
|
|
||||||
use std::fs::{create_dir, File};
|
|
||||||
use std::io::{BufRead, BufReader};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
const DEFAULT_PACKAGE_INDEX_FILE: &str = "package_index.json";
|
|
||||||
|
|
||||||
pub struct DefaultPackageManager {
|
|
||||||
package_dir: PathBuf,
|
|
||||||
data_dir: PathBuf,
|
|
||||||
|
|
||||||
package_resolver: Option<Box<dyn PackageResolver>>,
|
|
||||||
|
|
||||||
local_index: Option<PackageIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultPackageManager {
|
|
||||||
pub fn new(
|
|
||||||
package_dir: PathBuf,
|
|
||||||
data_dir: PathBuf,
|
|
||||||
package_resolver: Option<Box<dyn PackageResolver>>,
|
|
||||||
) -> DefaultPackageManager {
|
|
||||||
let local_index = Self::load_local_index(&data_dir);
|
|
||||||
|
|
||||||
DefaultPackageManager {
|
|
||||||
package_dir,
|
|
||||||
data_dir,
|
|
||||||
package_resolver,
|
|
||||||
local_index,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_default(
|
|
||||||
package_resolver: Option<Box<dyn PackageResolver>>,
|
|
||||||
) -> DefaultPackageManager {
|
|
||||||
DefaultPackageManager::new(
|
|
||||||
crate::context::get_package_dir(),
|
|
||||||
crate::context::get_data_dir(),
|
|
||||||
package_resolver,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_package_index_path(data_dir: &Path) -> PathBuf {
|
|
||||||
data_dir.join(DEFAULT_PACKAGE_INDEX_FILE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_local_index(data_dir: &Path) -> Option<super::PackageIndex> {
|
|
||||||
let local_index_file = File::open(Self::get_package_index_path(data_dir));
|
|
||||||
if let Ok(local_index_file) = local_index_file {
|
|
||||||
let reader = BufReader::new(local_index_file);
|
|
||||||
let local_index = serde_json::from_reader(reader);
|
|
||||||
|
|
||||||
if let Ok(local_index) = local_index {
|
|
||||||
return local_index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_index() -> Result<super::PackageIndex, Box<dyn Error>> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let request = client
|
|
||||||
.get("https://hub.espanso.org/json/")
|
|
||||||
.header("User-Agent", format!("espanso/{}", crate::VERSION));
|
|
||||||
|
|
||||||
let mut res = request.send()?;
|
|
||||||
let body = res.text()?;
|
|
||||||
let index: PackageIndex = serde_json::from_str(&body)?;
|
|
||||||
|
|
||||||
Ok(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_package_from_readme(readme_path: &Path) -> Option<Package> {
|
|
||||||
lazy_static! {
|
|
||||||
static ref FIELD_REGEX: Regex =
|
|
||||||
Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read readme line by line
|
|
||||||
let file = File::open(readme_path);
|
|
||||||
if let Ok(file) = file {
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let mut fields: HashMap<String, String> = HashMap::new();
|
|
||||||
|
|
||||||
let mut started = false;
|
|
||||||
|
|
||||||
for (_index, line) in reader.lines().enumerate() {
|
|
||||||
let line = line.unwrap();
|
|
||||||
if line.contains("---") {
|
|
||||||
if started {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
} else if started {
|
|
||||||
let caps = FIELD_REGEX.captures(&line);
|
|
||||||
if let Some(caps) = caps {
|
|
||||||
let property = caps.get(1);
|
|
||||||
let value = caps.get(2);
|
|
||||||
if property.is_some() && value.is_some() {
|
|
||||||
fields.insert(
|
|
||||||
property.unwrap().as_str().to_owned(),
|
|
||||||
value.unwrap().as_str().to_owned(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fields.contains_key("package_name")
|
|
||||||
|| !fields.contains_key("package_title")
|
|
||||||
|| !fields.contains_key("package_version")
|
|
||||||
|| !fields.contains_key("package_repo")
|
|
||||||
|| !fields.contains_key("package_desc")
|
|
||||||
|| !fields.contains_key("package_author")
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let original_repo = if fields.contains_key("package_original_repo") {
|
|
||||||
fields.get("package_original_repo").unwrap().clone()
|
|
||||||
} else {
|
|
||||||
fields.get("package_repo").unwrap().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_core = if fields.contains_key("is_core") {
|
|
||||||
match fields.get("is_core").unwrap().clone().as_ref() {
|
|
||||||
"true" => true,
|
|
||||||
"false" => false,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let package = Package {
|
|
||||||
name: fields.get("package_name").unwrap().clone(),
|
|
||||||
title: fields.get("package_title").unwrap().clone(),
|
|
||||||
version: fields.get("package_version").unwrap().clone(),
|
|
||||||
repo: fields.get("package_repo").unwrap().clone(),
|
|
||||||
desc: fields.get("package_desc").unwrap().clone(),
|
|
||||||
author: fields.get("package_author").unwrap().clone(),
|
|
||||||
is_core,
|
|
||||||
original_repo,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(package)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_index_timestamp(&self) -> u64 {
|
|
||||||
if let Some(local_index) = &self.local_index {
|
|
||||||
return local_index.last_update;
|
|
||||||
}
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_local_packages_names(&self) -> Vec<String> {
|
|
||||||
let dir = fs::read_dir(&self.package_dir);
|
|
||||||
let mut output = Vec::new();
|
|
||||||
if let Ok(dir) = dir {
|
|
||||||
for entry in dir {
|
|
||||||
if let Ok(entry) = entry {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
let name = path.file_name();
|
|
||||||
if let Some(name) = name {
|
|
||||||
output.push(name.to_str().unwrap().to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cache_local_index(&self) {
|
|
||||||
if let Some(local_index) = &self.local_index {
|
|
||||||
let serialized =
|
|
||||||
serde_json::to_string(local_index).expect("Unable to serialize local index");
|
|
||||||
let local_index_file = self.data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(local_index_file, serialized).expect("Unable to cache local index");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::PackageManager for DefaultPackageManager {
|
|
||||||
fn is_index_outdated(&self) -> bool {
|
|
||||||
let current_time = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("Time went backwards");
|
|
||||||
let current_timestamp = current_time.as_secs();
|
|
||||||
|
|
||||||
let local_index_timestamp = self.local_index_timestamp();
|
|
||||||
|
|
||||||
// Local index is outdated if older than a day
|
|
||||||
local_index_timestamp + 60 * 60 * 24 < current_timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>> {
|
|
||||||
if force || self.is_index_outdated() {
|
|
||||||
let updated_index = DefaultPackageManager::request_index()?;
|
|
||||||
self.local_index = Some(updated_index);
|
|
||||||
|
|
||||||
// Save the index to file
|
|
||||||
self.cache_local_index();
|
|
||||||
|
|
||||||
Ok(Updated)
|
|
||||||
} else {
|
|
||||||
Ok(NotOutdated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_package(&self, name: &str) -> Option<Package> {
|
|
||||||
if let Some(local_index) = &self.local_index {
|
|
||||||
let result = local_index
|
|
||||||
.packages
|
|
||||||
.iter()
|
|
||||||
.find(|package| package.name == name);
|
|
||||||
if let Some(package) = result {
|
|
||||||
return Some(package.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_package(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
allow_external: bool,
|
|
||||||
proxy: Option<String>,
|
|
||||||
) -> Result<InstallResult, Box<dyn Error>> {
|
|
||||||
let package = self.get_package(name);
|
|
||||||
match package {
|
|
||||||
Some(package) => {
|
|
||||||
if package.is_core || allow_external {
|
|
||||||
self.install_package_from_repo(name, &package.repo, proxy)
|
|
||||||
} else {
|
|
||||||
Ok(BlockedExternalPackage(package.original_repo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Ok(NotFoundInIndex),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_package_from_repo(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
repo_url: &str,
|
|
||||||
proxy: Option<String>,
|
|
||||||
) -> Result<InstallResult, Box<dyn Error>> {
|
|
||||||
// Check if package is already installed
|
|
||||||
let packages = self.list_local_packages_names();
|
|
||||||
if packages.iter().any(|p| p == name) {
|
|
||||||
// Package already installed
|
|
||||||
return Ok(AlreadyInstalled);
|
|
||||||
}
|
|
||||||
|
|
||||||
let temp_dir = self
|
|
||||||
.package_resolver
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.clone_repo_to_temp(repo_url, proxy)?;
|
|
||||||
|
|
||||||
let temp_package_dir = temp_dir.path().join(name);
|
|
||||||
if !temp_package_dir.exists() {
|
|
||||||
return Ok(InstallResult::NotFoundInRepo);
|
|
||||||
}
|
|
||||||
|
|
||||||
let readme_path = temp_package_dir.join("README.md");
|
|
||||||
|
|
||||||
let package = Self::parse_package_from_readme(&readme_path);
|
|
||||||
if package.is_none() {
|
|
||||||
return Ok(InstallResult::UnableToParsePackageInfo);
|
|
||||||
}
|
|
||||||
let package = package.unwrap();
|
|
||||||
|
|
||||||
let source_dir = temp_package_dir.join(package.version);
|
|
||||||
if !source_dir.exists() {
|
|
||||||
return Ok(InstallResult::MissingPackageVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_dir = &self.package_dir.join(name);
|
|
||||||
create_dir(&target_dir)?;
|
|
||||||
|
|
||||||
crate::utils::copy_dir(&source_dir, target_dir)?;
|
|
||||||
|
|
||||||
let readme_dest = target_dir.join("README.md");
|
|
||||||
std::fs::copy(readme_path, readme_dest)?;
|
|
||||||
|
|
||||||
Ok(InstallResult::Installed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>> {
|
|
||||||
let package_dir = self.package_dir.join(name);
|
|
||||||
if !package_dir.exists() {
|
|
||||||
return Ok(RemoveResult::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::remove_dir_all(package_dir)?;
|
|
||||||
|
|
||||||
Ok(Removed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_local_packages(&self) -> Vec<Package> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
|
|
||||||
let package_names = self.list_local_packages_names();
|
|
||||||
|
|
||||||
for name in package_names.iter() {
|
|
||||||
let package_dir = &self.package_dir.join(name);
|
|
||||||
let readme_file = package_dir.join("README.md");
|
|
||||||
let package = Self::parse_package_from_readme(&readme_file);
|
|
||||||
if let Some(package) = package {
|
|
||||||
output.push(package);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::package::zip::ZipPackageResolver;
|
|
||||||
use crate::package::InstallResult::*;
|
|
||||||
use crate::package::PackageManager;
|
|
||||||
use std::fs::{create_dir, create_dir_all};
|
|
||||||
use std::path::Path;
|
|
||||||
use tempfile::{NamedTempFile, TempDir};
|
|
||||||
|
|
||||||
const OUTDATED_INDEX_CONTENT: &str = include_str!("../res/test/outdated_index.json");
|
|
||||||
const INDEX_CONTENT_WITHOUT_UPDATE: &str =
|
|
||||||
include_str!("../res/test/index_without_update.json");
|
|
||||||
const GET_PACKAGE_INDEX: &str = include_str!("../res/test/get_package_index.json");
|
|
||||||
const INSTALL_PACKAGE_INDEX: &str = include_str!("../res/test/install_package_index.json");
|
|
||||||
|
|
||||||
struct TempPackageManager {
|
|
||||||
package_dir: TempDir,
|
|
||||||
data_dir: TempDir,
|
|
||||||
package_manager: DefaultPackageManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_temp_package_manager<F>(setup: F) -> TempPackageManager
|
|
||||||
where
|
|
||||||
F: Fn(&Path, &Path) -> (),
|
|
||||||
{
|
|
||||||
let package_dir = TempDir::new().expect("unable to create temp directory");
|
|
||||||
let data_dir = TempDir::new().expect("unable to create temp directory");
|
|
||||||
|
|
||||||
setup(package_dir.path(), data_dir.path());
|
|
||||||
|
|
||||||
let package_manager = DefaultPackageManager::new(
|
|
||||||
package_dir.path().clone().to_path_buf(),
|
|
||||||
data_dir.path().clone().to_path_buf(),
|
|
||||||
Some(Box::new(ZipPackageResolver::new())),
|
|
||||||
);
|
|
||||||
|
|
||||||
TempPackageManager {
|
|
||||||
package_dir,
|
|
||||||
data_dir,
|
|
||||||
package_manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_download_index() {
|
|
||||||
create_temp_package_manager(|_, _| {});
|
|
||||||
let index = DefaultPackageManager::request_index();
|
|
||||||
|
|
||||||
assert!(index.is_ok());
|
|
||||||
assert!(index.unwrap().packages.len() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_outdated_index() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(temp.package_manager.is_index_outdated());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_up_to_date_index_should_not_be_updated() {
|
|
||||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
let current_time = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("Time went backwards");
|
|
||||||
let current_timestamp = current_time.as_secs();
|
|
||||||
let new_contents =
|
|
||||||
INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
|
|
||||||
std::fs::write(index_file, new_contents).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager.update_index(false).unwrap(),
|
|
||||||
UpdateResult::NotOutdated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_up_to_date_index_with_force_should_be_updated() {
|
|
||||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
let current_time = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("Time went backwards");
|
|
||||||
let current_timestamp = current_time.as_secs();
|
|
||||||
let new_contents =
|
|
||||||
INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
|
|
||||||
std::fs::write(index_file, new_contents).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager.update_index(true).unwrap(),
|
|
||||||
UpdateResult::Updated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_outdated_index_should_be_updated() {
|
|
||||||
let mut temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, OUTDATED_INDEX_CONTENT).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager.update_index(false).unwrap(),
|
|
||||||
UpdateResult::Updated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_index_should_create_file() {
|
|
||||||
let mut temp = create_temp_package_manager(|_, _| {});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager.update_index(false).unwrap(),
|
|
||||||
UpdateResult::Updated
|
|
||||||
);
|
|
||||||
assert!(temp
|
|
||||||
.data_dir
|
|
||||||
.path()
|
|
||||||
.join(DEFAULT_PACKAGE_INDEX_FILE)
|
|
||||||
.exists())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_package_should_be_found() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.get_package("italian-accents")
|
|
||||||
.unwrap()
|
|
||||||
.title,
|
|
||||||
"Italian Accents"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_package_should_not_be_found() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, GET_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(temp.package_manager.get_package("not-existing").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_list_local_packages_names() {
|
|
||||||
let temp = create_temp_package_manager(|package_dir, _| {
|
|
||||||
create_dir(package_dir.join("package-1")).unwrap();
|
|
||||||
create_dir(package_dir.join("package2")).unwrap();
|
|
||||||
std::fs::write(package_dir.join("dummyfile.txt"), "test").unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
let packages = temp.package_manager.list_local_packages_names();
|
|
||||||
assert_eq!(packages.len(), 2);
|
|
||||||
assert!(packages.iter().any(|p| p == "package-1"));
|
|
||||||
assert!(packages.iter().any(|p| p == "package2"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package_not_found() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("doesnotexist", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
NotFoundInIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package_already_installed() {
|
|
||||||
let temp = create_temp_package_manager(|package_dir, data_dir| {
|
|
||||||
create_dir(package_dir.join("italian-accents")).unwrap();
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("italian-accents", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
AlreadyInstalled
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("dummy-package", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
Installed
|
|
||||||
);
|
|
||||||
assert!(temp.package_dir.path().join("dummy-package").exists());
|
|
||||||
assert!(temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/README.md")
|
|
||||||
.exists());
|
|
||||||
assert!(temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/package.yml")
|
|
||||||
.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package_does_not_exist_in_repo() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("not-existing", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
NotFoundInRepo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package_missing_version() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("dummy-package2", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
MissingPackageVersion
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package_missing_readme_unable_to_parse_package_info() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("dummy-package3", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
UnableToParsePackageInfo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_install_package_bad_readme_unable_to_parse_package_info() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("dummy-package4", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
UnableToParsePackageInfo
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_list_local_packages() {
|
|
||||||
let temp = create_temp_package_manager(|_, data_dir| {
|
|
||||||
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
|
||||||
std::fs::write(index_file, INSTALL_PACKAGE_INDEX).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.install_package("dummy-package", false, None)
|
|
||||||
.unwrap(),
|
|
||||||
Installed
|
|
||||||
);
|
|
||||||
assert!(temp.package_dir.path().join("dummy-package").exists());
|
|
||||||
assert!(temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/README.md")
|
|
||||||
.exists());
|
|
||||||
assert!(temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/package.yml")
|
|
||||||
.exists());
|
|
||||||
|
|
||||||
let list = temp.package_manager.list_local_packages();
|
|
||||||
assert_eq!(list.len(), 1);
|
|
||||||
assert_eq!(list[0].name, "dummy-package");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_package() {
|
|
||||||
let temp = create_temp_package_manager(|package_dir, _| {
|
|
||||||
let dummy_package_dir = package_dir.join("dummy-package");
|
|
||||||
create_dir_all(&dummy_package_dir).unwrap();
|
|
||||||
std::fs::write(dummy_package_dir.join("README.md"), "readme").unwrap();
|
|
||||||
std::fs::write(dummy_package_dir.join("package.yml"), "name: package").unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(temp.package_dir.path().join("dummy-package").exists());
|
|
||||||
assert!(temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/README.md")
|
|
||||||
.exists());
|
|
||||||
assert!(temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/package.yml")
|
|
||||||
.exists());
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager
|
|
||||||
.remove_package("dummy-package")
|
|
||||||
.unwrap(),
|
|
||||||
RemoveResult::Removed
|
|
||||||
);
|
|
||||||
assert!(!temp.package_dir.path().join("dummy-package").exists());
|
|
||||||
assert!(!temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/README.md")
|
|
||||||
.exists());
|
|
||||||
assert!(!temp
|
|
||||||
.package_dir
|
|
||||||
.path()
|
|
||||||
.join("dummy-package/package.yml")
|
|
||||||
.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_package_not_found() {
|
|
||||||
let temp = create_temp_package_manager(|_, _| {});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
temp.package_manager.remove_package("not-existing").unwrap(),
|
|
||||||
RemoveResult::NotFound
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_package_from_readme() {
|
|
||||||
let file = NamedTempFile::new().unwrap();
|
|
||||||
fs::write(
|
|
||||||
file.path(),
|
|
||||||
r###"
|
|
||||||
---
|
|
||||||
package_name: "italian-accents"
|
|
||||||
package_title: "Italian Accents"
|
|
||||||
package_desc: "Include Italian accents substitutions to espanso."
|
|
||||||
package_version: "0.1.0"
|
|
||||||
package_author: "Federico Terzi"
|
|
||||||
package_repo: "https://github.com/federico-terzi/espanso-hub-core"
|
|
||||||
is_core: true
|
|
||||||
---
|
|
||||||
"###,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
|
|
||||||
|
|
||||||
let target_package = Package {
|
|
||||||
name: "italian-accents".to_string(),
|
|
||||||
title: "Italian Accents".to_string(),
|
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
|
|
||||||
desc: "Include Italian accents substitutions to espanso.".to_string(),
|
|
||||||
author: "Federico Terzi".to_string(),
|
|
||||||
original_repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
|
|
||||||
is_core: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(package, target_package);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_package_from_readme_with_bad_metadata() {
|
|
||||||
let file = NamedTempFile::new().unwrap();
|
|
||||||
fs::write(
|
|
||||||
file.path(),
|
|
||||||
r###"
|
|
||||||
---
|
|
||||||
package_name: italian-accents
|
|
||||||
package_title: "Italian Accents"
|
|
||||||
package_desc: "Include Italian accents substitutions to espanso."
|
|
||||||
package_version:"0.1.0"
|
|
||||||
package_author:Federico Terzi
|
|
||||||
package_repo: "https://github.com/federico-terzi/espanso-hub-core"
|
|
||||||
is_core: true
|
|
||||||
---
|
|
||||||
Readme text
|
|
||||||
"###,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
|
|
||||||
|
|
||||||
let target_package = Package {
|
|
||||||
name: "italian-accents".to_string(),
|
|
||||||
title: "Italian Accents".to_string(),
|
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
|
|
||||||
desc: "Include Italian accents substitutions to espanso.".to_string(),
|
|
||||||
author: "Federico Terzi".to_string(),
|
|
||||||
original_repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
|
|
||||||
is_core: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(package, target_package);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub(crate) mod default;
|
|
||||||
pub(crate) mod zip;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::error::Error;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
pub trait PackageManager {
|
|
||||||
fn is_index_outdated(&self) -> bool;
|
|
||||||
fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>>;
|
|
||||||
|
|
||||||
fn get_package(&self, name: &str) -> Option<Package>;
|
|
||||||
|
|
||||||
fn install_package(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
allow_external: bool,
|
|
||||||
proxy: Option<String>,
|
|
||||||
) -> Result<InstallResult, Box<dyn Error>>;
|
|
||||||
fn install_package_from_repo(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
repo_url: &str,
|
|
||||||
proxy: Option<String>,
|
|
||||||
) -> Result<InstallResult, Box<dyn Error>>;
|
|
||||||
|
|
||||||
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
|
|
||||||
|
|
||||||
fn list_local_packages(&self) -> Vec<Package>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PackageResolver {
|
|
||||||
fn clone_repo_to_temp(
|
|
||||||
&self,
|
|
||||||
repo_url: &str,
|
|
||||||
proxy: Option<String>,
|
|
||||||
) -> Result<TempDir, Box<dyn Error>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct Package {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
pub version: String,
|
|
||||||
pub repo: String,
|
|
||||||
pub desc: String,
|
|
||||||
pub author: String,
|
|
||||||
|
|
||||||
#[serde(default = "default_is_core")]
|
|
||||||
pub is_core: bool,
|
|
||||||
#[serde(default = "default_original_repo")]
|
|
||||||
pub original_repo: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_is_core() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
fn default_original_repo() -> String {
|
|
||||||
"".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct PackageIndex {
|
|
||||||
#[serde(rename = "lastUpdate")]
|
|
||||||
pub last_update: u64,
|
|
||||||
|
|
||||||
pub packages: Vec<Package>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum UpdateResult {
|
|
||||||
NotOutdated,
|
|
||||||
Updated,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum InstallResult {
|
|
||||||
NotFoundInIndex,
|
|
||||||
NotFoundInRepo,
|
|
||||||
UnableToParsePackageInfo,
|
|
||||||
MissingPackageVersion,
|
|
||||||
AlreadyInstalled,
|
|
||||||
Installed,
|
|
||||||
BlockedExternalPackage(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum RemoveResult {
|
|
||||||
NotFound,
|
|
||||||
Removed,
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::io::{copy, Cursor};
|
|
||||||
use std::{fs, io};
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
pub struct ZipPackageResolver;
|
|
||||||
|
|
||||||
impl ZipPackageResolver {
|
|
||||||
pub fn new() -> ZipPackageResolver {
|
|
||||||
return ZipPackageResolver {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::PackageResolver for ZipPackageResolver {
|
|
||||||
fn clone_repo_to_temp(
|
|
||||||
&self,
|
|
||||||
repo_url: &str,
|
|
||||||
proxy: Option<String>,
|
|
||||||
) -> Result<TempDir, Box<dyn Error>> {
|
|
||||||
let temp_dir = TempDir::new()?;
|
|
||||||
|
|
||||||
let zip_url = repo_url.to_owned() + "/archive/master.zip";
|
|
||||||
|
|
||||||
let mut client = reqwest::Client::builder();
|
|
||||||
|
|
||||||
if let Some(proxy) = proxy {
|
|
||||||
let proxy = reqwest::Proxy::https(&proxy).expect("unable to setup https proxy");
|
|
||||||
client = client.proxy(proxy);
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = client.build().expect("unable to create http client");
|
|
||||||
|
|
||||||
// Download the archive from GitHub
|
|
||||||
let mut response = client.get(&zip_url).send()?;
|
|
||||||
|
|
||||||
// Extract zip file
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
copy(&mut response, &mut buffer)?;
|
|
||||||
|
|
||||||
let reader = Cursor::new(buffer);
|
|
||||||
|
|
||||||
let mut archive = zip::ZipArchive::new(reader).unwrap();
|
|
||||||
|
|
||||||
// Find the root folder name
|
|
||||||
let mut root_folder = {
|
|
||||||
let root_folder = archive.by_index(0).unwrap();
|
|
||||||
let root_folder = root_folder.sanitized_name();
|
|
||||||
root_folder.to_str().unwrap().to_owned()
|
|
||||||
};
|
|
||||||
root_folder.push(std::path::MAIN_SEPARATOR);
|
|
||||||
|
|
||||||
for i in 1..archive.len() {
|
|
||||||
let mut file = archive.by_index(i).unwrap();
|
|
||||||
|
|
||||||
let current_path = file.sanitized_name();
|
|
||||||
let current_filename = current_path.to_str().unwrap();
|
|
||||||
let trimmed_filename = current_filename.trim_start_matches(&root_folder);
|
|
||||||
|
|
||||||
let outpath = temp_dir.path().join(trimmed_filename);
|
|
||||||
|
|
||||||
{
|
|
||||||
let comment = file.comment();
|
|
||||||
if !comment.is_empty() {
|
|
||||||
debug!("File {} comment: {}", i, comment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (&*file.name()).ends_with('/') {
|
|
||||||
debug!(
|
|
||||||
"File {} extracted to \"{}\"",
|
|
||||||
i,
|
|
||||||
outpath.as_path().display()
|
|
||||||
);
|
|
||||||
fs::create_dir_all(&outpath).unwrap();
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
"File {} extracted to \"{}\" ({} bytes)",
|
|
||||||
i,
|
|
||||||
outpath.as_path().display(),
|
|
||||||
file.size()
|
|
||||||
);
|
|
||||||
if let Some(p) = outpath.parent() {
|
|
||||||
if !p.exists() {
|
|
||||||
fs::create_dir_all(&p).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut outfile = fs::File::create(&outpath).unwrap();
|
|
||||||
io::copy(&mut file, &mut outfile).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(temp_dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::super::PackageResolver;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_clone_temp_repository() {
|
|
||||||
let resolver = ZipPackageResolver::new();
|
|
||||||
let cloned_dir = resolver
|
|
||||||
.clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core", None)
|
|
||||||
.unwrap();
|
|
||||||
assert!(cloned_dir.path().join("LICENSE").exists());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
use std::io;
|
|
||||||
use std::process::{Child, Command, Stdio};
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
Command::new(cmd)
|
|
||||||
.creation_flags(0x08000008) // Detached Process without window
|
|
||||||
.args(args)
|
|
||||||
.spawn()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
|
|
||||||
Command::new(cmd).args(args).spawn()
|
|
||||||
}
|
|
|
@ -1,224 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::event::ActionType;
|
|
||||||
use crate::event::{Event, SystemEvent};
|
|
||||||
use log::error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::io::{BufReader, Read, Write};
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
mod windows;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
mod unix;
|
|
||||||
|
|
||||||
pub trait IPCServer {
|
|
||||||
fn start(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IPCClient {
|
|
||||||
fn send_command(&self, command: IPCCommand) -> Result<(), String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_command_or_warn(service: Service, configs: Configs, command: IPCCommand) {
|
|
||||||
let ipc_client = get_ipc_client(service, configs);
|
|
||||||
if let Err(e) = ipc_client.send_command(command) {
|
|
||||||
error!("unable to send command to IPC server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct IPCCommand {
|
|
||||||
pub id: String,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub payload: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IPCCommand {
|
|
||||||
fn to_event(&self) -> Option<Event> {
|
|
||||||
match self.id.as_ref() {
|
|
||||||
"exit" => Some(Event::Action(ActionType::Exit)),
|
|
||||||
"wexit" => Some(Event::Action(ActionType::ExitWorker)),
|
|
||||||
"toggle" => Some(Event::Action(ActionType::Toggle)),
|
|
||||||
"enable" => Some(Event::Action(ActionType::Enable)),
|
|
||||||
"disable" => Some(Event::Action(ActionType::Disable)),
|
|
||||||
"restartworker" => Some(Event::Action(ActionType::RestartWorker)),
|
|
||||||
"notify" => Some(Event::System(SystemEvent::NotifyRequest(
|
|
||||||
self.payload.clone(),
|
|
||||||
))),
|
|
||||||
"trigger" => Some(Event::System(SystemEvent::Trigger(self.payload.clone()))),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from(event: Event) -> Option<IPCCommand> {
|
|
||||||
match event {
|
|
||||||
Event::Action(ActionType::Exit) => Some(IPCCommand {
|
|
||||||
id: "exit".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}),
|
|
||||||
Event::Action(ActionType::ExitWorker) => Some(IPCCommand {
|
|
||||||
id: "wexit".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}),
|
|
||||||
Event::Action(ActionType::Toggle) => Some(IPCCommand {
|
|
||||||
id: "toggle".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}),
|
|
||||||
Event::Action(ActionType::Enable) => Some(IPCCommand {
|
|
||||||
id: "enable".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}),
|
|
||||||
Event::Action(ActionType::Disable) => Some(IPCCommand {
|
|
||||||
id: "disable".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}),
|
|
||||||
Event::Action(ActionType::RestartWorker) => Some(IPCCommand {
|
|
||||||
id: "restartworker".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}),
|
|
||||||
Event::System(SystemEvent::NotifyRequest(message)) => Some(IPCCommand {
|
|
||||||
id: "notify".to_owned(),
|
|
||||||
payload: message,
|
|
||||||
}),
|
|
||||||
Event::System(SystemEvent::Trigger(trigger)) => Some(IPCCommand {
|
|
||||||
id: "trigger".to_owned(),
|
|
||||||
payload: trigger,
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exit() -> IPCCommand {
|
|
||||||
Self {
|
|
||||||
id: "exit".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exit_worker() -> IPCCommand {
|
|
||||||
Self {
|
|
||||||
id: "wexit".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restart_worker() -> IPCCommand {
|
|
||||||
Self {
|
|
||||||
id: "restartworker".to_owned(),
|
|
||||||
payload: "".to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger(trigger: &str) -> IPCCommand {
|
|
||||||
Self {
|
|
||||||
id: "trigger".to_owned(),
|
|
||||||
payload: trigger.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) {
|
|
||||||
match stream {
|
|
||||||
Ok(stream) => {
|
|
||||||
let mut json_str = String::new();
|
|
||||||
let mut buf_reader = BufReader::new(stream);
|
|
||||||
let res = buf_reader.read_to_string(&mut json_str);
|
|
||||||
|
|
||||||
if res.is_ok() {
|
|
||||||
let command: Result<IPCCommand, serde_json::Error> =
|
|
||||||
serde_json::from_str(&json_str);
|
|
||||||
match command {
|
|
||||||
Ok(command) => {
|
|
||||||
let event = command.to_event();
|
|
||||||
if let Some(event) = event {
|
|
||||||
event_channel.send(event).expect("Broken event channel");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error deserializing JSON command: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("Error: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_command<W: Write, E: Error>(
|
|
||||||
command: IPCCommand,
|
|
||||||
stream: Result<W, E>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
match stream {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
let json_str = serde_json::to_string(&command);
|
|
||||||
if let Ok(json_str) = json_str {
|
|
||||||
stream.write_all(json_str.as_bytes()).unwrap_or_else(|e| {
|
|
||||||
println!("Can't write to IPC socket: {}", e);
|
|
||||||
});
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => return Err(format!("Can't connect to daemon: {}", e)),
|
|
||||||
}
|
|
||||||
|
|
||||||
Err("Can't send command".to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Service {
|
|
||||||
Daemon,
|
|
||||||
Worker,
|
|
||||||
}
|
|
||||||
|
|
||||||
// UNIX IMPLEMENTATION
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn get_ipc_server(
|
|
||||||
service: Service,
|
|
||||||
_: Configs,
|
|
||||||
event_channel: Sender<Event>,
|
|
||||||
) -> impl IPCServer {
|
|
||||||
unix::UnixIPCServer::new(service, event_channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
|
|
||||||
unix::UnixIPCClient::new(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn get_ipc_server(
|
|
||||||
service: Service,
|
|
||||||
_: Configs,
|
|
||||||
event_channel: Sender<Event>,
|
|
||||||
) -> impl IPCServer {
|
|
||||||
windows::WindowsIPCServer::new(service, event_channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
|
|
||||||
windows::WindowsIPCClient::new(service)
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::IPCCommand;
|
|
||||||
use log::{info, warn};
|
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
|
|
||||||
use super::Service;
|
|
||||||
use crate::context;
|
|
||||||
use crate::event::*;
|
|
||||||
use crate::protocol::{process_event, send_command};
|
|
||||||
|
|
||||||
const DAEMON_UNIX_SOCKET_NAME: &str = "espanso.sock";
|
|
||||||
const WORKER_UNIX_SOCKET_NAME: &str = "worker.sock";
|
|
||||||
|
|
||||||
pub struct UnixIPCServer {
|
|
||||||
service: Service,
|
|
||||||
event_channel: Sender<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UnixIPCServer {
|
|
||||||
pub fn new(service: Service, event_channel: Sender<Event>) -> UnixIPCServer {
|
|
||||||
UnixIPCServer {
|
|
||||||
service,
|
|
||||||
event_channel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_unix_name(service: &Service) -> String {
|
|
||||||
match service {
|
|
||||||
Service::Daemon => DAEMON_UNIX_SOCKET_NAME.to_owned(),
|
|
||||||
Service::Worker => WORKER_UNIX_SOCKET_NAME.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::IPCServer for UnixIPCServer {
|
|
||||||
fn start(&self) {
|
|
||||||
let event_channel = self.event_channel.clone();
|
|
||||||
let socket_name = get_unix_name(&self.service);
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("ipc_server".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
let espanso_dir = context::get_data_dir();
|
|
||||||
let unix_socket = espanso_dir.join(socket_name);
|
|
||||||
|
|
||||||
std::fs::remove_file(unix_socket.clone()).unwrap_or_else(|e| {
|
|
||||||
warn!("Unable to delete Unix socket: {}", e);
|
|
||||||
});
|
|
||||||
let listener =
|
|
||||||
UnixListener::bind(unix_socket.clone()).expect("Can't bind to Unix Socket");
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Binded to IPC unix socket: {}",
|
|
||||||
unix_socket.as_path().display()
|
|
||||||
);
|
|
||||||
|
|
||||||
for stream in listener.incoming() {
|
|
||||||
process_event(&event_channel, stream);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("Unable to spawn IPC server thread");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UnixIPCClient {
|
|
||||||
service: Service,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UnixIPCClient {
|
|
||||||
pub fn new(service: Service) -> UnixIPCClient {
|
|
||||||
UnixIPCClient { service }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::IPCClient for UnixIPCClient {
|
|
||||||
fn send_command(&self, command: IPCCommand) -> Result<(), String> {
|
|
||||||
let espanso_dir = context::get_data_dir();
|
|
||||||
let socket_name = get_unix_name(&self.service);
|
|
||||||
let unix_socket = espanso_dir.join(socket_name);
|
|
||||||
|
|
||||||
// Open the stream
|
|
||||||
let stream = UnixStream::connect(unix_socket);
|
|
||||||
|
|
||||||
send_command(command, stream)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::IPCCommand;
|
|
||||||
use log::info;
|
|
||||||
use std::net::{TcpListener, TcpStream};
|
|
||||||
use std::sync::mpsc::Sender;
|
|
||||||
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::context;
|
|
||||||
use crate::event::*;
|
|
||||||
use crate::protocol::{process_event, send_command, Service};
|
|
||||||
use named_pipe::{PipeClient, PipeOptions, PipeServer};
|
|
||||||
use std::io::Error;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
const DAEMON_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansodaemon";
|
|
||||||
const WORKER_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansoworker";
|
|
||||||
const CLIENT_TIMEOUT: u32 = 2000;
|
|
||||||
|
|
||||||
pub struct WindowsIPCServer {
|
|
||||||
service: Service,
|
|
||||||
event_channel: Sender<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_pipe_name(service: &Service) -> String {
|
|
||||||
match service {
|
|
||||||
Service::Daemon => DAEMON_WIN_PIPE_NAME.to_owned(),
|
|
||||||
Service::Worker => WORKER_WIN_PIPE_NAME.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsIPCServer {
|
|
||||||
pub fn new(service: Service, event_channel: Sender<Event>) -> WindowsIPCServer {
|
|
||||||
WindowsIPCServer {
|
|
||||||
service,
|
|
||||||
event_channel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::IPCServer for WindowsIPCServer {
|
|
||||||
fn start(&self) {
|
|
||||||
let event_channel = self.event_channel.clone();
|
|
||||||
let pipe_name = get_pipe_name(&self.service);
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("ipc_server".to_string())
|
|
||||||
.spawn(move || {
|
|
||||||
let options = PipeOptions::new(&pipe_name);
|
|
||||||
|
|
||||||
info!("Binding to named pipe: {}", pipe_name);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let server = options
|
|
||||||
.single()
|
|
||||||
.expect("unable to initialize IPC named pipe");
|
|
||||||
let pipe_server = server.wait();
|
|
||||||
process_event(&event_channel, pipe_server);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("Unable to spawn IPC server thread");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WindowsIPCClient {
|
|
||||||
service: Service,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsIPCClient {
|
|
||||||
pub fn new(service: Service) -> WindowsIPCClient {
|
|
||||||
WindowsIPCClient { service }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::IPCClient for WindowsIPCClient {
|
|
||||||
fn send_command(&self, command: IPCCommand) -> Result<(), String> {
|
|
||||||
let pipe_name = get_pipe_name(&self.service);
|
|
||||||
let client = PipeClient::connect_ms(pipe_name, CLIENT_TIMEOUT);
|
|
||||||
|
|
||||||
send_command(command, client)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,902 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::extension::{Extension, ExtensionResult};
|
|
||||||
use crate::matcher::{Match, MatchContentType, MatchVariable};
|
|
||||||
use log::{error, warn};
|
|
||||||
use regex::{Captures, Regex};
|
|
||||||
use serde_yaml::Value;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref VAR_REGEX: Regex =
|
|
||||||
Regex::new(r"\{\{\s*((?P<name>\w+)(\.(?P<subname>(\w+)))?)\s*\}\}").unwrap();
|
|
||||||
static ref UNKNOWN_VARIABLE: String = "".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DefaultRenderer {
|
|
||||||
extension_map: HashMap<String, Box<dyn Extension>>,
|
|
||||||
|
|
||||||
// Regex used to identify matches (and arguments) in passive expansions
|
|
||||||
passive_match_regex: Regex,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultRenderer {
|
|
||||||
pub fn new(extensions: Vec<Box<dyn Extension>>, config: Configs) -> DefaultRenderer {
|
|
||||||
// Register all the extensions
|
|
||||||
let mut extension_map = HashMap::new();
|
|
||||||
for extension in extensions.into_iter() {
|
|
||||||
extension_map.insert(extension.name(), extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile the regexes
|
|
||||||
let passive_match_regex = Regex::new(&config.passive_match_regex).unwrap_or_else(|e| {
|
|
||||||
panic!("Invalid passive match regex: {:?}", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
DefaultRenderer {
|
|
||||||
extension_map,
|
|
||||||
passive_match_regex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_match(config: &Configs, trigger: &str) -> Option<(Match, usize)> {
|
|
||||||
let mut result = None;
|
|
||||||
|
|
||||||
// TODO: if performances become a problem, implement a more efficient lookup
|
|
||||||
for m in config.matches.iter() {
|
|
||||||
for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() {
|
|
||||||
if m_trigger == trigger {
|
|
||||||
result = Some((m.clone(), trigger_offset));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Renderer for DefaultRenderer {
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
m: &Match,
|
|
||||||
trigger_offset: usize,
|
|
||||||
config: &Configs,
|
|
||||||
args: Vec<String>,
|
|
||||||
) -> RenderResult {
|
|
||||||
// Manage the different types of matches
|
|
||||||
match &m.content {
|
|
||||||
// Text Match
|
|
||||||
MatchContentType::Text(content) => {
|
|
||||||
let target_string = if content._has_vars {
|
|
||||||
// Find all the variables that are required by the current match
|
|
||||||
let mut target_vars: HashSet<String> = HashSet::new();
|
|
||||||
|
|
||||||
for caps in VAR_REGEX.captures_iter(&content.replace) {
|
|
||||||
let var_name = caps.name("name").unwrap().as_str();
|
|
||||||
target_vars.insert(var_name.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
let match_variables: HashSet<&String> =
|
|
||||||
content.vars.iter().map(|var| &var.name).collect();
|
|
||||||
|
|
||||||
// Find the global variables that are not specified in the var list
|
|
||||||
let mut missing_globals = Vec::new();
|
|
||||||
let mut specified_globals: HashMap<String, &MatchVariable> = HashMap::new();
|
|
||||||
for global_var in config.global_vars.iter() {
|
|
||||||
if target_vars.contains(&global_var.name) {
|
|
||||||
if match_variables.contains(&global_var.name) {
|
|
||||||
specified_globals.insert(global_var.name.clone(), &global_var);
|
|
||||||
} else {
|
|
||||||
missing_globals.push(global_var);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the variable evaluation order
|
|
||||||
let mut variables: Vec<&MatchVariable> = Vec::new();
|
|
||||||
// First place the global that are not explicitly specified
|
|
||||||
variables.extend(missing_globals);
|
|
||||||
// Then the ones explicitly specified, in the given order
|
|
||||||
variables.extend(&content.vars);
|
|
||||||
|
|
||||||
// Replace variable type "global" with the actual reference
|
|
||||||
let variables: Vec<&MatchVariable> = variables
|
|
||||||
.into_iter()
|
|
||||||
.map(|variable| {
|
|
||||||
if variable.var_type == "global" {
|
|
||||||
if let Some(actual_variable) = specified_globals.get(&variable.name)
|
|
||||||
{
|
|
||||||
return actual_variable.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variable
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut output_map: HashMap<String, ExtensionResult> = HashMap::new();
|
|
||||||
|
|
||||||
for variable in variables.into_iter() {
|
|
||||||
// In case of variables of type match, we need to recursively call
|
|
||||||
// the render function
|
|
||||||
if variable.var_type == "match" {
|
|
||||||
// Extract the match trigger from the variable params
|
|
||||||
let trigger = variable.params.get(&Value::from("trigger"));
|
|
||||||
if trigger.is_none() {
|
|
||||||
warn!(
|
|
||||||
"Missing param 'trigger' in match variable: {}",
|
|
||||||
variable.name
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let trigger = trigger.unwrap();
|
|
||||||
|
|
||||||
// Find the given match from the active configs
|
|
||||||
let inner_match =
|
|
||||||
DefaultRenderer::find_match(config, trigger.as_str().unwrap_or(""));
|
|
||||||
|
|
||||||
if inner_match.is_none() {
|
|
||||||
warn!(
|
|
||||||
"Could not find inner match with trigger: '{}'",
|
|
||||||
trigger.as_str().unwrap_or("undefined")
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (inner_match, trigger_offset) = inner_match.unwrap();
|
|
||||||
|
|
||||||
// Render the inner match
|
|
||||||
// TODO: inner arguments
|
|
||||||
let result =
|
|
||||||
self.render_match(&inner_match, trigger_offset, config, vec![]);
|
|
||||||
|
|
||||||
// Inner matches are only supported for text-expansions, warn the user otherwise
|
|
||||||
match result {
|
|
||||||
RenderResult::Text(inner_content) => {
|
|
||||||
output_map.insert(variable.name.clone(), ExtensionResult::Single(inner_content));
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal extension variables
|
|
||||||
let extension = self.extension_map.get(&variable.var_type);
|
|
||||||
if let Some(extension) = extension {
|
|
||||||
let ext_res =
|
|
||||||
extension.calculate(&variable.params, &args, &output_map);
|
|
||||||
match ext_res {
|
|
||||||
Ok(ext_out) => {
|
|
||||||
if let Some(output) = ext_out {
|
|
||||||
output_map.insert(variable.name.clone(), output);
|
|
||||||
} else {
|
|
||||||
output_map.insert(
|
|
||||||
variable.name.clone(),
|
|
||||||
ExtensionResult::Single("".to_owned()),
|
|
||||||
);
|
|
||||||
warn!(
|
|
||||||
"Could not generate output for variable: {}",
|
|
||||||
variable.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => return RenderResult::Error,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!(
|
|
||||||
"No extension found for variable type: {}",
|
|
||||||
variable.var_type
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the variables
|
|
||||||
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
|
||||||
let var_name = caps.name("name").unwrap().as_str();
|
|
||||||
let var_subname = caps.name("subname");
|
|
||||||
match output_map.get(var_name) {
|
|
||||||
Some(result) => match result {
|
|
||||||
ExtensionResult::Single(output) => output,
|
|
||||||
ExtensionResult::Multiple(results) => match var_subname {
|
|
||||||
Some(var_subname) => {
|
|
||||||
let var_subname = var_subname.as_str();
|
|
||||||
results.get(var_subname).unwrap_or(&UNKNOWN_VARIABLE)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!(
|
|
||||||
"nested name missing from multi-value variable: {}",
|
|
||||||
var_name
|
|
||||||
);
|
|
||||||
&UNKNOWN_VARIABLE
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
None => &UNKNOWN_VARIABLE,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result.to_string()
|
|
||||||
} else {
|
|
||||||
// No variables, simple text substitution
|
|
||||||
content.replace.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Unescape any brackets (needed to be able to insert double brackets in replacement
|
|
||||||
// text, without triggering the variable system). See issue #187
|
|
||||||
let mut target_string = target_string.replace("\\{", "{").replace("\\}", "}");
|
|
||||||
|
|
||||||
// Render any argument that may be present
|
|
||||||
if !args.is_empty() {
|
|
||||||
target_string = utils::render_args(&target_string, &args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle case propagation
|
|
||||||
target_string = if m.propagate_case {
|
|
||||||
let trigger = &m.triggers[trigger_offset];
|
|
||||||
|
|
||||||
// The check should be carried out from the position of the first
|
|
||||||
// alphabetic letter
|
|
||||||
// See issue #244
|
|
||||||
let first_alphabetic =
|
|
||||||
trigger.chars().position(|c| c.is_alphabetic()).unwrap_or(0);
|
|
||||||
|
|
||||||
let first_char = trigger.chars().nth(first_alphabetic);
|
|
||||||
let second_char = trigger.chars().nth(first_alphabetic + 1);
|
|
||||||
let mode: i32 = if let Some(first_char) = first_char {
|
|
||||||
if first_char.is_uppercase() {
|
|
||||||
if let Some(second_char) = second_char {
|
|
||||||
if second_char.is_uppercase() {
|
|
||||||
2 // Full CAPITALIZATION
|
|
||||||
} else {
|
|
||||||
1 // Only first letter capitalized: Capitalization
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
2 // Single char, defaults to full CAPITALIZATION
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0 // Lowercase, no action
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
match mode {
|
|
||||||
1 => {
|
|
||||||
// Capitalize the first letter
|
|
||||||
let mut v: Vec<char> = target_string.chars().collect();
|
|
||||||
v[0] = v[0].to_uppercase().nth(0).unwrap();
|
|
||||||
v.into_iter().collect()
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
// Full capitalization
|
|
||||||
target_string.to_uppercase()
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Noop
|
|
||||||
target_string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
target_string
|
|
||||||
};
|
|
||||||
|
|
||||||
RenderResult::Text(target_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image Match
|
|
||||||
MatchContentType::Image(content) => {
|
|
||||||
// Make sure the image exist beforehand
|
|
||||||
if content.path.exists() {
|
|
||||||
RenderResult::Image(content.path.clone())
|
|
||||||
} else {
|
|
||||||
error!("Image not found in path: {:?}", content.path);
|
|
||||||
RenderResult::Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult {
|
|
||||||
// Render the matches
|
|
||||||
let result = self
|
|
||||||
.passive_match_regex
|
|
||||||
.replace_all(&text, |caps: &Captures| {
|
|
||||||
let match_name = if let Some(name) = caps.name("name") {
|
|
||||||
name.as_str()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the original matching string, useful to return the match untouched
|
|
||||||
let original_match = caps.get(0).unwrap().as_str();
|
|
||||||
|
|
||||||
// Find the corresponding match
|
|
||||||
let m = DefaultRenderer::find_match(config, match_name);
|
|
||||||
|
|
||||||
// If no match is found, leave the match without modifications
|
|
||||||
if m.is_none() {
|
|
||||||
return original_match.to_owned();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the args by separating them
|
|
||||||
let match_args = if let Some(args) = caps.name("args") {
|
|
||||||
args.as_str()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
let args: Vec<String> = utils::split_args(
|
|
||||||
match_args,
|
|
||||||
config.passive_arg_delimiter,
|
|
||||||
config.passive_arg_escape,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (m, trigger_offset) = m.unwrap();
|
|
||||||
// Render the actual match
|
|
||||||
let result = self.render_match(&m, trigger_offset, &config, args);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
RenderResult::Text(out) => out,
|
|
||||||
_ => original_match.to_owned(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
RenderResult::Text(result.into_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTS
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn get_renderer(config: Configs) -> DefaultRenderer {
|
|
||||||
DefaultRenderer::new(
|
|
||||||
vec![
|
|
||||||
Box::new(crate::extension::dummy::DummyExtension::new("dummy")),
|
|
||||||
Box::new(crate::extension::vardummy::VarDummyExtension::new()),
|
|
||||||
Box::new(crate::extension::multiecho::MultiEchoExtension::new()),
|
|
||||||
],
|
|
||||||
config,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_config_for(s: &str) -> Configs {
|
|
||||||
let config: Configs = serde_yaml::from_str(s).unwrap();
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_render(rendered: RenderResult, target: &str) {
|
|
||||||
match rendered {
|
|
||||||
RenderResult::Text(rendered) => {
|
|
||||||
assert_eq!(rendered, target);
|
|
||||||
}
|
|
||||||
_ => assert!(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_no_matches() {
|
|
||||||
let text = r###"
|
|
||||||
this text contains no matches
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: test
|
|
||||||
replace: result
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_no_args() {
|
|
||||||
let text = "this is a :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: result
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is a result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_multiple_match_no_args() {
|
|
||||||
let text = "this is a :test and then another :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: result
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is a result and then another result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_multiline_no_args() {
|
|
||||||
let text = r###"this is a
|
|
||||||
:test
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let result = r###"this is a
|
|
||||||
result
|
|
||||||
"###;
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: result
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_nested_matches_no_args() {
|
|
||||||
let text = ":greet";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':greet'
|
|
||||||
replace: "hi {{name}}"
|
|
||||||
vars:
|
|
||||||
- name: name
|
|
||||||
type: match
|
|
||||||
params:
|
|
||||||
trigger: ":name"
|
|
||||||
|
|
||||||
- trigger: ':name'
|
|
||||||
replace: john
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "hi john");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_with_args() {
|
|
||||||
let text = ":greet/Jon/";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':greet'
|
|
||||||
replace: "Hi $0$"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "Hi Jon");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_no_args_should_not_replace_args_syntax() {
|
|
||||||
let text = ":greet";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':greet'
|
|
||||||
replace: "Hi $0$"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "Hi $0$");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_with_multiple_args() {
|
|
||||||
let text = ":greet/Jon/Snow/";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':greet'
|
|
||||||
replace: "Hi $0$, there is $1$ outside"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "Hi Jon, there is Snow outside");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_with_escaped_args() {
|
|
||||||
let text = ":greet/Jon/10\\/12/";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':greet'
|
|
||||||
replace: "Hi $0$, today is $1$"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "Hi Jon, today is 10/12");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_with_args_not_closed() {
|
|
||||||
let text = ":greet/Jon/Snow";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':greet'
|
|
||||||
replace: "Hi $0$"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "Hi JonSnow");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_local_var() {
|
|
||||||
let text = "this is :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: "my {{output}}"
|
|
||||||
vars:
|
|
||||||
- name: output
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "result"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is my result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_global_var() {
|
|
||||||
let text = "this is :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
global_vars:
|
|
||||||
- name: output
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "result"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: "my {{output}}"
|
|
||||||
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is my result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_global_var_is_overridden_by_local() {
|
|
||||||
let text = "this is :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
global_vars:
|
|
||||||
- name: output
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "result"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: "my {{output}}"
|
|
||||||
vars:
|
|
||||||
- name: "output"
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "local"
|
|
||||||
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is my local");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_match_with_unknown_variable_does_not_crash() {
|
|
||||||
let text = "this is :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: "my {{unknown}}"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is my ");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_escaped_double_brackets_should_not_consider_them_variable() {
|
|
||||||
let text = "this is :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: ':test'
|
|
||||||
replace: "my \\{\\{unknown\\}\\}"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is my {{unknown}}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_multi_trigger_no_args() {
|
|
||||||
let text = "this is a :yolo and :test";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- triggers: [':test', ':yolo']
|
|
||||||
replace: result
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "this is a result and result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_passive_simple_match_multi_trigger_with_args() {
|
|
||||||
let text = ":yolo/Jon/";
|
|
||||||
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- triggers: [':greet', ':yolo']
|
|
||||||
replace: "Hi $0$"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let rendered = renderer.render_passive(text, &config);
|
|
||||||
|
|
||||||
verify_render(rendered, "Hi Jon");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_match_case_propagation_no_case() {
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: 'test'
|
|
||||||
replace: result
|
|
||||||
propagate_case: true
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let m = config.matches[0].clone();
|
|
||||||
|
|
||||||
let trigger_offset = m.triggers.iter().position(|x| x == "test").unwrap();
|
|
||||||
|
|
||||||
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
|
|
||||||
|
|
||||||
verify_render(rendered, "result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_match_case_propagation_first_capital() {
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: 'test'
|
|
||||||
replace: result
|
|
||||||
propagate_case: true
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let m = config.matches[0].clone();
|
|
||||||
|
|
||||||
let trigger_offset = m.triggers.iter().position(|x| x == "Test").unwrap();
|
|
||||||
|
|
||||||
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
|
|
||||||
|
|
||||||
verify_render(rendered, "Result");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_match_case_propagation_all_capital() {
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: 'test'
|
|
||||||
replace: result
|
|
||||||
propagate_case: true
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
|
|
||||||
let m = config.matches[0].clone();
|
|
||||||
|
|
||||||
let trigger_offset = m.triggers.iter().position(|x| x == "TEST").unwrap();
|
|
||||||
|
|
||||||
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
|
|
||||||
|
|
||||||
verify_render(rendered, "RESULT");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_variable_order() {
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: 'test'
|
|
||||||
replace: "{{output}}"
|
|
||||||
vars:
|
|
||||||
- name: first
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "hello"
|
|
||||||
- name: output
|
|
||||||
type: vardummy
|
|
||||||
params:
|
|
||||||
target: "first"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
let m = config.matches[0].clone();
|
|
||||||
let rendered = renderer.render_match(&m, 0, &config, vec![]);
|
|
||||||
verify_render(rendered, "hello");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_global_variable_order() {
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
global_vars:
|
|
||||||
- name: hello
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "hello"
|
|
||||||
matches:
|
|
||||||
- trigger: 'test'
|
|
||||||
replace: "{{hello}} {{output}}"
|
|
||||||
vars:
|
|
||||||
- name: first
|
|
||||||
type: dummy
|
|
||||||
params:
|
|
||||||
echo: "world"
|
|
||||||
- name: output
|
|
||||||
type: vardummy
|
|
||||||
params:
|
|
||||||
target: "first"
|
|
||||||
- name: hello
|
|
||||||
type: global
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
let m = config.matches[0].clone();
|
|
||||||
let rendered = renderer.render_match(&m, 0, &config, vec![]);
|
|
||||||
verify_render(rendered, "hello world");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_multiple_results() {
|
|
||||||
let config = get_config_for(
|
|
||||||
r###"
|
|
||||||
matches:
|
|
||||||
- trigger: 'test'
|
|
||||||
replace: "hello {{var1.name}}"
|
|
||||||
vars:
|
|
||||||
- name: var1
|
|
||||||
type: multiecho
|
|
||||||
params:
|
|
||||||
name: "world"
|
|
||||||
"###,
|
|
||||||
);
|
|
||||||
|
|
||||||
let renderer = get_renderer(config.clone());
|
|
||||||
let m = config.matches[0].clone();
|
|
||||||
let rendered = renderer.render_match(&m, 0, &config, vec![]);
|
|
||||||
verify_render(rendered, "hello world");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::config::Configs;
|
|
||||||
use crate::matcher::Match;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub(crate) mod default;
|
|
||||||
pub(crate) mod utils;
|
|
||||||
|
|
||||||
pub trait Renderer {
|
|
||||||
// Render a match output
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
m: &Match,
|
|
||||||
trigger_offset: usize,
|
|
||||||
config: &Configs,
|
|
||||||
args: Vec<String>,
|
|
||||||
) -> RenderResult;
|
|
||||||
|
|
||||||
// Render a passive expansion text
|
|
||||||
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RenderResult {
|
|
||||||
Text(String),
|
|
||||||
Image(PathBuf),
|
|
||||||
Error,
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of espanso.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2020 Federico Terzi
|
|
||||||
*
|
|
||||||
* espanso is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* espanso is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use regex::{Captures, Regex};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_args(text: &str, args: &Vec<String>) -> String {
|
|
||||||
let result = ARG_REGEX.replace_all(text, |caps: &Captures| {
|
|
||||||
let position_str = caps.name("pos").unwrap().as_str();
|
|
||||||
let position = position_str.parse::<i32>().unwrap_or(-1);
|
|
||||||
|
|
||||||
if position >= 0 && position < args.len() as i32 {
|
|
||||||
args[position as usize].to_owned()
|
|
||||||
} else {
|
|
||||||
"".to_owned()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec<String> {
|
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
// Make sure the text is not empty
|
|
||||||
if text.is_empty() {
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut last = String::from("");
|
|
||||||
let mut previous: char = char::from(0);
|
|
||||||
text.chars().into_iter().for_each(|c| {
|
|
||||||
if c == delimiter {
|
|
||||||
if previous != escape {
|
|
||||||
output.push(last.clone());
|
|
||||||
last = String::from("");
|
|
||||||
} else {
|
|
||||||
last.push(c);
|
|
||||||
}
|
|
||||||
} else if c == escape {
|
|
||||||
if previous == escape {
|
|
||||||
last.push(c);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
last.push(c);
|
|
||||||
}
|
|
||||||
previous = c;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the last one
|
|
||||||
output.push(last);
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTS
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_args_no_args() {
|
|
||||||
let args = vec!["hello".to_owned()];
|
|
||||||
assert_eq!(render_args("no args", &args), "no args")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_args_one_arg() {
|
|
||||||
let args = vec!["jon".to_owned()];
|
|
||||||
assert_eq!(render_args("hello $0$", &args), "hello jon")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_args_one_multiple_args() {
|
|
||||||
let args = vec!["jon".to_owned(), "snow".to_owned()];
|
|
||||||
assert_eq!(
|
|
||||||
render_args("hello $0$, the $1$ is white", &args),
|
|
||||||
"hello jon, the snow is white"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_render_args_out_of_range() {
|
|
||||||
let args = vec!["jon".to_owned()];
|
|
||||||
assert_eq!(render_args("hello $10$", &args), "hello ")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_args_one_arg() {
|
|
||||||
assert_eq!(split_args("jon", '/', '\\'), vec!["jon"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_args_two_args() {
|
|
||||||
assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_args_escaping() {
|
|
||||||
assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_args_escaping_escape() {
|
|
||||||
assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_args_empty() {
|
|
||||||
let empty_vec: Vec<String> = vec![];
|
|
||||||
assert_eq!(split_args("", '/', '\\'), empty_vec)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
# espanso configuration file
|
|
||||||
|
|
||||||
# This is the default configuration file, change it as you like it
|
|
||||||
# You can refer to the official documentation:
|
|
||||||
# https://espanso.org/docs/
|
|
||||||
|
|
||||||
# Matches are the substitution rules, when you type the "trigger" string
|
|
||||||
# it gets replaced by the "replace" string.
|
|
||||||
matches:
|
|
||||||
# Simple text replacement
|
|
||||||
- trigger: ":espanso"
|
|
||||||
replace: "Hi there!"
|
|
||||||
|
|
||||||
# Dates
|
|
||||||
- trigger: ":date"
|
|
||||||
replace: "{{mydate}}"
|
|
||||||
vars:
|
|
||||||
- name: mydate
|
|
||||||
type: date
|
|
||||||
params:
|
|
||||||
format: "%m/%d/%Y"
|
|
||||||
|
|
||||||
# Shell commands
|
|
||||||
- trigger: ":shell"
|
|
||||||
replace: "{{output}}"
|
|
||||||
vars:
|
|
||||||
- name: output
|
|
||||||
type: shell
|
|
||||||
params:
|
|
||||||
cmd: "echo Hello from your shell"
|
|
Before Width: | Height: | Size: 11 KiB |
|
@ -1,11 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=espanso daemon
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart={{{espanso_path}}} daemon
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>Label</key>
|
|
||||||
<string>com.federicoterzi.espanso</string>
|
|
||||||
<key>EnvironmentVariables</key>
|
|
||||||
<dict>
|
|
||||||
<key>PATH</key>
|
|
||||||
<string>{{{PATH}}}</string>
|
|
||||||
</dict>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>{{{espanso_path}}}</string>
|
|
||||||
<string>daemon</string>
|
|
||||||
</array>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<true/>
|
|
||||||
<key>StandardErrorPath</key>
|
|
||||||
<string>/tmp/espanso.err</string>
|
|
||||||
<key>StandardOutPath</key>
|
|
||||||
<string>/tmp/espanso.out</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|