mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-30 18:57:25 +00:00
feat(snippets): implement snippet placeholder resolution
This commit introduces a new method to parse and resolve placeholders in snippet content, allowing for dynamic content insertion such as clipboard text, current date, and time. Additionally, it implements cursor positioning for better user experience during snippet expansion.
This commit is contained in:
parent
77f7f8c113
commit
b68c4ccb6e
2 changed files with 97 additions and 4 deletions
|
@ -1,10 +1,18 @@
|
||||||
use crate::snippets::input_manager::{InputEvent, InputManager};
|
use crate::snippets::input_manager::{InputEvent, InputManager};
|
||||||
use crate::snippets::manager::SnippetManager;
|
use crate::snippets::manager::SnippetManager;
|
||||||
|
use arboard::Clipboard;
|
||||||
|
use chrono::Local;
|
||||||
|
use enigo::Key as EnigoKey;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
const BUFFER_SIZE: usize = 30;
|
const BUFFER_SIZE: usize = 30;
|
||||||
|
|
||||||
|
struct ResolvedSnippet {
|
||||||
|
content: String,
|
||||||
|
cursor_pos: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ExpansionEngine {
|
pub struct ExpansionEngine {
|
||||||
buffer: Arc<Mutex<String>>,
|
buffer: Arc<Mutex<String>>,
|
||||||
snippet_manager: Arc<SnippetManager>,
|
snippet_manager: Arc<SnippetManager>,
|
||||||
|
@ -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<usize> = 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) {
|
fn expand_snippet(&self, keyword: &str, content: &str) {
|
||||||
let mut backspaces = String::new();
|
let mut backspaces = String::new();
|
||||||
for _ in 0..keyword.len() {
|
for _ in 0..keyword.len() {
|
||||||
backspaces.push('\u{8}');
|
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 input_manager = self.input_manager.clone();
|
||||||
let content_to_paste = content.to_string();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
if let Err(e) = input_manager.inject_text(&backspaces) {
|
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) {
|
if let Err(e) = input_manager.inject_text(&content_to_paste) {
|
||||||
eprintln!("Failed to inject snippet content: {}", e);
|
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();
|
let mut buffer = self.buffer.lock().unwrap();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use enigo::{Enigo, Keyboard};
|
use enigo::{Enigo, Key as EnigoKey, Keyboard};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rdev::Key;
|
use rdev::Key;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
@ -26,6 +26,7 @@ pub enum InputEvent {
|
||||||
pub trait InputManager: Send + Sync {
|
pub trait InputManager: Send + Sync {
|
||||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send + Sync>) -> Result<()>;
|
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send + Sync>) -> Result<()>;
|
||||||
fn inject_text(&self, text: &str) -> Result<()>;
|
fn inject_text(&self, text: &str) -> Result<()>;
|
||||||
|
fn inject_key_clicks(&self, key: EnigoKey, count: usize) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RdevInputManager;
|
pub struct RdevInputManager;
|
||||||
|
@ -94,6 +95,14 @@ impl InputManager for RdevInputManager {
|
||||||
|
|
||||||
Ok(())
|
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
|
// this implementation for wayland, because wayland is a pain and rdev no worky
|
||||||
|
@ -112,6 +121,7 @@ impl EvdevInputManager {
|
||||||
KeyCode::KEY_TAB,
|
KeyCode::KEY_TAB,
|
||||||
KeyCode::KEY_SPACE,
|
KeyCode::KEY_SPACE,
|
||||||
KeyCode::KEY_BACKSPACE,
|
KeyCode::KEY_BACKSPACE,
|
||||||
|
KeyCode::KEY_LEFT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let text: &str =
|
let text: &str =
|
||||||
|
@ -298,6 +308,13 @@ impl EvdevInputManager {
|
||||||
thread::sleep(Duration::from_millis(10));
|
thread::sleep(Duration::from_millis(10));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enigo_to_evdev(key: EnigoKey) -> Option<KeyCode> {
|
||||||
|
match key {
|
||||||
|
EnigoKey::LeftArrow => Some(KeyCode::KEY_LEFT),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
@ -397,13 +414,21 @@ impl InputManager for EvdevInputManager {
|
||||||
if ch == '\u{8}' {
|
if ch == '\u{8}' {
|
||||||
self.inject_key_click(&mut *device, KeyCode::KEY_BACKSPACE)?;
|
self.inject_key_click(&mut *device, KeyCode::KEY_BACKSPACE)?;
|
||||||
} else {
|
} else {
|
||||||
self.inject_char(&mut *device, ch)?;
|
|
||||||
self.inject_char(&mut *device, ch)?;
|
|
||||||
self.inject_char(&mut *device, ch)?;
|
self.inject_char(&mut *device, ch)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
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<char> {
|
pub fn key_to_char(key: &Key, is_shifted: bool) -> Option<char> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue