diff --git a/src-tauri/src/snippets/engine.rs b/src-tauri/src/snippets/engine.rs index b016e5e..c2c0145 100644 --- a/src-tauri/src/snippets/engine.rs +++ b/src-tauri/src/snippets/engine.rs @@ -1,10 +1,18 @@ use crate::snippets::input_manager::{InputEvent, InputManager}; use crate::snippets::manager::SnippetManager; +use arboard::Clipboard; +use chrono::Local; +use enigo::Key as EnigoKey; use std::sync::{Arc, Mutex}; use std::thread; const BUFFER_SIZE: usize = 30; +struct ResolvedSnippet { + content: String, + cursor_pos: Option, +} + pub struct ExpansionEngine { buffer: Arc>, snippet_manager: Arc, @@ -72,14 +80,65 @@ impl ExpansionEngine { } } + fn parse_and_resolve_placeholders(&self, raw_content: &str) -> ResolvedSnippet { + let mut resolved_content = String::with_capacity(raw_content.len()); + let mut cursor_pos: Option = None; + let mut last_end = 0; + + for (start, _) in raw_content.match_indices('{') { + if start < last_end { + continue; + } + if let Some(end) = raw_content[start..].find('}') { + let placeholder = &raw_content[start + 1..start + end]; + + resolved_content.push_str(&raw_content[last_end..start]); + + let replacement = match placeholder { + "cursor" => { + if cursor_pos.is_none() { + cursor_pos = Some(resolved_content.chars().count()); + } + String::new() + } + "clipboard" => Clipboard::new() + .ok() + .and_then(|mut c| c.get_text().ok()) + .unwrap_or_default(), + "date" => Local::now().format("%d %b %Y").to_string(), + "time" => Local::now().format("%H:%M").to_string(), + "datetime" => Local::now().format("%d %b %Y at %H:%M").to_string(), + "day" => Local::now().format("%A").to_string(), + _ => raw_content[start..start + end + 1].to_string(), + }; + resolved_content.push_str(&replacement); + last_end = start + end + 1; + } + } + resolved_content.push_str(&raw_content[last_end..]); + + ResolvedSnippet { + content: resolved_content, + cursor_pos, + } + } + fn expand_snippet(&self, keyword: &str, content: &str) { let mut backspaces = String::new(); for _ in 0..keyword.len() { backspaces.push('\u{8}'); } + let resolved = self.parse_and_resolve_placeholders(content); + let content_to_paste = resolved.content; + + let chars_to_move_left = if let Some(pos) = resolved.cursor_pos { + content_to_paste.chars().count() - pos + } else { + 0 + }; + 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) { @@ -89,6 +148,15 @@ impl ExpansionEngine { if let Err(e) = input_manager.inject_text(&content_to_paste) { eprintln!("Failed to inject snippet content: {}", e); } + + if chars_to_move_left > 0 { + thread::sleep(std::time::Duration::from_millis(50)); + if let Err(e) = + input_manager.inject_key_clicks(EnigoKey::LeftArrow, chars_to_move_left) + { + eprintln!("Failed to inject cursor movement: {}", e); + } + } }); let mut buffer = self.buffer.lock().unwrap(); diff --git a/src-tauri/src/snippets/input_manager.rs b/src-tauri/src/snippets/input_manager.rs index 48ec984..5a11082 100644 --- a/src-tauri/src/snippets/input_manager.rs +++ b/src-tauri/src/snippets/input_manager.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use enigo::{Enigo, Keyboard}; +use enigo::{Enigo, Key as EnigoKey, Keyboard}; use lazy_static::lazy_static; use rdev::Key; use std::sync::{Arc, Mutex}; @@ -26,6 +26,7 @@ pub enum InputEvent { pub trait InputManager: Send + Sync { fn start_listening(&self, callback: Box) -> Result<()>; fn inject_text(&self, text: &str) -> Result<()>; + fn inject_key_clicks(&self, key: EnigoKey, count: usize) -> Result<()>; } pub struct RdevInputManager; @@ -94,6 +95,14 @@ impl InputManager for RdevInputManager { Ok(()) } + + fn inject_key_clicks(&self, key: EnigoKey, count: usize) -> Result<()> { + let mut enigo = ENIGO.lock().unwrap(); + for _ in 0..count { + enigo.key(key, enigo::Direction::Click)?; + } + Ok(()) + } } // this implementation for wayland, because wayland is a pain and rdev no worky @@ -112,6 +121,7 @@ impl EvdevInputManager { KeyCode::KEY_TAB, KeyCode::KEY_SPACE, KeyCode::KEY_BACKSPACE, + KeyCode::KEY_LEFT, ]); let text: &str = @@ -298,6 +308,13 @@ impl EvdevInputManager { thread::sleep(Duration::from_millis(10)); Ok(()) } + + fn enigo_to_evdev(key: EnigoKey) -> Option { + match key { + EnigoKey::LeftArrow => Some(KeyCode::KEY_LEFT), + _ => None, + } + } } #[cfg(target_os = "linux")] @@ -397,13 +414,21 @@ impl InputManager for EvdevInputManager { if ch == '\u{8}' { self.inject_key_click(&mut *device, KeyCode::KEY_BACKSPACE)?; } else { - self.inject_char(&mut *device, ch)?; - self.inject_char(&mut *device, ch)?; self.inject_char(&mut *device, ch)?; } } Ok(()) } + + fn inject_key_clicks(&self, key: EnigoKey, count: usize) -> Result<()> { + if let Some(keycode) = Self::enigo_to_evdev(key) { + let mut device = self.virtual_device.lock().unwrap(); + for _ in 0..count { + self.inject_key_click(&mut *device, keycode)?; + } + } + Ok(()) + } } pub fn key_to_char(key: &Key, is_shifted: bool) -> Option {