commit
c454227973
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -32,3 +32,7 @@ profile
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
DerivedData
|
DerivedData
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
*.snap
|
||||||
|
|
||||||
|
venv/
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -370,7 +370,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
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.5.0"
|
version = "0.5.1"
|
||||||
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"
|
||||||
|
|
|
@ -307,6 +307,10 @@ void trigger_alt_shift_ins_paste() {
|
||||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000);
|
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() {
|
void trigger_copy() {
|
||||||
// Release the other keys, for an explanation, read the 'trigger_paste' method
|
// Release the other keys, for an explanation, read the 'trigger_paste' method
|
||||||
|
|
||||||
|
@ -467,8 +471,8 @@ int32_t is_current_window_special() {
|
||||||
if (res > 0) {
|
if (res > 0) {
|
||||||
if (strstr(class_buffer, "terminal") != NULL) {
|
if (strstr(class_buffer, "terminal") != NULL) {
|
||||||
return 1;
|
return 1;
|
||||||
}else if (strstr(class_buffer, "URxvt") != NULL) { // Manjaro terminal
|
}else if (strstr(class_buffer, "URxvt") != NULL) { // urxvt terminal
|
||||||
return 1;
|
return 4;
|
||||||
}else if (strstr(class_buffer, "XTerm") != NULL) { // XTerm and UXTerm
|
}else if (strstr(class_buffer, "XTerm") != NULL) { // XTerm and UXTerm
|
||||||
return 1;
|
return 1;
|
||||||
}else if (strstr(class_buffer, "Termite") != NULL) { // Termite
|
}else if (strstr(class_buffer, "Termite") != NULL) { // Termite
|
||||||
|
|
|
@ -92,6 +92,11 @@ extern "C" void trigger_shift_ins_paste();
|
||||||
*/
|
*/
|
||||||
extern "C" void trigger_alt_shift_ins_paste();
|
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 )
|
* Trigger copy shortcut ( Pressing CTRL+C )
|
||||||
*/
|
*/
|
||||||
|
|
23
packager.py
23
packager.py
|
@ -7,6 +7,7 @@ import click
|
||||||
import shutil
|
import shutil
|
||||||
import toml
|
import toml
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import glob
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@ -77,9 +78,23 @@ def build_windows(package_info):
|
||||||
TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win")
|
TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win")
|
||||||
os.makedirs(TARGET_DIR, exist_ok=True)
|
os.makedirs(TARGET_DIR, exist_ok=True)
|
||||||
|
|
||||||
print("Downloading Visual C++ redistributable")
|
print("Gathering CRT DLLs...")
|
||||||
vc_redist_file = os.path.join(TARGET_DIR, "vc_redist.x64.exe")
|
msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
|
||||||
urllib.request.urlretrieve("https://aka.ms/vs/16/release/vc_redist.x64.exe", vc_redist_file)
|
print("Found Redists: ", msvc_dirs)
|
||||||
|
|
||||||
|
msvc_dir = msvc_dirs[0]
|
||||||
|
print("Using: ",msvc_dir)
|
||||||
|
if len(msvc_dir) == 0:
|
||||||
|
raise Exception("Cannot find redistributable dlls")
|
||||||
|
dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll")
|
||||||
|
|
||||||
|
print("Found DLLs:")
|
||||||
|
dll_include_list = []
|
||||||
|
for dll in dll_files:
|
||||||
|
print("Including: "+dll)
|
||||||
|
dll_include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
|
||||||
|
|
||||||
|
dll_include = "\r\n".join(dll_include_list)
|
||||||
|
|
||||||
INSTALLER_NAME = f"espanso-win-installer"
|
INSTALLER_NAME = f"espanso-win-installer"
|
||||||
|
|
||||||
|
@ -100,6 +115,7 @@ def build_windows(package_info):
|
||||||
content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe"))
|
content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe"))
|
||||||
content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR))
|
content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR))
|
||||||
content = content.replace("{{{output_name}}}", INSTALLER_NAME)
|
content = content.replace("{{{output_name}}}", INSTALLER_NAME)
|
||||||
|
content = content.replace("{{{dll_include}}}", dll_include)
|
||||||
|
|
||||||
with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script:
|
with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script:
|
||||||
output_script.write(content)
|
output_script.write(content)
|
||||||
|
@ -174,6 +190,7 @@ def build_mac(package_info):
|
||||||
|
|
||||||
print("Done!")
|
print("Done!")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("[[ espanso packager ]]")
|
print("[[ espanso packager ]]")
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@ Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
ChangesEnvironment=yes
|
ChangesEnvironment=yes
|
||||||
AlwaysRestart = yes
|
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
@ -38,7 +37,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
[Files]
|
[Files]
|
||||||
Source: "{{{executable_path}}}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{{{executable_path}}}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{{{app_icon}}}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{{{app_icon}}}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "vc_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
|
{{{dll_include}}}
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
|
@ -53,18 +52,13 @@ Name: "StartMenuEntry" ; Description: "Start espanso at Windows startup" ;
|
||||||
const
|
const
|
||||||
ModPathName = 'modifypath';
|
ModPathName = 'modifypath';
|
||||||
ModPathType = 'user';
|
ModPathType = 'user';
|
||||||
|
|
||||||
function ModPathDir(): TArrayOfString;
|
function ModPathDir(): TArrayOfString;
|
||||||
begin
|
begin
|
||||||
setArrayLength(Result, 1)
|
setArrayLength(Result, 1)
|
||||||
Result[0] := ExpandConstant('{app}');
|
Result[0] := ExpandConstant('{app}');
|
||||||
end;
|
end;
|
||||||
#include "modpath.iss"
|
#include "modpath.iss"
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: {tmp}\vc_redist.x64.exe; \
|
|
||||||
Parameters: "/install /quiet /norestart"; \
|
|
||||||
StatusMsg: "Installing Visual C++ 2019 Redistributable";
|
|
||||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "start"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Parameters: "start"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
[UninstallRun]
|
[UninstallRun]
|
||||||
|
|
70
snapcraft.yaml
Normal file
70
snapcraft.yaml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
name: espanso
|
||||||
|
version: 0.5.1
|
||||||
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
|
description: |
|
||||||
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
||||||
|
## What is a Text Expander?
|
||||||
|
|
||||||
|
A text expander is a program that detects when you type
|
||||||
|
a specific keyword and replaces it with something else.
|
||||||
|
This is useful in many ways:
|
||||||
|
* Save a lot of typing, expanding common sentences.
|
||||||
|
* Create system-wide code snippets.
|
||||||
|
* Execute custom scripts
|
||||||
|
* Use emojis like a pro.
|
||||||
|
___
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
* Works on Windows, macOS and Linux
|
||||||
|
* Works with almost any program
|
||||||
|
* Works with Emojis 😄
|
||||||
|
* Works with Images
|
||||||
|
* Date expansion support
|
||||||
|
* Custom scripts support
|
||||||
|
* Shell commands support
|
||||||
|
* App-specific configurations
|
||||||
|
* Expandable with packages
|
||||||
|
* Built-in package manager for espanso hub: https://hub.espanso.org/
|
||||||
|
* File based configuration
|
||||||
|
|
||||||
|
## Get Started
|
||||||
|
|
||||||
|
Visit the official documentation: https://espanso.org/docs/
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you need some help to setup espanso, want to ask a question or simply get involved
|
||||||
|
in the community, Join the official Subreddit: https://www.reddit.com/r/espanso/
|
||||||
|
|
||||||
|
confinement: classic
|
||||||
|
base: core18
|
||||||
|
|
||||||
|
parts:
|
||||||
|
espanso:
|
||||||
|
plugin: rust
|
||||||
|
source: .
|
||||||
|
build-packages:
|
||||||
|
- libssl-dev
|
||||||
|
- pkg-config
|
||||||
|
- cmake
|
||||||
|
- libxtst-dev
|
||||||
|
- libx11-dev
|
||||||
|
- libxdo-dev
|
||||||
|
stage-packages:
|
||||||
|
- libx11-6
|
||||||
|
- libxau6
|
||||||
|
- libxcb1
|
||||||
|
- libxdmcp6
|
||||||
|
- libxdo3
|
||||||
|
- libxext6
|
||||||
|
- libxinerama1
|
||||||
|
- libxkbcommon0
|
||||||
|
- libxtst6
|
||||||
|
- libnotify-bin
|
||||||
|
- xclip
|
||||||
|
|
||||||
|
apps:
|
||||||
|
espanso:
|
||||||
|
command: bin/espanso
|
|
@ -44,5 +44,6 @@ extern {
|
||||||
pub fn trigger_terminal_paste();
|
pub fn trigger_terminal_paste();
|
||||||
pub fn trigger_shift_ins_paste();
|
pub fn trigger_shift_ins_paste();
|
||||||
pub fn trigger_alt_shift_ins_paste();
|
pub fn trigger_alt_shift_ins_paste();
|
||||||
|
pub fn trigger_ctrl_alt_paste();
|
||||||
pub fn trigger_copy();
|
pub fn trigger_copy();
|
||||||
}
|
}
|
|
@ -38,7 +38,7 @@ pub(crate) mod runtime;
|
||||||
const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml");
|
const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml");
|
||||||
|
|
||||||
pub const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml";
|
pub const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml";
|
||||||
const USER_CONFIGS_FOLDER_NAME: &str = "user";
|
pub const USER_CONFIGS_FOLDER_NAME: &str = "user";
|
||||||
|
|
||||||
// Default values for primitives
|
// Default values for primitives
|
||||||
fn default_name() -> String{ "default".to_owned() }
|
fn default_name() -> String{ "default".to_owned() }
|
||||||
|
@ -54,7 +54,7 @@ fn default_config_caching_interval() -> i32 { 800 }
|
||||||
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
|
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
|
||||||
fn default_toggle_interval() -> u32 { 230 }
|
fn default_toggle_interval() -> u32 { 230 }
|
||||||
fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
|
fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
|
||||||
fn default_preserve_clipboard() -> bool {false}
|
fn default_preserve_clipboard() -> bool {true}
|
||||||
fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
|
fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
|
||||||
fn default_passive_arg_delimiter() -> char { '/' }
|
fn default_passive_arg_delimiter() -> char { '/' }
|
||||||
fn default_passive_arg_escape() -> char { '\\' }
|
fn default_passive_arg_escape() -> char { '\\' }
|
||||||
|
@ -70,7 +70,7 @@ fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Configs {
|
pub struct Configs {
|
||||||
#[serde(default = "default_name")]
|
#[serde(default = "default_name")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[serde(default = "default_parent")]
|
#[serde(default = "default_parent")]
|
||||||
|
@ -253,10 +253,10 @@ impl Configs {
|
||||||
let mut merged_matches = new_config.matches;
|
let mut merged_matches = new_config.matches;
|
||||||
let mut match_trigger_set = HashSet::new();
|
let mut match_trigger_set = HashSet::new();
|
||||||
merged_matches.iter().for_each(|m| {
|
merged_matches.iter().for_each(|m| {
|
||||||
match_trigger_set.insert(m.trigger.clone());
|
match_trigger_set.extend(m.triggers.clone());
|
||||||
});
|
});
|
||||||
let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
|
let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
|
||||||
!match_trigger_set.contains(&m.trigger)
|
!m.triggers.iter().any(|trigger| match_trigger_set.contains(trigger))
|
||||||
}).cloned().collect();
|
}).cloned().collect();
|
||||||
|
|
||||||
merged_matches.extend(parent_matches);
|
merged_matches.extend(parent_matches);
|
||||||
|
@ -280,10 +280,10 @@ impl Configs {
|
||||||
// Merge matches
|
// Merge matches
|
||||||
let mut match_trigger_set = HashSet::new();
|
let mut match_trigger_set = HashSet::new();
|
||||||
self.matches.iter().for_each(|m| {
|
self.matches.iter().for_each(|m| {
|
||||||
match_trigger_set.insert(m.trigger.clone());
|
match_trigger_set.extend(m.triggers.clone());
|
||||||
});
|
});
|
||||||
let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
|
let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
|
||||||
!match_trigger_set.contains(&m.trigger)
|
!m.triggers.iter().any(|trigger| match_trigger_set.contains(trigger))
|
||||||
}).cloned().collect();
|
}).cloned().collect();
|
||||||
|
|
||||||
self.matches.extend(default_matches);
|
self.matches.extend(default_matches);
|
||||||
|
@ -465,16 +465,16 @@ impl ConfigSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_conflicts(default: &Configs, specific: &Vec<Configs>) -> bool {
|
fn has_conflicts(default: &Configs, specific: &Vec<Configs>) -> bool {
|
||||||
let mut sorted_triggers : Vec<String> = default.matches.iter().map(|t| {
|
let mut sorted_triggers : Vec<String> = default.matches.iter().flat_map(|t| {
|
||||||
t.trigger.clone()
|
t.triggers.clone()
|
||||||
}).collect();
|
}).collect();
|
||||||
sorted_triggers.sort();
|
sorted_triggers.sort();
|
||||||
|
|
||||||
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
|
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
|
||||||
|
|
||||||
for s in specific.iter() {
|
for s in specific.iter() {
|
||||||
let mut specific_triggers : Vec<String> = s.matches.iter().map(|t| {
|
let mut specific_triggers : Vec<String> = s.matches.iter().flat_map(|t| {
|
||||||
t.trigger.clone()
|
t.triggers.clone()
|
||||||
}).collect();
|
}).collect();
|
||||||
specific_triggers.sort();
|
specific_triggers.sort();
|
||||||
has_conflicts |= Self::list_has_conflicts(&specific_triggers);
|
has_conflicts |= Self::list_has_conflicts(&specific_triggers);
|
||||||
|
@ -831,9 +831,9 @@ mod tests {
|
||||||
assert_eq!(config_set.default.matches.len(), 2);
|
assert_eq!(config_set.default.matches.len(), 2);
|
||||||
assert_eq!(config_set.specific[0].matches.len(), 3);
|
assert_eq!(config_set.specific[0].matches.len(), 3);
|
||||||
|
|
||||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == "hello").is_some());
|
assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == "hello").is_some());
|
||||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":lol").is_some());
|
assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == ":lol").is_some());
|
||||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":yess").is_some());
|
assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == ":yess").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -860,12 +860,12 @@ mod tests {
|
||||||
|
|
||||||
assert!(config_set.specific[0].matches.iter().find(|x| {
|
assert!(config_set.specific[0].matches.iter().find(|x| {
|
||||||
if let MatchContentType::Text(content) = &x.content {
|
if let MatchContentType::Text(content) = &x.content {
|
||||||
x.trigger == ":lol" && content.replace == "newstring"
|
x.triggers[0] == ":lol" && content.replace == "newstring"
|
||||||
}else{
|
}else{
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}).is_some());
|
}).is_some());
|
||||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":yess").is_some());
|
assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == ":yess").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -894,7 +894,7 @@ mod tests {
|
||||||
|
|
||||||
assert!(config_set.specific[0].matches.iter().find(|x| {
|
assert!(config_set.specific[0].matches.iter().find(|x| {
|
||||||
if let MatchContentType::Text(content) = &x.content {
|
if let MatchContentType::Text(content) = &x.content {
|
||||||
x.trigger == "hello" && content.replace == "newstring"
|
x.triggers[0] == "hello" && content.replace == "newstring"
|
||||||
}else{
|
}else{
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -962,8 +962,8 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 0);
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
assert_eq!(config_set.default.matches.len(), 2);
|
assert_eq!(config_set.default.matches.len(), 2);
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hello"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -983,9 +983,9 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 1);
|
assert_eq!(config_set.specific.len(), 1);
|
||||||
assert_eq!(config_set.default.matches.len(), 1);
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||||
assert!(!config_set.default.matches.iter().any(|m| m.trigger == "hello"));
|
assert!(!config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "hello"));
|
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1016,9 +1016,9 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 0);
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
assert_eq!(config_set.default.matches.len(), 3);
|
assert_eq!(config_set.default.matches.len(), 3);
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hello"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "super"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "super"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1042,7 +1042,7 @@ mod tests {
|
||||||
assert_eq!(config_set.default.matches.len(), 1);
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
assert!(config_set.default.matches.iter().any(|m| {
|
assert!(config_set.default.matches.iter().any(|m| {
|
||||||
if let MatchContentType::Text(content) = &m.content {
|
if let MatchContentType::Text(content) = &m.content {
|
||||||
m.trigger == "hasta" && content.replace == "world"
|
m.triggers[0] == "hasta" && content.replace == "world"
|
||||||
}else{
|
}else{
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -1068,8 +1068,8 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 0);
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
assert_eq!(config_set.default.matches.len(), 2);
|
assert_eq!(config_set.default.matches.len(), 2);
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "harry"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "harry"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1089,8 +1089,8 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 1);
|
assert_eq!(config_set.specific.len(), 1);
|
||||||
assert_eq!(config_set.default.matches.len(), 1);
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry"));
|
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "harry"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1120,9 +1120,9 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 1);
|
assert_eq!(config_set.specific.len(), 1);
|
||||||
assert_eq!(config_set.default.matches.len(), 1);
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry"));
|
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "harry"));
|
||||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "ron"));
|
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "ron"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -23,7 +23,7 @@ use crate::event::*;
|
||||||
use crate::event::KeyModifier::*;
|
use crate::event::KeyModifier::*;
|
||||||
use crate::bridge::linux::*;
|
use crate::bridge::linux::*;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use log::{error, info};
|
use log::{debug, error, info};
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32,
|
||||||
(*_self).send_channel.send(event).unwrap();
|
(*_self).send_channel.send(event).unwrap();
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Unable to receive char: {}",e);
|
debug!("Unable to receive char: {}",e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}else{ // Modifier event
|
}else{ // Modifier event
|
||||||
|
|
63
src/edit.rs
Normal file
63
src/edit.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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 = Command::new(&editor)
|
||||||
|
.arg(file_path)
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ use crate::keyboard::KeyboardManager;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use crate::config::BackendType;
|
use crate::config::BackendType;
|
||||||
use crate::clipboard::ClipboardManager;
|
use crate::clipboard::ClipboardManager;
|
||||||
use log::{info, warn, error};
|
use log::{info, warn, debug, error};
|
||||||
use crate::ui::{UIManager, MenuItem, MenuItemType};
|
use crate::ui::{UIManager, MenuItem, MenuItemType};
|
||||||
use crate::event::{ActionEventReceiver, ActionType};
|
use crate::event::{ActionEventReceiver, ActionType};
|
||||||
use crate::extension::Extension;
|
use crate::extension::Extension;
|
||||||
|
@ -132,7 +132,7 @@ lazy_static! {
|
||||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||||
MatchReceiver for Engine<'a, S, C, M, U, R>{
|
MatchReceiver for Engine<'a, S, C, M, U, R>{
|
||||||
|
|
||||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
|
||||||
let config = self.config_manager.active_config();
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
if !config.enable_active {
|
if !config.enable_active {
|
||||||
|
@ -141,20 +141,21 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
|
|
||||||
// avoid espanso reinterpreting its own actions
|
// avoid espanso reinterpreting its own actions
|
||||||
if self.check_last_action_and_set(self.action_noop_interval) {
|
if self.check_last_action_and_set(self.action_noop_interval) {
|
||||||
|
debug!("Last action was too near, nooping the action.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let char_count = if trailing_separator.is_none() {
|
let char_count = if trailing_separator.is_none() {
|
||||||
m.trigger.chars().count() as i32
|
m.triggers[trigger_offset].chars().count() as i32
|
||||||
}else{
|
}else{
|
||||||
m.trigger.chars().count() as i32 + 1 // Count also the separator
|
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
|
||||||
};
|
};
|
||||||
|
|
||||||
self.keyboard_manager.delete_string(char_count);
|
self.keyboard_manager.delete_string(char_count);
|
||||||
|
|
||||||
let mut previous_clipboard_content : Option<String> = None;
|
let mut previous_clipboard_content : Option<String> = None;
|
||||||
|
|
||||||
let rendered = self.renderer.render_match(m, config, vec![]);
|
let rendered = self.renderer.render_match(m, trigger_offset, config, vec![]);
|
||||||
|
|
||||||
match rendered {
|
match rendered {
|
||||||
RenderResult::Text(mut target_string) => {
|
RenderResult::Text(mut target_string) => {
|
||||||
|
@ -233,7 +234,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
||||||
},
|
},
|
||||||
RenderResult::Error => {
|
RenderResult::Error => {
|
||||||
error!("Could not render match: {}", m.trigger);
|
error!("Could not render match: {}", m.triggers[trigger_offset]);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
src/extension/clipboard.rs
Normal file
43
src/extension/clipboard.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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 serde_yaml::{Mapping, Value};
|
||||||
|
use crate::clipboard::ClipboardManager;
|
||||||
|
|
||||||
|
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, params: &Mapping, _: &Vec<String>) -> Option<String> {
|
||||||
|
self.clipboard_manager.get_clipboard()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,24 +18,27 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
use crate::clipboard::ClipboardManager;
|
||||||
|
|
||||||
mod date;
|
mod date;
|
||||||
mod shell;
|
mod shell;
|
||||||
mod script;
|
mod script;
|
||||||
mod random;
|
mod random;
|
||||||
mod dummy;
|
mod clipboard;
|
||||||
|
pub mod dummy;
|
||||||
|
|
||||||
pub trait Extension {
|
pub trait Extension {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>;
|
fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_extensions() -> Vec<Box<dyn Extension>> {
|
pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<dyn Extension>>{
|
||||||
vec![
|
vec![
|
||||||
Box::new(date::DateExtension::new()),
|
Box::new(date::DateExtension::new()),
|
||||||
Box::new(shell::ShellExtension::new()),
|
Box::new(shell::ShellExtension::new()),
|
||||||
Box::new(script::ScriptExtension::new()),
|
Box::new(script::ScriptExtension::new()),
|
||||||
Box::new(random::RandomExtension::new()),
|
Box::new(random::RandomExtension::new()),
|
||||||
Box::new(dummy::DummyExtension::new()),
|
Box::new(dummy::DummyExtension::new()),
|
||||||
|
Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -52,6 +52,8 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
||||||
trigger_alt_shift_ins_paste();
|
trigger_alt_shift_ins_paste();
|
||||||
}else if is_special == 3 { // Special case for Emacs
|
}else if is_special == 3 { // Special case for Emacs
|
||||||
trigger_shift_ins_paste();
|
trigger_shift_ins_paste();
|
||||||
|
}else if is_special == 4 { // CTRL+ALT+V used in some terminals (urxvt)
|
||||||
|
trigger_ctrl_alt_paste();
|
||||||
}else{
|
}else{
|
||||||
trigger_terminal_paste();
|
trigger_terminal_paste();
|
||||||
}
|
}
|
||||||
|
@ -65,6 +67,9 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
||||||
PasteShortcut::ShiftInsert=> {
|
PasteShortcut::ShiftInsert=> {
|
||||||
trigger_shift_ins_paste();
|
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.")
|
error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ pub enum PasteShortcut {
|
||||||
CtrlV, // Classic Ctrl+V shortcut
|
CtrlV, // Classic Ctrl+V shortcut
|
||||||
CtrlShiftV, // Could be used to paste without formatting in many applications
|
CtrlShiftV, // Could be used to paste without formatting in many applications
|
||||||
ShiftInsert, // Often used in Linux systems
|
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
|
MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
85
src/main.rs
85
src/main.rs
|
@ -46,6 +46,7 @@ use crate::package::default::DefaultPackageManager;
|
||||||
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
mod edit;
|
||||||
mod event;
|
mod event;
|
||||||
mod check;
|
mod check;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
@ -96,6 +97,10 @@ fn main() {
|
||||||
.subcommand(SubCommand::with_name("toggle")
|
.subcommand(SubCommand::with_name("toggle")
|
||||||
.about("Toggle the status of the espanso replacement engine."))
|
.about("Toggle the status of the espanso replacement engine."))
|
||||||
)
|
)
|
||||||
|
.subcommand(SubCommand::with_name("edit")
|
||||||
|
.about("Open the default text editor to edit config files and reload them automatically when exiting")
|
||||||
|
.arg(Arg::with_name("config")
|
||||||
|
.help("Defaults to \"default\". The configuration file name to edit (without the .yml extension).")))
|
||||||
.subcommand(SubCommand::with_name("dump")
|
.subcommand(SubCommand::with_name("dump")
|
||||||
.about("Prints all current configuration options."))
|
.about("Prints all current configuration options."))
|
||||||
.subcommand(SubCommand::with_name("detect")
|
.subcommand(SubCommand::with_name("detect")
|
||||||
|
@ -146,6 +151,14 @@ fn main() {
|
||||||
|
|
||||||
let matches = clap_instance.clone().get_matches();
|
let matches = clap_instance.clone().get_matches();
|
||||||
|
|
||||||
|
// The edit subcommand must be run before the configuration parsing. Otherwise, if the
|
||||||
|
// configuration is corrupted, the edit command won't work, which makes it pretty useless.
|
||||||
|
if let Some(matches) = matches.subcommand_matches("edit") {
|
||||||
|
edit_main(matches);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let log_level = matches.occurrences_of("v") as i32;
|
let log_level = matches.occurrences_of("v") as i32;
|
||||||
|
|
||||||
// Load the configuration
|
// Load the configuration
|
||||||
|
@ -156,7 +169,7 @@ fn main() {
|
||||||
|
|
||||||
config_set.default.log_level = log_level;
|
config_set.default.log_level = log_level;
|
||||||
|
|
||||||
// Match the correct subcommand
|
// Commands that require the configuration
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("cmd") {
|
if let Some(matches) = matches.subcommand_matches("cmd") {
|
||||||
cmd_main(config_set, matches);
|
cmd_main(config_set, matches);
|
||||||
|
@ -331,7 +344,7 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
|
||||||
|
|
||||||
let keyboard_manager = keyboard::get_manager();
|
let keyboard_manager = keyboard::get_manager();
|
||||||
|
|
||||||
let extensions = extension::get_extensions();
|
let extensions = extension::get_extensions(Box::new(clipboard::get_manager()));
|
||||||
|
|
||||||
let renderer = render::default::DefaultRenderer::new(extensions,
|
let renderer = render::default::DefaultRenderer::new(extensions,
|
||||||
config_manager.default_config().clone());
|
config_manager.default_config().clone());
|
||||||
|
@ -873,6 +886,74 @@ fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edit_main(matches: &ArgMatches) {
|
||||||
|
// Determine which is the file to edit
|
||||||
|
let config = matches.value_of("config").unwrap_or("default");
|
||||||
|
|
||||||
|
let config_dir = crate::context::get_config_dir();
|
||||||
|
|
||||||
|
let config_path = match config {
|
||||||
|
"default" => {
|
||||||
|
config_dir.join(crate::config::DEFAULT_CONFIG_FILE_NAME)
|
||||||
|
},
|
||||||
|
name => { // Otherwise, search in the user/ config folder
|
||||||
|
config_dir.join(crate::config::USER_CONFIGS_FOLDER_NAME)
|
||||||
|
.join(name.to_owned() + ".yml")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Editing file: {:?}", &config_path);
|
||||||
|
|
||||||
|
// Based on the fact that the file already exists or not, we should detect in different
|
||||||
|
// ways if a reload is needed
|
||||||
|
let should_reload =if config_path.exists() {
|
||||||
|
// Get the last modified date, so that we can detect if the user actually edits the file
|
||||||
|
// before reloading
|
||||||
|
let metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
|
||||||
|
let last_modified = metadata.modified().expect("cannot read file last modified date");
|
||||||
|
|
||||||
|
let result = crate::edit::open_editor(&config_path);
|
||||||
|
if result {
|
||||||
|
let new_metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
|
||||||
|
let new_last_modified = new_metadata.modified().expect("cannot read file last modified date");
|
||||||
|
|
||||||
|
if last_modified != new_last_modified {
|
||||||
|
println!("File has been modified, reloading configuration");
|
||||||
|
true
|
||||||
|
}else{
|
||||||
|
println!("File has not been modified, avoiding reload");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
let result = crate::edit::open_editor(&config_path);
|
||||||
|
if result {
|
||||||
|
// If the file has been created, we should reload the espanso config
|
||||||
|
if config_path.exists() {
|
||||||
|
println!("A new file has been created, reloading configuration");
|
||||||
|
true
|
||||||
|
}else{
|
||||||
|
println!("No file has been created, avoiding reload");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_reload {
|
||||||
|
// Load the configuration
|
||||||
|
let mut config_set = ConfigSet::load_default().unwrap_or_else(|e| {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
eprintln!("Unable to reload espanso due to previous configuration error.");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
restart_main(config_set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn acquire_lock() -> Option<File> {
|
fn acquire_lock() -> Option<File> {
|
||||||
let espanso_dir = context::get_data_dir();
|
let espanso_dir = context::get_data_dir();
|
||||||
|
|
|
@ -29,14 +29,15 @@ pub(crate) mod scrolling;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct Match {
|
pub struct Match {
|
||||||
pub trigger: String,
|
pub triggers: Vec<String>,
|
||||||
pub content: MatchContentType,
|
pub content: MatchContentType,
|
||||||
pub word: bool,
|
pub word: bool,
|
||||||
pub passive_only: bool,
|
pub passive_only: bool,
|
||||||
|
pub propagate_case: bool,
|
||||||
|
|
||||||
// Automatically calculated from the trigger, used by the matcher to check for correspondences.
|
// Automatically calculated from the triggers, used by the matcher to check for correspondences.
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub _trigger_sequence: Vec<TriggerEntry>,
|
pub _trigger_sequences: Vec<Vec<TriggerEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
@ -74,18 +75,49 @@ 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 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)
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate the trigger sequence
|
// If propagate_case is true, we need to generate all the possible triggers
|
||||||
let mut trigger_sequence = Vec::new();
|
// For example, specifying "hello" as a trigger, we need to have:
|
||||||
let trigger_chars : Vec<char> = other.trigger.chars().collect();
|
// "hello", "Hello", "HELLO"
|
||||||
trigger_sequence.extend(trigger_chars.into_iter().map(|c| {
|
if other.propagate_case {
|
||||||
TriggerEntry::Char(c)
|
// List with first letter capitalized
|
||||||
}));
|
let first_capitalized : Vec<String> = triggers.iter().map(|trigger| {
|
||||||
if other.word { // If it's a word match, end with a word separator
|
let mut capitalized = trigger.clone();
|
||||||
trigger_sequence.push(TriggerEntry::WordSeparator);
|
let mut v: Vec<char> = capitalized.chars().collect();
|
||||||
|
v[0] = v[0].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 content = if let Some(replace) = &other.replace { // Text match
|
let content = if let Some(replace) = &other.replace { // Text match
|
||||||
let new_replace = replace.clone();
|
let new_replace = replace.clone();
|
||||||
|
|
||||||
|
@ -132,11 +164,12 @@ impl<'a> From<&'a AutoMatch> for Match{
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
trigger: other.trigger.clone(),
|
triggers,
|
||||||
content,
|
content,
|
||||||
word: other.word,
|
word: other.word,
|
||||||
passive_only: other.passive_only,
|
passive_only: other.passive_only,
|
||||||
_trigger_sequence: trigger_sequence,
|
_trigger_sequences: trigger_sequences,
|
||||||
|
propagate_case: other.propagate_case,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,8 +177,12 @@ impl<'a> From<&'a AutoMatch> for Match{
|
||||||
/// Used to deserialize the Match struct before applying some custom elaboration.
|
/// Used to deserialize the Match struct before applying some custom elaboration.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
struct AutoMatch {
|
struct AutoMatch {
|
||||||
|
#[serde(default = "default_trigger")]
|
||||||
pub trigger: String,
|
pub trigger: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_triggers")]
|
||||||
|
pub triggers: Vec<String>,
|
||||||
|
|
||||||
#[serde(default = "default_replace")]
|
#[serde(default = "default_replace")]
|
||||||
pub replace: Option<String>,
|
pub replace: Option<String>,
|
||||||
|
|
||||||
|
@ -160,13 +197,19 @@ struct AutoMatch {
|
||||||
|
|
||||||
#[serde(default = "default_passive_only")]
|
#[serde(default = "default_passive_only")]
|
||||||
pub passive_only: bool,
|
pub passive_only: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_propagate_case")]
|
||||||
|
pub propagate_case: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_trigger() -> String {"".to_owned()}
|
||||||
|
fn default_triggers() -> Vec<String> {Vec::new()}
|
||||||
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
||||||
fn default_word() -> bool {false}
|
fn default_word() -> bool {false}
|
||||||
fn default_passive_only() -> bool {false}
|
fn default_passive_only() -> bool {false}
|
||||||
fn default_replace() -> Option<String> {None}
|
fn default_replace() -> Option<String> {None}
|
||||||
fn default_image_path() -> Option<String> {None}
|
fn default_image_path() -> Option<String> {None}
|
||||||
|
fn default_propagate_case() -> bool {false}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MatchVariable {
|
pub struct MatchVariable {
|
||||||
|
@ -175,9 +218,12 @@ pub struct MatchVariable {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub var_type: String,
|
pub var_type: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_params")]
|
||||||
pub params: Mapping,
|
pub params: Mapping,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_params() -> Mapping {Mapping::new()}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub enum TriggerEntry {
|
pub enum TriggerEntry {
|
||||||
Char(char),
|
Char(char),
|
||||||
|
@ -185,7 +231,7 @@ pub enum TriggerEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MatchReceiver {
|
pub trait MatchReceiver {
|
||||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>);
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize);
|
||||||
fn on_enable_update(&self, status: bool);
|
fn on_enable_update(&self, status: bool);
|
||||||
fn on_passive(&self);
|
fn on_passive(&self);
|
||||||
}
|
}
|
||||||
|
@ -281,10 +327,10 @@ mod tests {
|
||||||
|
|
||||||
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t'));
|
assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
|
||||||
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e'));
|
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
|
||||||
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s'));
|
assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
|
||||||
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t'));
|
assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -297,11 +343,11 @@ mod tests {
|
||||||
|
|
||||||
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
|
||||||
assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t'));
|
assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
|
||||||
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e'));
|
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
|
||||||
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s'));
|
assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
|
||||||
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t'));
|
assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
|
||||||
assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator);
|
assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -322,4 +368,108 @@ mod tests {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -39,6 +39,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
||||||
struct MatchEntry<'a> {
|
struct MatchEntry<'a> {
|
||||||
start: usize,
|
start: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
trigger_offset: usize, // The index of the trigger in the Match that matched
|
||||||
_match: &'a Match
|
_match: &'a Match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,8 +74,8 @@ 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 {
|
fn is_matching(mtc: &Match, current_char: &str, start: usize, trigger_offset: usize, is_current_word_separator: bool) -> bool {
|
||||||
match mtc._trigger_sequence[start] {
|
match mtc._trigger_sequences[trigger_offset][start] {
|
||||||
TriggerEntry::Char(c) => {
|
TriggerEntry::Char(c) => {
|
||||||
current_char.starts_with(c)
|
current_char.starts_with(c)
|
||||||
},
|
},
|
||||||
|
@ -112,38 +113,43 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
|
|
||||||
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> = active_config.matches.iter()
|
let mut new_matches: Vec<MatchEntry> = Vec::new();
|
||||||
.filter(|&x| {
|
|
||||||
// only active-enabled matches are considered
|
|
||||||
if x.passive_only {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = Self::is_matching(x, c, 0, is_current_word_separator);
|
for m in active_config.matches.iter() {
|
||||||
|
// only active-enabled matches are considered
|
||||||
|
if m.passive_only {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if x.word {
|
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
|
result = result && *was_previous_word_separator
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
if result {
|
||||||
})
|
new_matches.push(MatchEntry{
|
||||||
.map(|x | MatchEntry{
|
start: 1,
|
||||||
start: 1,
|
count: m._trigger_sequences[trigger_offset].len(),
|
||||||
count: x._trigger_sequence.len(),
|
trigger_offset,
|
||||||
_match: &x
|
_match: &m
|
||||||
})
|
});
|
||||||
.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_mut() {
|
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| {
|
||||||
Self::is_matching(x._match, c, x.start, is_current_word_separator)
|
Self::is_matching(x._match, c, x.start, x.trigger_offset, is_current_word_separator)
|
||||||
})
|
})
|
||||||
.map(|x | MatchEntry{
|
.map(|x | MatchEntry{
|
||||||
start: x.start+1,
|
start: x.start+1,
|
||||||
count: x.count,
|
count: x.count,
|
||||||
|
trigger_offset: x.trigger_offset,
|
||||||
_match: &x._match
|
_match: &x._match
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -154,11 +160,11 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
None => {new_matches},
|
None => {new_matches},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut found_match = None;
|
let mut found_entry = None;
|
||||||
|
|
||||||
for entry in combined_matches.iter() {
|
for entry in combined_matches.iter() {
|
||||||
if entry.start == entry.count {
|
if entry.start == entry.count {
|
||||||
found_match = Some(entry._match);
|
found_entry = Some(entry.clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +177,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
|
|
||||||
*was_previous_word_separator = is_current_word_separator;
|
*was_previous_word_separator = is_current_word_separator;
|
||||||
|
|
||||||
if let Some(mtc) = found_match {
|
if let Some(entry) = found_entry {
|
||||||
|
let mtc = entry._match;
|
||||||
|
|
||||||
if let Some(last) = current_set_queue.back_mut() {
|
if let Some(last) = current_set_queue.back_mut() {
|
||||||
last.clear();
|
last.clear();
|
||||||
}
|
}
|
||||||
|
@ -194,7 +202,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
// Force espanso to consider the last char as a separator
|
// Force espanso to consider the last char as a separator
|
||||||
*was_previous_word_separator = true;
|
*was_previous_word_separator = true;
|
||||||
|
|
||||||
self.receiver.on_match(mtc, trailing_separator);
|
self.receiver.on_match(mtc, trailing_separator, entry.trigger_offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ use crate::extension::Extension;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
||||||
|
static ref UNKNOWN_VARIABLE : String = "".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DefaultRenderer {
|
pub struct DefaultRenderer {
|
||||||
|
@ -58,15 +59,18 @@ impl DefaultRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_match(config: &Configs, trigger: &str) -> Option<Match> {
|
fn find_match(config: &Configs, trigger: &str) -> Option<(Match, usize)> {
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
|
||||||
// TODO: if performances become a problem, implement a more efficient lookup
|
// TODO: if performances become a problem, implement a more efficient lookup
|
||||||
for m in config.matches.iter() {
|
for m in config.matches.iter() {
|
||||||
if m.trigger == trigger {
|
for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() {
|
||||||
result = Some(m.clone());
|
if m_trigger == trigger {
|
||||||
break;
|
result = Some((m.clone(), trigger_offset));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
|
@ -74,7 +78,7 @@ impl DefaultRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Renderer for DefaultRenderer {
|
impl super::Renderer for DefaultRenderer {
|
||||||
fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult {
|
fn render_match(&self, m: &Match, trigger_offset: usize, config: &Configs, args: Vec<String>) -> RenderResult {
|
||||||
// Manage the different types of matches
|
// Manage the different types of matches
|
||||||
match &m.content {
|
match &m.content {
|
||||||
// Text Match
|
// Text Match
|
||||||
|
@ -103,11 +107,11 @@ impl super::Renderer for DefaultRenderer {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let inner_match = inner_match.unwrap();
|
let (inner_match, trigger_offset) = inner_match.unwrap();
|
||||||
|
|
||||||
// Render the inner match
|
// Render the inner match
|
||||||
// TODO: inner arguments
|
// TODO: inner arguments
|
||||||
let result = self.render_match(&inner_match, config, vec![]);
|
let result = self.render_match(&inner_match, trigger_offset, config, vec![]);
|
||||||
|
|
||||||
// Inner matches are only supported for text-expansions, warn the user otherwise
|
// Inner matches are only supported for text-expansions, warn the user otherwise
|
||||||
match result {
|
match result {
|
||||||
|
@ -138,7 +142,7 @@ impl super::Renderer for DefaultRenderer {
|
||||||
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
||||||
let var_name = caps.name("name").unwrap().as_str();
|
let var_name = caps.name("name").unwrap().as_str();
|
||||||
let output = output_map.get(var_name);
|
let output = output_map.get(var_name);
|
||||||
output.unwrap()
|
output.unwrap_or(&UNKNOWN_VARIABLE)
|
||||||
});
|
});
|
||||||
|
|
||||||
result.to_string()
|
result.to_string()
|
||||||
|
@ -146,9 +150,55 @@ impl super::Renderer for DefaultRenderer {
|
||||||
content.replace.clone()
|
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 target_string = target_string.replace("\\{", "{")
|
||||||
|
.replace("\\}", "}");
|
||||||
|
|
||||||
// Render any argument that may be present
|
// Render any argument that may be present
|
||||||
let target_string = utils::render_args(&target_string, &args);
|
let target_string = utils::render_args(&target_string, &args);
|
||||||
|
|
||||||
|
// Handle case propagation
|
||||||
|
let target_string = if m.propagate_case {
|
||||||
|
let trigger = &m.triggers[trigger_offset];
|
||||||
|
let first_char = trigger.chars().nth(0);
|
||||||
|
let second_char = trigger.chars().nth(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)
|
RenderResult::Text(target_string)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -196,9 +246,9 @@ impl super::Renderer for DefaultRenderer {
|
||||||
config.passive_arg_delimiter,
|
config.passive_arg_delimiter,
|
||||||
config.passive_arg_escape);
|
config.passive_arg_escape);
|
||||||
|
|
||||||
let m = m.unwrap();
|
let (m, trigger_offset) = m.unwrap();
|
||||||
// Render the actual match
|
// Render the actual match
|
||||||
let result = self.render_match(&m, &config, args);
|
let result = self.render_match(&m, trigger_offset, &config, args);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
RenderResult::Text(out) => {
|
RenderResult::Text(out) => {
|
||||||
|
@ -221,7 +271,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn get_renderer(config: Configs) -> DefaultRenderer {
|
fn get_renderer(config: Configs) -> DefaultRenderer {
|
||||||
DefaultRenderer::new(crate::extension::get_extensions(), config)
|
DefaultRenderer::new(vec![Box::new(crate::extension::dummy::DummyExtension::new())], config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_config_for(s: &str) -> Configs {
|
fn get_config_for(s: &str) -> Configs {
|
||||||
|
@ -481,4 +531,132 @@ mod tests {
|
||||||
|
|
||||||
verify_render(rendered, "this is my local");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ pub(crate) mod utils;
|
||||||
|
|
||||||
pub trait Renderer {
|
pub trait Renderer {
|
||||||
// Render a match output
|
// Render a match output
|
||||||
fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult;
|
fn render_match(&self, m: &Match, trigger_offset: usize, config: &Configs, args: Vec<String>) -> RenderResult;
|
||||||
|
|
||||||
// Render a passive expansion text
|
// Render a passive expansion text
|
||||||
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
|
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user