Add experimental backspace undo feature
This commit is contained in:
parent
a6b78e7142
commit
61c17b26a4
|
@ -131,6 +131,9 @@ fn default_show_notifications() -> bool {
|
||||||
fn default_auto_restart() -> bool {
|
fn default_auto_restart() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
fn default_undo_backspace() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
fn default_show_icon() -> bool {
|
fn default_show_icon() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -215,6 +218,9 @@ pub struct Configs {
|
||||||
#[serde(default = "default_enable_active")]
|
#[serde(default = "default_enable_active")]
|
||||||
pub enable_active: bool,
|
pub enable_active: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_undo_backspace")]
|
||||||
|
pub undo_backspace: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub paste_shortcut: PasteShortcut,
|
pub paste_shortcut: PasteShortcut,
|
||||||
|
|
||||||
|
|
132
src/engine.rs
132
src/engine.rs
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
use crate::clipboard::ClipboardManager;
|
use crate::clipboard::ClipboardManager;
|
||||||
use crate::config::BackendType;
|
use crate::config::BackendType;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::{Configs, ConfigManager};
|
||||||
use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
|
use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
|
||||||
use crate::keyboard::KeyboardManager;
|
use crate::keyboard::KeyboardManager;
|
||||||
use crate::matcher::{Match, MatchReceiver};
|
use crate::matcher::{Match, MatchReceiver};
|
||||||
|
@ -50,6 +50,8 @@ pub struct Engine<
|
||||||
is_injecting: Arc<AtomicBool>,
|
is_injecting: Arc<AtomicBool>,
|
||||||
|
|
||||||
enabled: RefCell<bool>,
|
enabled: RefCell<bool>,
|
||||||
|
// Trigger string and injected text len pair
|
||||||
|
last_expansion_data: RefCell<Option<(String, i32)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<
|
||||||
|
@ -70,6 +72,7 @@ impl<
|
||||||
is_injecting: Arc<AtomicBool>,
|
is_injecting: Arc<AtomicBool>,
|
||||||
) -> Engine<'a, S, C, M, U, R> {
|
) -> Engine<'a, S, C, M, U, R> {
|
||||||
let enabled = RefCell::new(true);
|
let enabled = RefCell::new(true);
|
||||||
|
let last_expansion_data = RefCell::new(None);
|
||||||
|
|
||||||
Engine {
|
Engine {
|
||||||
keyboard_manager,
|
keyboard_manager,
|
||||||
|
@ -79,6 +82,7 @@ impl<
|
||||||
renderer,
|
renderer,
|
||||||
is_injecting,
|
is_injecting,
|
||||||
enabled,
|
enabled,
|
||||||
|
last_expansion_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,17 +151,63 @@ impl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inject_text(&self, config: &Configs, target_string: &str, force_clipboard: bool) {
|
||||||
|
let backend = if force_clipboard {
|
||||||
|
&BackendType::Clipboard
|
||||||
|
} else if config.backend == BackendType::Auto {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
||||||
|
if all_ascii {
|
||||||
|
debug!(
|
||||||
|
"All elements of the replacement are ascii, using Inject backend"
|
||||||
|
);
|
||||||
|
&BackendType::Inject
|
||||||
|
} else {
|
||||||
|
debug!("There are non-ascii characters, using Clipboard backend");
|
||||||
|
&BackendType::Clipboard
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&BackendType::Inject
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&config.backend
|
||||||
|
};
|
||||||
|
|
||||||
|
match backend {
|
||||||
|
BackendType::Inject => {
|
||||||
|
// To handle newlines, substitute each "\n" char with an Enter key press.
|
||||||
|
let splits = target_string.split('\n');
|
||||||
|
|
||||||
|
for (i, split) in splits.enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
self.keyboard_manager.send_enter(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keyboard_manager.send_string(&config, split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendType::Clipboard => {
|
||||||
|
self.clipboard_manager.set_clipboard(&target_string);
|
||||||
|
self.keyboard_manager.trigger_paste(&config);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Unsupported backend type evaluation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn inject_match(
|
fn inject_match(
|
||||||
&self,
|
&self,
|
||||||
m: &Match,
|
m: &Match,
|
||||||
trailing_separator: Option<char>,
|
trailing_separator: Option<char>,
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
skip_delete: bool,
|
skip_delete: bool,
|
||||||
) {
|
) -> Option<(String, i32)> {
|
||||||
let config = self.config_manager.active_config();
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
if !config.enable_active {
|
if !config.enable_active {
|
||||||
return;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block espanso from reinterpreting its own actions
|
// Block espanso from reinterpreting its own actions
|
||||||
|
@ -179,6 +229,8 @@ impl<
|
||||||
.renderer
|
.renderer
|
||||||
.render_match(m, trigger_offset, config, vec![]);
|
.render_match(m, trigger_offset, config, vec![]);
|
||||||
|
|
||||||
|
let mut expansion_data: Option<(String, i32)> = None;
|
||||||
|
|
||||||
match rendered {
|
match rendered {
|
||||||
RenderResult::Text(mut target_string) => {
|
RenderResult::Text(mut target_string) => {
|
||||||
// If a trailing separator was counted in the match, add it back to the target string
|
// If a trailing separator was counted in the match, add it back to the target string
|
||||||
|
@ -213,54 +265,13 @@ impl<
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let backend = if m.force_clipboard {
|
// If the preserve_clipboard option is enabled, save the current
|
||||||
&BackendType::Clipboard
|
// clipboard content to restore it later.
|
||||||
} else if config.backend == BackendType::Auto {
|
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
|
||||||
if all_ascii {
|
|
||||||
debug!(
|
|
||||||
"All elements of the replacement are ascii, using Inject backend"
|
|
||||||
);
|
|
||||||
&BackendType::Inject
|
|
||||||
} else {
|
|
||||||
debug!("There are non-ascii characters, using Clipboard backend");
|
|
||||||
&BackendType::Clipboard
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&BackendType::Inject
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&config.backend
|
|
||||||
};
|
|
||||||
|
|
||||||
match backend {
|
self.inject_text(&config, &target_string, m.force_clipboard);
|
||||||
BackendType::Inject => {
|
|
||||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
|
||||||
let splits = target_string.split('\n');
|
|
||||||
|
|
||||||
for (i, split) in splits.enumerate() {
|
expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32));
|
||||||
if i > 0 {
|
|
||||||
self.keyboard_manager.send_enter(&config);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.keyboard_manager.send_string(&config, split);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackendType::Clipboard => {
|
|
||||||
// If the preserve_clipboard option is enabled, save the current
|
|
||||||
// clipboard content to restore it later.
|
|
||||||
previous_clipboard_content =
|
|
||||||
self.return_content_if_preserve_clipboard_is_enabled();
|
|
||||||
|
|
||||||
self.clipboard_manager.set_clipboard(&target_string);
|
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("Unsupported backend type evaluation.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(moves) = cursor_rewind {
|
if let Some(moves) = cursor_rewind {
|
||||||
// Simulate left arrow key presses to bring the cursor into the desired position
|
// Simulate left arrow key presses to bring the cursor into the desired position
|
||||||
|
@ -294,6 +305,8 @@ impl<
|
||||||
|
|
||||||
// Re-allow espanso to interpret actions
|
// Re-allow espanso to interpret actions
|
||||||
self.is_injecting.store(false, Release);
|
self.is_injecting.store(false, Release);
|
||||||
|
|
||||||
|
expansion_data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +324,26 @@ impl<
|
||||||
> 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>, trigger_offset: usize) {
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
|
||||||
self.inject_match(m, trailing_separator, trigger_offset, false);
|
let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false);
|
||||||
|
let mut last_expansion_data = self.last_expansion_data.borrow_mut();
|
||||||
|
(*last_expansion_data) = expansion_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_undo(&self) {
|
||||||
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
|
if !config.undo_backspace {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_expansion_data = self.last_expansion_data.borrow();
|
||||||
|
if let Some(ref last_expansion_data) = *last_expansion_data {
|
||||||
|
let (trigger_string, injected_text_len) = last_expansion_data;
|
||||||
|
// Delete the previously injected text, minus one character as it has been consumed by the backspace
|
||||||
|
self.keyboard_manager.delete_string(&config, *injected_text_len - 1);
|
||||||
|
// Restore previous text
|
||||||
|
self.inject_text(&config, trigger_string, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enable_update(&self, status: bool) {
|
fn on_enable_update(&self, status: bool) {
|
||||||
|
|
|
@ -273,6 +273,7 @@ pub trait MatchReceiver {
|
||||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize);
|
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);
|
||||||
|
fn on_undo(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Matcher: KeyEventReceiver {
|
pub trait Matcher: KeyEventReceiver {
|
||||||
|
|
|
@ -33,6 +33,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
||||||
passive_press_time: RefCell<SystemTime>,
|
passive_press_time: RefCell<SystemTime>,
|
||||||
is_enabled: RefCell<bool>,
|
is_enabled: RefCell<bool>,
|
||||||
was_previous_char_word_separator: RefCell<bool>,
|
was_previous_char_word_separator: RefCell<bool>,
|
||||||
|
was_previous_char_a_match: RefCell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -57,6 +58,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
||||||
passive_press_time,
|
passive_press_time,
|
||||||
is_enabled: RefCell::new(true),
|
is_enabled: RefCell::new(true),
|
||||||
was_previous_char_word_separator: RefCell::new(true),
|
was_previous_char_word_separator: RefCell::new(true),
|
||||||
|
was_previous_char_a_match: RefCell::new(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +113,9 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
||||||
|
(*was_previous_char_a_match) = false;
|
||||||
|
|
||||||
let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
|
let mut 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();
|
||||||
|
@ -192,9 +197,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
if let Some(entry) = found_entry {
|
if let Some(entry) = found_entry {
|
||||||
let mtc = entry._match;
|
let mtc = entry._match;
|
||||||
|
|
||||||
if let Some(last) = current_set_queue.back_mut() {
|
current_set_queue.clear();
|
||||||
last.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
let trailing_separator = if !mtc.word {
|
let trailing_separator = if !mtc.word {
|
||||||
// If it's not a word match, it cannot have a trailing separator
|
// If it's not a word match, it cannot have a trailing separator
|
||||||
|
@ -216,12 +219,17 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
|
|
||||||
self.receiver
|
self.receiver
|
||||||
.on_match(mtc, trailing_separator, entry.trigger_offset);
|
.on_match(mtc, trailing_separator, entry.trigger_offset);
|
||||||
|
|
||||||
|
|
||||||
|
(*was_previous_char_a_match) = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_modifier(&self, m: KeyModifier) {
|
fn handle_modifier(&self, m: KeyModifier) {
|
||||||
let config = self.config_manager.default_config();
|
let config = self.config_manager.default_config();
|
||||||
|
|
||||||
|
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
||||||
|
|
||||||
// TODO: at the moment, activating the passive key triggers the toggle key
|
// TODO: at the moment, activating the passive key triggers the toggle key
|
||||||
// study a mechanism to avoid this problem
|
// study a mechanism to avoid this problem
|
||||||
|
|
||||||
|
@ -253,8 +261,16 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
if m == BACKSPACE {
|
if m == BACKSPACE {
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
||||||
current_set_queue.pop_back();
|
current_set_queue.pop_back();
|
||||||
|
|
||||||
|
if (*was_previous_char_a_match) {
|
||||||
|
current_set_queue.clear();
|
||||||
|
self.receiver.on_undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable the "backspace undo" feature
|
||||||
|
(*was_previous_char_a_match) = false;
|
||||||
|
|
||||||
// Consider modifiers as separators to improve word matches reliability
|
// Consider modifiers as separators to improve word matches reliability
|
||||||
if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
|
if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
|
||||||
let mut was_previous_char_word_separator =
|
let mut was_previous_char_word_separator =
|
||||||
|
@ -269,6 +285,10 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
|
||||||
let mut was_previous_char_word_separator =
|
let mut was_previous_char_word_separator =
|
||||||
self.was_previous_char_word_separator.borrow_mut();
|
self.was_previous_char_word_separator.borrow_mut();
|
||||||
*was_previous_char_word_separator = true;
|
*was_previous_char_word_separator = true;
|
||||||
|
|
||||||
|
// Disable the "backspace undo" feature
|
||||||
|
let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut();
|
||||||
|
(*was_previous_char_a_match) = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user