mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
feat(snippets): Implement core expansion engine
This commit implements implements the `inject_text` method using the `enigo` crate for input simulation. A new `ExpansionEngine` is created to listen for keyboard input, maintain a buffer of recent characters, and check against the database for a matching keyword to trigger an expansion.
This commit is contained in:
parent
d72b4dd450
commit
7088cb17fd
7 changed files with 186 additions and 16 deletions
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
@ -4441,6 +4441,7 @@ dependencies = [
|
|||
"hex",
|
||||
"image",
|
||||
"keyring",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
|
|
|
@ -55,6 +55,7 @@ trash = "5.2.2"
|
|||
rdev = "0.5.3"
|
||||
evdev = "0.13.1"
|
||||
anyhow = "1.0.98"
|
||||
lazy_static = "1.5.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
|
|
@ -18,9 +18,11 @@ use browser_extension::WsState;
|
|||
use frecency::FrecencyManager;
|
||||
use quicklinks::QuicklinkManager;
|
||||
use selection::get_text;
|
||||
use snippets::engine::ExpansionEngine;
|
||||
use snippets::input_manager::{InputManager, RdevInputManager};
|
||||
use snippets::manager::SnippetManager;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::{Emitter, Manager};
|
||||
|
@ -155,14 +157,17 @@ fn setup_global_shortcut(app: &mut tauri::App) -> Result<(), Box<dyn std::error:
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_input_listener() {
|
||||
fn setup_input_listener(app: &tauri::AppHandle) {
|
||||
let snippet_manager = app.state::<SnippetManager>().inner().clone();
|
||||
let snippet_manager_arc = Arc::new(snippet_manager);
|
||||
|
||||
let input_manager = RdevInputManager::new();
|
||||
let input_manager_arc = Arc::new(input_manager);
|
||||
|
||||
let engine = ExpansionEngine::new(snippet_manager_arc, input_manager_arc);
|
||||
thread::spawn(move || {
|
||||
let manager = RdevInputManager::new();
|
||||
let callback = |event| {
|
||||
println!("[InputManager] Received Key: {:?}", event);
|
||||
};
|
||||
if let Err(e) = manager.start_listening(Box::new(callback)) {
|
||||
eprintln!("[InputManager] Failed to start: {}", e);
|
||||
if let Err(e) = engine.start_listening() {
|
||||
eprintln!("[ExpansionEngine] Failed to start: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -256,7 +261,7 @@ pub fn run() {
|
|||
|
||||
setup_background_refresh();
|
||||
setup_global_shortcut(app)?;
|
||||
setup_input_listener();
|
||||
setup_input_listener(app.handle());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
|
97
src-tauri/src/snippets/engine.rs
Normal file
97
src-tauri/src/snippets/engine.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use crate::snippets::input_manager::{key_to_char, InputEvent, InputManager};
|
||||
use crate::snippets::manager::SnippetManager;
|
||||
use rdev::Key;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
const BUFFER_SIZE: usize = 30;
|
||||
|
||||
pub struct ExpansionEngine {
|
||||
buffer: Arc<Mutex<String>>,
|
||||
snippet_manager: Arc<SnippetManager>,
|
||||
input_manager: Arc<dyn InputManager>,
|
||||
}
|
||||
|
||||
impl ExpansionEngine {
|
||||
pub fn new(
|
||||
snippet_manager: Arc<SnippetManager>,
|
||||
input_manager: Arc<dyn InputManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
buffer: Arc::new(Mutex::new(String::with_capacity(BUFFER_SIZE))),
|
||||
snippet_manager,
|
||||
input_manager,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_listening(&self) -> anyhow::Result<()> {
|
||||
let engine = Arc::new(self.clone_for_thread());
|
||||
self.input_manager
|
||||
.start_listening(Box::new(move |event| {
|
||||
engine.handle_key_press(event);
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clone_for_thread(&self) -> Self {
|
||||
Self {
|
||||
buffer: self.buffer.clone(),
|
||||
snippet_manager: self.snippet_manager.clone(),
|
||||
input_manager: self.input_manager.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_press(&self, event: InputEvent) {
|
||||
let InputEvent::KeyPress(key) = event;
|
||||
let mut buffer = self.buffer.lock().unwrap();
|
||||
|
||||
match key {
|
||||
Key::Backspace => {
|
||||
buffer.pop();
|
||||
}
|
||||
key => {
|
||||
if let Some(ch) = key_to_char(&key) {
|
||||
buffer.push(ch);
|
||||
if buffer.len() > BUFFER_SIZE {
|
||||
buffer.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(snippets) = self.snippet_manager.list_snippets() {
|
||||
for snippet in snippets {
|
||||
if buffer.ends_with(&snippet.keyword) {
|
||||
let (keyword, content) = (snippet.keyword.clone(), snippet.content.clone());
|
||||
drop(buffer);
|
||||
self.expand_snippet(&keyword, &content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_snippet(&self, keyword: &str, content: &str) {
|
||||
let mut backspaces = String::new();
|
||||
for _ in 0..keyword.len() {
|
||||
backspaces.push('\u{8}');
|
||||
}
|
||||
|
||||
let input_manager = self.input_manager.clone();
|
||||
let content_to_paste = content.to_string();
|
||||
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = input_manager.inject_text(&backspaces) {
|
||||
eprintln!("Failed to inject backspaces: {}", e);
|
||||
}
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(50));
|
||||
if let Err(e) = input_manager.inject_text(&content_to_paste) {
|
||||
eprintln!("Failed to inject snippet content: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut buffer = self.buffer.lock().unwrap();
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
|
@ -1,13 +1,20 @@
|
|||
use anyhow::Result;
|
||||
use enigo::{Enigo, Keyboard};
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::Key;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
#[derive(Debug)]
|
||||
lazy_static! {
|
||||
static ref ENIGO: Mutex<Enigo> = Mutex::new(Enigo::new(&enigo::Settings::default()).unwrap());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InputEvent {
|
||||
KeyPress(Key),
|
||||
}
|
||||
|
||||
pub trait InputManager {
|
||||
pub trait InputManager: Send + Sync {
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send>) -> Result<()>;
|
||||
fn inject_text(&self, text: &str) -> Result<()>;
|
||||
}
|
||||
|
@ -35,8 +42,10 @@ impl InputManager for RdevInputManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn inject_text(&self, _text: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
fn inject_text(&self, text: &str) -> Result<()> {
|
||||
let mut enigo = ENIGO.lock().unwrap();
|
||||
enigo.text(text)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +78,6 @@ impl InputManager for EvdevInputManager {
|
|||
}
|
||||
|
||||
for mut device in devices {
|
||||
let cb = callback.as_ref();
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
match device.fetch_events() {
|
||||
|
@ -95,4 +103,58 @@ impl InputManager for EvdevInputManager {
|
|||
fn inject_text(&self, _text: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_to_char(key: &Key) -> Option<char> {
|
||||
match key {
|
||||
Key::KeyA => Some('a'),
|
||||
Key::KeyB => Some('b'),
|
||||
Key::KeyC => Some('c'),
|
||||
Key::KeyD => Some('d'),
|
||||
Key::KeyE => Some('e'),
|
||||
Key::KeyF => Some('f'),
|
||||
Key::KeyG => Some('g'),
|
||||
Key::KeyH => Some('h'),
|
||||
Key::KeyI => Some('i'),
|
||||
Key::KeyJ => Some('j'),
|
||||
Key::KeyK => Some('k'),
|
||||
Key::KeyL => Some('l'),
|
||||
Key::KeyM => Some('m'),
|
||||
Key::KeyN => Some('n'),
|
||||
Key::KeyO => Some('o'),
|
||||
Key::KeyP => Some('p'),
|
||||
Key::KeyQ => Some('q'),
|
||||
Key::KeyR => Some('r'),
|
||||
Key::KeyS => Some('s'),
|
||||
Key::KeyT => Some('t'),
|
||||
Key::KeyU => Some('u'),
|
||||
Key::KeyV => Some('v'),
|
||||
Key::KeyW => Some('w'),
|
||||
Key::KeyX => Some('x'),
|
||||
Key::KeyY => Some('y'),
|
||||
Key::KeyZ => Some('z'),
|
||||
Key::Num0 => Some('0'),
|
||||
Key::Num1 => Some('1'),
|
||||
Key::Num2 => Some('2'),
|
||||
Key::Num3 => Some('3'),
|
||||
Key::Num4 => Some('4'),
|
||||
Key::Num5 => Some('5'),
|
||||
Key::Num6 => Some('6'),
|
||||
Key::Num7 => Some('7'),
|
||||
Key::Num8 => Some('8'),
|
||||
Key::Num9 => Some('9'),
|
||||
Key::Space => Some(' '),
|
||||
Key::Slash => Some('/'),
|
||||
Key::Dot => Some('.'),
|
||||
Key::Comma => Some(','),
|
||||
Key::Minus => Some('-'),
|
||||
Key::Equal => Some('='),
|
||||
Key::LeftBracket => Some('['),
|
||||
Key::RightBracket => Some(']'),
|
||||
Key::BackSlash => Some('\\'),
|
||||
Key::SemiColon => Some(';'),
|
||||
Key::Quote => Some('\''),
|
||||
Key::BackQuote => Some('`'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
|
@ -2,11 +2,12 @@ use crate::error::AppError;
|
|||
use crate::snippets::types::Snippet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{params, Connection, Result as RusqliteResult};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SnippetManager {
|
||||
db: Mutex<Connection>,
|
||||
db: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl SnippetManager {
|
||||
|
@ -17,7 +18,9 @@ impl SnippetManager {
|
|||
.map_err(|_| AppError::DirectoryNotFound)?;
|
||||
let db_path = data_dir.join("snippets.sqlite");
|
||||
let db = Connection::open(db_path)?;
|
||||
Ok(Self { db: Mutex::new(db) })
|
||||
Ok(Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init_db(&self) -> RusqliteResult<()> {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod engine;
|
||||
pub mod input_manager;
|
||||
pub mod manager;
|
||||
pub mod types;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue