commit
7c06cf0acb
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -370,7 +370,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
|
|
@ -56,6 +56,7 @@ Many people helped the project along the way, thanks to all of you. In particula
|
||||||
* [Scrumplex](https://scrumplex.net/) - Official AUR repo mantainer and Linux Guru
|
* [Scrumplex](https://scrumplex.net/) - Official AUR repo mantainer and Linux Guru
|
||||||
* [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester
|
* [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester
|
||||||
* [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester
|
* [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester
|
||||||
|
* [Timo Runge](http://timorunge.com/) - MacOS contributor
|
||||||
|
|
||||||
## Remarks
|
## Remarks
|
||||||
|
|
||||||
|
|
|
@ -278,6 +278,12 @@ void delete_string(int32_t count) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void left_arrow(int32_t count) {
|
||||||
|
for (int i = 0; i<count; i++) {
|
||||||
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Left", 8000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void trigger_paste() {
|
void trigger_paste() {
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+v", 8000);
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+v", 8000);
|
||||||
}
|
}
|
||||||
|
@ -446,6 +452,8 @@ int32_t is_current_window_terminal() {
|
||||||
return 1;
|
return 1;
|
||||||
}else if (strstr(class_buffer, "konsole") != NULL) { // KDE Konsole
|
}else if (strstr(class_buffer, "konsole") != NULL) { // KDE Konsole
|
||||||
return 1;
|
return 1;
|
||||||
|
}else if (strstr(class_buffer, "Terminator") != NULL) { // Terminator
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,11 @@ extern "C" void send_string(const char * string);
|
||||||
*/
|
*/
|
||||||
extern "C" void delete_string(int32_t count);
|
extern "C" void delete_string(int32_t count);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send the left arrow keypress, *count* times.
|
||||||
|
*/
|
||||||
|
extern "C" void left_arrow(int32_t count);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Trigger normal paste ( Pressing CTRL+V )
|
* Trigger normal paste ( Pressing CTRL+V )
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -37,6 +37,11 @@ int32_t initialize(void * context, const char * icon_path);
|
||||||
*/
|
*/
|
||||||
int32_t eventloop();
|
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,
|
* Called when a new keypress is made, the first argument is an char array,
|
||||||
* while the second is the size of the array.
|
* while the second is the size of the array.
|
||||||
|
@ -60,6 +65,11 @@ void send_string(const char * string);
|
||||||
*/
|
*/
|
||||||
void send_vkey(int32_t vk);
|
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.
|
* Send the backspace keypress, *count* times.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -64,6 +64,12 @@ int32_t eventloop() {
|
||||||
[NSApp run];
|
[NSApp run];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t headless_eventloop() {
|
||||||
|
NSApplication * application = [NSApplication sharedApplication];
|
||||||
|
[NSApp run];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void send_string(const char * string) {
|
void send_string(const char * string) {
|
||||||
char * stringCopy = strdup(string);
|
char * stringCopy = strdup(string);
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
@ -101,23 +107,7 @@ void send_string(const char * string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void delete_string(int32_t count) {
|
void delete_string(int32_t count) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
send_multi_vkey(0x33, count);
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
CGEventRef keydown;
|
|
||||||
keydown = CGEventCreateKeyboardEvent(NULL, 0x33, true);
|
|
||||||
CGEventPost(kCGHIDEventTap, keydown);
|
|
||||||
CFRelease(keydown);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
|
|
||||||
CGEventRef keyup;
|
|
||||||
keyup = CGEventCreateKeyboardEvent(NULL, 0x33, false);
|
|
||||||
CGEventPost(kCGHIDEventTap, keyup);
|
|
||||||
CFRelease(keyup);
|
|
||||||
|
|
||||||
usleep(2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_vkey(int32_t vk) {
|
void send_vkey(int32_t vk) {
|
||||||
|
@ -127,14 +117,34 @@ void send_vkey(int32_t vk) {
|
||||||
CGEventPost(kCGHIDEventTap, keydown);
|
CGEventPost(kCGHIDEventTap, keydown);
|
||||||
CFRelease(keydown);
|
CFRelease(keydown);
|
||||||
|
|
||||||
usleep(2000);
|
usleep(500);
|
||||||
|
|
||||||
CGEventRef keyup;
|
CGEventRef keyup;
|
||||||
keyup = CGEventCreateKeyboardEvent(NULL, vk, false);
|
keyup = CGEventCreateKeyboardEvent(NULL, vk, false);
|
||||||
CGEventPost(kCGHIDEventTap, keyup);
|
CGEventPost(kCGHIDEventTap, keyup);
|
||||||
CFRelease(keyup);
|
CFRelease(keyup);
|
||||||
|
|
||||||
usleep(2000);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -243,7 +243,12 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
|
||||||
if (GetKeyboardState(lpKeyState.data())) {
|
if (GetKeyboardState(lpKeyState.data())) {
|
||||||
// Convert the virtual key to an unicode char
|
// Convert the virtual key to an unicode char
|
||||||
std::array<WCHAR, 4> buffer;
|
std::array<WCHAR, 4> buffer;
|
||||||
int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), 0, currentKeyboardLayout);
|
|
||||||
|
// 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;
|
//std::cout << result << " " << buffer[0] << " " << raw->data.keyboard.VKey << std::endl;
|
||||||
|
|
||||||
|
@ -448,24 +453,7 @@ void send_string(const wchar_t * string) {
|
||||||
* Send the backspace keypress, *count* times.
|
* Send the backspace keypress, *count* times.
|
||||||
*/
|
*/
|
||||||
void delete_string(int32_t count) {
|
void delete_string(int32_t count) {
|
||||||
std::vector<INPUT> vec;
|
send_multi_vkey(VK_BACK, count);
|
||||||
|
|
||||||
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_BACK;
|
|
||||||
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_vkey(int32_t vk) {
|
void send_vkey(int32_t vk) {
|
||||||
|
@ -487,6 +475,27 @@ void send_vkey(int32_t vk) {
|
||||||
SendInput(vec.size(), vec.data(), sizeof(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 trigger_paste() {
|
void trigger_paste() {
|
||||||
std::vector<INPUT> vec;
|
std::vector<INPUT> vec;
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,11 @@ extern "C" void send_string(const wchar_t * string);
|
||||||
*/
|
*/
|
||||||
extern "C" void send_vkey(int32_t vk);
|
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 backspace keypress, *count* times.
|
* Send the backspace keypress, *count* times.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -39,6 +39,7 @@ extern {
|
||||||
|
|
||||||
pub fn send_string(string: *const c_char);
|
pub fn send_string(string: *const c_char);
|
||||||
pub fn delete_string(count: i32);
|
pub fn delete_string(count: i32);
|
||||||
|
pub fn left_arrow(count: i32);
|
||||||
pub fn trigger_paste();
|
pub fn trigger_paste();
|
||||||
pub fn trigger_terminal_paste();
|
pub fn trigger_terminal_paste();
|
||||||
}
|
}
|
|
@ -31,6 +31,7 @@ pub struct MacMenuItem {
|
||||||
extern {
|
extern {
|
||||||
pub fn initialize(s: *const c_void, icon_path: *const c_char);
|
pub fn initialize(s: *const c_void, icon_path: *const c_char);
|
||||||
pub fn eventloop();
|
pub fn eventloop();
|
||||||
|
pub fn headless_eventloop();
|
||||||
|
|
||||||
// System
|
// System
|
||||||
pub fn check_accessibility() -> i32;
|
pub fn check_accessibility() -> i32;
|
||||||
|
@ -54,6 +55,7 @@ extern {
|
||||||
|
|
||||||
pub fn send_string(string: *const c_char);
|
pub fn send_string(string: *const c_char);
|
||||||
pub fn send_vkey(vk: i32);
|
pub fn send_vkey(vk: i32);
|
||||||
|
pub fn send_multi_vkey(vk: i32, count: i32);
|
||||||
pub fn delete_string(count: i32);
|
pub fn delete_string(count: i32);
|
||||||
pub fn trigger_paste();
|
pub fn trigger_paste();
|
||||||
}
|
}
|
|
@ -55,6 +55,7 @@ extern {
|
||||||
pub fn eventloop();
|
pub fn eventloop();
|
||||||
pub fn send_string(string: *const u16);
|
pub fn send_string(string: *const u16);
|
||||||
pub fn send_vkey(vk: i32);
|
pub fn send_vkey(vk: i32);
|
||||||
|
pub fn send_multi_vkey(vk: i32, count: i32);
|
||||||
pub fn delete_string(count: i32);
|
pub fn delete_string(count: i32);
|
||||||
pub fn trigger_paste();
|
pub fn trigger_paste();
|
||||||
}
|
}
|
|
@ -50,6 +50,7 @@ fn default_log_level() -> i32 { 0 }
|
||||||
fn default_ipc_server_port() -> i32 { 34982 }
|
fn default_ipc_server_port() -> i32 { 34982 }
|
||||||
fn default_use_system_agent() -> bool { true }
|
fn default_use_system_agent() -> bool { true }
|
||||||
fn default_config_caching_interval() -> i32 { 800 }
|
fn default_config_caching_interval() -> i32 { 800 }
|
||||||
|
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n'] }
|
||||||
fn default_toggle_interval() -> u32 { 230 }
|
fn default_toggle_interval() -> u32 { 230 }
|
||||||
fn default_backspace_limit() -> i32 { 3 }
|
fn default_backspace_limit() -> i32 { 3 }
|
||||||
fn default_exclude_default_matches() -> bool {false}
|
fn default_exclude_default_matches() -> bool {false}
|
||||||
|
@ -87,6 +88,9 @@ pub struct Configs {
|
||||||
#[serde(default = "default_config_caching_interval")]
|
#[serde(default = "default_config_caching_interval")]
|
||||||
pub config_caching_interval: i32,
|
pub config_caching_interval: i32,
|
||||||
|
|
||||||
|
#[serde(default = "default_word_separators")]
|
||||||
|
pub word_separators: Vec<char>, // TODO: add parsing test
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub toggle_key: KeyModifier,
|
pub toggle_key: KeyModifier,
|
||||||
|
|
||||||
|
|
|
@ -103,16 +103,22 @@ lazy_static! {
|
||||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
||||||
MatchReceiver for Engine<'a, S, C, M, U>{
|
MatchReceiver for Engine<'a, S, C, M, U>{
|
||||||
|
|
||||||
fn on_match(&self, m: &Match) {
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
|
||||||
let config = self.config_manager.active_config();
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
if config.disabled {
|
if config.disabled {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.keyboard_manager.delete_string(m.trigger.chars().count() as i32);
|
let char_count = if trailing_separator.is_none() {
|
||||||
|
m.trigger.chars().count() as i32
|
||||||
|
}else{
|
||||||
|
m.trigger.chars().count() as i32 + 1 // Count also the separator
|
||||||
|
};
|
||||||
|
|
||||||
let target_string = if m._has_vars {
|
self.keyboard_manager.delete_string(char_count);
|
||||||
|
|
||||||
|
let mut target_string = if m._has_vars {
|
||||||
let mut output_map = HashMap::new();
|
let mut output_map = HashMap::new();
|
||||||
|
|
||||||
for variable in m.vars.iter() {
|
for variable in m.vars.iter() {
|
||||||
|
@ -142,6 +148,37 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
m.replace.clone()
|
m.replace.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
match config.backend {
|
match config.backend {
|
||||||
BackendType::Inject => {
|
BackendType::Inject => {
|
||||||
// Send the expected string. On linux, newlines are managed automatically
|
// Send the expected string. On linux, newlines are managed automatically
|
||||||
|
@ -151,7 +188,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
self.keyboard_manager.send_string(&target_string);
|
self.keyboard_manager.send_string(&target_string);
|
||||||
}else{
|
}else{
|
||||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
// To handle newlines, substitute each "\n" char with an Enter key press.
|
||||||
let splits = target_string.lines();
|
let splits = target_string.split('\n');
|
||||||
|
|
||||||
for (i, split) in splits.enumerate() {
|
for (i, split) in splits.enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
|
@ -167,6 +204,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
self.keyboard_manager.trigger_paste();
|
self.keyboard_manager.trigger_paste();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(moves);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enable_update(&self, status: bool) {
|
fn on_enable_update(&self, status: bool) {
|
||||||
|
|
|
@ -53,4 +53,10 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
||||||
fn delete_string(&self, count: i32) {
|
fn delete_string(&self, count: i32) {
|
||||||
unsafe {delete_string(count)}
|
unsafe {delete_string(count)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_cursor_left(&self, count: i32) {
|
||||||
|
unsafe {
|
||||||
|
left_arrow(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -48,4 +48,11 @@ impl super::KeyboardManager for MacKeyboardManager {
|
||||||
fn delete_string(&self, count: i32) {
|
fn delete_string(&self, count: i32) {
|
||||||
unsafe {delete_string(count)}
|
unsafe {delete_string(count)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_cursor_left(&self, count: i32) {
|
||||||
|
unsafe {
|
||||||
|
// Simulate the Left arrow count times
|
||||||
|
send_multi_vkey(0x7B, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -31,6 +31,7 @@ pub trait KeyboardManager {
|
||||||
fn send_enter(&self);
|
fn send_enter(&self);
|
||||||
fn trigger_paste(&self);
|
fn trigger_paste(&self);
|
||||||
fn delete_string(&self, count: i32);
|
fn delete_string(&self, count: i32);
|
||||||
|
fn move_cursor_left(&self, count: i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WINDOWS IMPLEMENTATION
|
// WINDOWS IMPLEMENTATION
|
||||||
|
|
|
@ -55,4 +55,11 @@ impl super::KeyboardManager for WindowsKeyboardManager {
|
||||||
delete_string(count)
|
delete_string(count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_cursor_left(&self, count: i32) {
|
||||||
|
unsafe {
|
||||||
|
// Send the left arrow key multiple times
|
||||||
|
send_multi_vkey(0x25, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
47
src/main.rs
47
src/main.rs
|
@ -530,6 +530,7 @@ fn restart_main(config_set: ConfigSet) {
|
||||||
|
|
||||||
/// Cli tool used to analyze active windows to extract useful information
|
/// Cli tool used to analyze active windows to extract useful information
|
||||||
/// to create configuration filters.
|
/// to create configuration filters.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn detect_main() {
|
fn detect_main() {
|
||||||
let system_manager = system::get_manager();
|
let system_manager = system::get_manager();
|
||||||
|
|
||||||
|
@ -562,6 +563,52 @@ fn detect_main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cli tool used to analyze active windows to extract useful information
|
||||||
|
/// to create configuration filters.
|
||||||
|
/// On macOS version we need to start an event loop for the app to register changes.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn detect_main() {
|
||||||
|
thread::spawn(|| {
|
||||||
|
use std::io::Write;
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
|
let system_manager = system::get_manager();
|
||||||
|
|
||||||
|
println!("Listening for changes, now focus the window you want to analyze.");
|
||||||
|
println!("Warning: stay on the window for a few seconds, as it may take a while to register.");
|
||||||
|
println!("You can terminate with CTRL+C\n");
|
||||||
|
|
||||||
|
let mut last_title : String = "".to_owned();
|
||||||
|
let mut last_class : String = "".to_owned();
|
||||||
|
let mut last_exec : String = "".to_owned();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let curr_title = system_manager.get_current_window_title().unwrap_or_default();
|
||||||
|
let curr_class = system_manager.get_current_window_class().unwrap_or_default();
|
||||||
|
let curr_exec = system_manager.get_current_window_executable().unwrap_or_default();
|
||||||
|
|
||||||
|
// Check if a change occurred
|
||||||
|
if curr_title != last_title || curr_class != last_class || curr_exec != last_exec {
|
||||||
|
println!("Detected change, current window has properties:");
|
||||||
|
println!("==> Title: '{}'", curr_title);
|
||||||
|
println!("==> Class: '{}'", curr_class);
|
||||||
|
println!("==> Executable: '{}'", curr_exec);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
last_title = curr_title;
|
||||||
|
last_class = curr_class;
|
||||||
|
last_exec = curr_exec;
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
crate::bridge::macos::headless_eventloop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send the given command to the espanso daemon
|
/// Send the given command to the espanso daemon
|
||||||
fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) {
|
fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) {
|
||||||
let command = if matches.subcommand_matches("exit").is_some() {
|
let command = if matches.subcommand_matches("exit").is_some() {
|
||||||
|
|
|
@ -30,9 +30,14 @@ pub struct Match {
|
||||||
pub trigger: String,
|
pub trigger: String,
|
||||||
pub replace: String,
|
pub replace: String,
|
||||||
pub vars: Vec<MatchVariable>,
|
pub vars: Vec<MatchVariable>,
|
||||||
|
pub word: bool,
|
||||||
|
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub _has_vars: bool,
|
pub _has_vars: bool,
|
||||||
|
|
||||||
|
// Automatically calculated from the trigger, used by the matcher to check for correspondences.
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub _trigger_sequence: Vec<TriggerEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'de> serde::Deserialize<'de> for Match {
|
impl <'de> serde::Deserialize<'de> for Match {
|
||||||
|
@ -50,14 +55,30 @@ impl<'a> From<&'a AutoMatch> for Match{
|
||||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap();
|
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: may need to replace windows newline (\r\n) with newline only (\n)
|
||||||
|
|
||||||
|
let new_replace = other.replace.clone();
|
||||||
|
|
||||||
// Check if the match contains variables
|
// Check if the match contains variables
|
||||||
let has_vars = VAR_REGEX.is_match(&other.replace);
|
let has_vars = VAR_REGEX.is_match(&other.replace);
|
||||||
|
|
||||||
|
// Calculate the trigger sequence
|
||||||
|
let mut trigger_sequence = Vec::new();
|
||||||
|
let trigger_chars : Vec<char> = other.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);
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
trigger: other.trigger.clone(),
|
trigger: other.trigger.clone(),
|
||||||
replace: other.replace.clone(),
|
replace: new_replace,
|
||||||
vars: other.vars.clone(),
|
vars: other.vars.clone(),
|
||||||
|
word: other.word.clone(),
|
||||||
_has_vars: has_vars,
|
_has_vars: has_vars,
|
||||||
|
_trigger_sequence: trigger_sequence,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,9 +91,13 @@ struct AutoMatch {
|
||||||
|
|
||||||
#[serde(default = "default_vars")]
|
#[serde(default = "default_vars")]
|
||||||
pub vars: Vec<MatchVariable>,
|
pub vars: Vec<MatchVariable>,
|
||||||
|
|
||||||
|
#[serde(default = "default_word")]
|
||||||
|
pub word: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
||||||
|
fn default_word() -> bool {false}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MatchVariable {
|
pub struct MatchVariable {
|
||||||
|
@ -84,8 +109,14 @@ pub struct MatchVariable {
|
||||||
pub params: Mapping,
|
pub params: Mapping,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
pub enum TriggerEntry {
|
||||||
|
Char(char),
|
||||||
|
WordSeparator
|
||||||
|
}
|
||||||
|
|
||||||
pub trait MatchReceiver {
|
pub trait MatchReceiver {
|
||||||
fn on_match(&self, m: &Match);
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>);
|
||||||
fn on_enable_update(&self, status: bool);
|
fn on_enable_update(&self, status: bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,4 +180,36 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(_match._has_vars, true);
|
assert_eq!(_match._has_vars, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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_sequence[0], TriggerEntry::Char('t'));
|
||||||
|
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e'));
|
||||||
|
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s'));
|
||||||
|
assert_eq!(_match._trigger_sequence[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_sequence[0], TriggerEntry::Char('t'));
|
||||||
|
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e'));
|
||||||
|
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s'));
|
||||||
|
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t'));
|
||||||
|
assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::matcher::{Match, MatchReceiver};
|
use crate::matcher::{Match, MatchReceiver, TriggerEntry};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use crate::event::{KeyModifier, ActionEventReceiver, ActionType};
|
use crate::event::{KeyModifier, ActionEventReceiver, ActionType};
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
|
@ -31,8 +31,10 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
||||||
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
||||||
toggle_press_time: RefCell<SystemTime>,
|
toggle_press_time: RefCell<SystemTime>,
|
||||||
is_enabled: RefCell<bool>,
|
is_enabled: RefCell<bool>,
|
||||||
|
was_previous_char_word_separator: RefCell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct MatchEntry<'a> {
|
struct MatchEntry<'a> {
|
||||||
start: usize,
|
start: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
@ -49,7 +51,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
||||||
receiver,
|
receiver,
|
||||||
current_set_queue,
|
current_set_queue,
|
||||||
toggle_press_time,
|
toggle_press_time,
|
||||||
is_enabled: RefCell::new(true)
|
is_enabled: RefCell::new(true),
|
||||||
|
was_previous_char_word_separator: RefCell::new(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +69,17 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
||||||
|
|
||||||
self.receiver.on_enable_update(*is_enabled);
|
self.receiver.on_enable_update(*is_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_matching(mtc: &Match, current_char: &str, start: usize, is_current_word_separator: bool) -> bool {
|
||||||
|
match mtc._trigger_sequence[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> {
|
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> {
|
||||||
|
@ -75,28 +89,49 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
return;
|
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()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Workaround needed on macos to consider espanso replacement key presses as separators.
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
if c.len() > 1 {
|
||||||
|
is_current_word_separator = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
|
||||||
|
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
||||||
|
|
||||||
let new_matches: Vec<MatchEntry> = self.config_manager.matches().iter()
|
let new_matches: Vec<MatchEntry> = active_config.matches.iter()
|
||||||
.filter(|&x| x.trigger.starts_with(c))
|
.filter(|&x| {
|
||||||
|
let mut result = Self::is_matching(x, c, 0, is_current_word_separator);
|
||||||
|
|
||||||
|
if x.word {
|
||||||
|
result = result && *was_previous_word_separator
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
})
|
||||||
.map(|x | MatchEntry{
|
.map(|x | MatchEntry{
|
||||||
start: 1,
|
start: 1,
|
||||||
count: x.trigger.chars().count(),
|
count: x._trigger_sequence.len(),
|
||||||
_match: &x
|
_match: &x
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
|
// 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() {
|
let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
|
||||||
Some(last_matches) => {
|
Some(last_matches) => {
|
||||||
let mut updated: Vec<MatchEntry> = last_matches.iter()
|
let mut updated: Vec<MatchEntry> = last_matches.iter()
|
||||||
.filter(|&x| {
|
.filter(|&x| {
|
||||||
let nchar = x._match.trigger.chars().nth(x.start);
|
Self::is_matching(x._match, c, x.start, is_current_word_separator)
|
||||||
if let Some(nchar) = nchar {
|
|
||||||
c.starts_with(nchar)
|
|
||||||
}else{
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.map(|x | MatchEntry{
|
.map(|x | MatchEntry{
|
||||||
start: x.start+1,
|
start: x.start+1,
|
||||||
|
@ -126,11 +161,29 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
current_set_queue.pop_front();
|
current_set_queue.pop_front();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(_match) = found_match {
|
*was_previous_word_separator = is_current_word_separator;
|
||||||
|
|
||||||
|
if let Some(mtc) = found_match {
|
||||||
if let Some(last) = current_set_queue.back_mut() {
|
if let Some(last) = current_set_queue.back_mut() {
|
||||||
last.clear();
|
last.clear();
|
||||||
}
|
}
|
||||||
self.receiver.on_match(_match);
|
|
||||||
|
let trailing_separator = 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 239 B After Width: | Height: | Size: 4.1 KiB |
Loading…
Reference in New Issue
Block a user