mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
feat(snippets): add create snippet form
This commit introduces a new `SnippetForm` component for creating snippets, updates the `ViewManager` to handle the new 'create-snippet-form' view, and integrates the form into the main page routing.
This commit is contained in:
parent
7088cb17fd
commit
de0b26e8ae
8 changed files with 525 additions and 81 deletions
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
@ -4467,6 +4467,7 @@ dependencies = [
|
|||
"trash",
|
||||
"url",
|
||||
"uuid",
|
||||
"xkbcommon 0.8.0",
|
||||
"zbus",
|
||||
"zip",
|
||||
]
|
||||
|
|
|
@ -56,6 +56,7 @@ rdev = "0.5.3"
|
|||
evdev = "0.13.1"
|
||||
anyhow = "1.0.98"
|
||||
lazy_static = "1.5.0"
|
||||
xkbcommon = "0.8.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
|
|
@ -13,6 +13,7 @@ mod quicklinks;
|
|||
mod snippets;
|
||||
mod system;
|
||||
|
||||
use crate::snippets::input_manager::EvdevInputManager;
|
||||
use crate::{app::App, cache::AppCache};
|
||||
use browser_extension::WsState;
|
||||
use frecency::FrecencyManager;
|
||||
|
@ -161,7 +162,7 @@ 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 = EvdevInputManager::new().unwrap();
|
||||
let input_manager_arc = Arc::new(input_manager);
|
||||
|
||||
let engine = ExpansionEngine::new(snippet_manager_arc, input_manager_arc);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::snippets::input_manager::{key_to_char, InputEvent, InputManager};
|
||||
use crate::snippets::input_manager::{InputEvent, InputManager};
|
||||
use crate::snippets::manager::SnippetManager;
|
||||
use rdev::Key;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
@ -12,6 +12,71 @@ pub struct ExpansionEngine {
|
|||
input_manager: Arc<dyn InputManager>,
|
||||
}
|
||||
|
||||
fn key_to_char(key: &Key, is_shifted: bool) -> Option<char> {
|
||||
if let Key::Backspace = key {
|
||||
return Some('\u{8}');
|
||||
}
|
||||
if let Key::Return | Key::KpReturn = key {
|
||||
return Some('\n');
|
||||
}
|
||||
if let Key::Tab = key {
|
||||
return Some('\t');
|
||||
}
|
||||
|
||||
let s = match key {
|
||||
Key::KeyA => "aA",
|
||||
Key::KeyB => "bB",
|
||||
Key::KeyC => "cC",
|
||||
Key::KeyD => "dD",
|
||||
Key::KeyE => "eE",
|
||||
Key::KeyF => "fF",
|
||||
Key::KeyG => "gG",
|
||||
Key::KeyH => "hH",
|
||||
Key::KeyI => "iI",
|
||||
Key::KeyJ => "jJ",
|
||||
Key::KeyK => "kK",
|
||||
Key::KeyL => "lL",
|
||||
Key::KeyM => "mM",
|
||||
Key::KeyN => "nN",
|
||||
Key::KeyO => "oO",
|
||||
Key::KeyP => "pP",
|
||||
Key::KeyQ => "qQ",
|
||||
Key::KeyR => "rR",
|
||||
Key::KeyS => "sS",
|
||||
Key::KeyT => "tT",
|
||||
Key::KeyU => "uU",
|
||||
Key::KeyV => "vV",
|
||||
Key::KeyW => "wW",
|
||||
Key::KeyX => "xX",
|
||||
Key::KeyY => "yY",
|
||||
Key::KeyZ => "zZ",
|
||||
Key::Num0 => "0)",
|
||||
Key::Num1 => "1!",
|
||||
Key::Num2 => "2@",
|
||||
Key::Num3 => "3#",
|
||||
Key::Num4 => "4$",
|
||||
Key::Num5 => "5%",
|
||||
Key::Num6 => "6^",
|
||||
Key::Num7 => "7&",
|
||||
Key::Num8 => "8*",
|
||||
Key::Num9 => "9(",
|
||||
Key::Space => " ",
|
||||
Key::Slash => "/?",
|
||||
Key::Dot => ".>",
|
||||
Key::Comma => ",<",
|
||||
Key::Minus => "-_",
|
||||
Key::Equal => "=+",
|
||||
Key::LeftBracket => "[{",
|
||||
Key::RightBracket => "]}",
|
||||
Key::BackSlash => "\\|",
|
||||
Key::SemiColon => ";:",
|
||||
Key::Quote => "'\"",
|
||||
Key::BackQuote => "`~",
|
||||
_ => return None,
|
||||
};
|
||||
s.chars().nth(if is_shifted { 1 } else { 0 })
|
||||
}
|
||||
|
||||
impl ExpansionEngine {
|
||||
pub fn new(
|
||||
snippet_manager: Arc<SnippetManager>,
|
||||
|
@ -42,19 +107,21 @@ impl ExpansionEngine {
|
|||
}
|
||||
|
||||
fn handle_key_press(&self, event: InputEvent) {
|
||||
let InputEvent::KeyPress(key) = event;
|
||||
let InputEvent::KeyPress(ch) = event;
|
||||
let mut buffer = self.buffer.lock().unwrap();
|
||||
|
||||
match key {
|
||||
Key::Backspace => {
|
||||
match ch {
|
||||
'\u{8}' => {
|
||||
buffer.pop();
|
||||
}
|
||||
key => {
|
||||
if let Some(ch) = key_to_char(&key) {
|
||||
buffer.push(ch);
|
||||
if buffer.len() > BUFFER_SIZE {
|
||||
buffer.remove(0);
|
||||
}
|
||||
'\n' | '\t' | '\u{1b}' => {
|
||||
buffer.clear();
|
||||
}
|
||||
ch if ch.is_control() => (),
|
||||
_ => {
|
||||
buffer.push(ch);
|
||||
if buffer.len() > BUFFER_SIZE {
|
||||
buffer.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +151,6 @@ impl ExpansionEngine {
|
|||
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);
|
||||
|
|
|
@ -2,20 +2,27 @@ use anyhow::Result;
|
|||
use enigo::{Enigo, Keyboard};
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::Key;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use evdev::{uinput::VirtualDevice, KeyCode};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::collections::HashSet;
|
||||
#[cfg(target_os = "linux")]
|
||||
use xkbcommon::xkb;
|
||||
|
||||
lazy_static! {
|
||||
static ref ENIGO: Mutex<Enigo> = Mutex::new(Enigo::new(&enigo::Settings::default()).unwrap());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InputEvent {
|
||||
KeyPress(Key),
|
||||
KeyPress(char),
|
||||
}
|
||||
|
||||
pub trait InputManager: Send + Sync {
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send>) -> Result<()>;
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send + Sync>) -> Result<()>;
|
||||
fn inject_text(&self, text: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
|
@ -28,11 +35,32 @@ impl RdevInputManager {
|
|||
}
|
||||
|
||||
impl InputManager for RdevInputManager {
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send>) -> Result<()> {
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send + Sync>) -> Result<()> {
|
||||
let shift_pressed = Arc::new(Mutex::new(false));
|
||||
let callback = Arc::new(callback);
|
||||
|
||||
let shift_clone_press = shift_pressed.clone();
|
||||
let shift_clone_release = shift_pressed.clone();
|
||||
let callback_clone = callback.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let cb = move |event: rdev::Event| {
|
||||
if let rdev::EventType::KeyPress(key) = event.event_type {
|
||||
callback(InputEvent::KeyPress(key));
|
||||
match event.event_type {
|
||||
rdev::EventType::KeyPress(key) => {
|
||||
if key == Key::ShiftLeft || key == Key::ShiftRight {
|
||||
*shift_clone_press.lock().unwrap() = true;
|
||||
}
|
||||
let is_shifted = *shift_clone_press.lock().unwrap();
|
||||
if let Some(ch) = key_to_char(&key, is_shifted) {
|
||||
callback_clone(InputEvent::KeyPress(ch));
|
||||
}
|
||||
}
|
||||
rdev::EventType::KeyRelease(key) => {
|
||||
if key == Key::ShiftLeft || key == Key::ShiftRight {
|
||||
*shift_clone_release.lock().unwrap() = false;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
if let Err(error) = rdev::listen(cb) {
|
||||
|
@ -51,18 +79,184 @@ impl InputManager for RdevInputManager {
|
|||
|
||||
// this implementation for wayland, because wayland is a pain and rdev no worky
|
||||
#[cfg(target_os = "linux")]
|
||||
pub struct EvdevInputManager;
|
||||
pub struct EvdevInputManager {
|
||||
virtual_device: Mutex<VirtualDevice>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl EvdevInputManager {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
pub fn new() -> Result<Self> {
|
||||
let mut key_codes = HashSet::new();
|
||||
key_codes.extend([
|
||||
KeyCode::KEY_LEFTSHIFT,
|
||||
KeyCode::KEY_ENTER,
|
||||
KeyCode::KEY_TAB,
|
||||
KeyCode::KEY_SPACE,
|
||||
KeyCode::KEY_BACKSPACE,
|
||||
]);
|
||||
|
||||
let text: &str =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_+-=[]{}\\|;:'\",./<>?`~";
|
||||
for ch in text.chars() {
|
||||
if let Some((key, _)) = Self::char_to_keycode_static(ch) {
|
||||
key_codes.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
let mut attribute_set = evdev::AttributeSet::new();
|
||||
for key in key_codes {
|
||||
attribute_set.insert(key);
|
||||
}
|
||||
|
||||
let uinput_device = evdev::uinput::VirtualDevice::builder()?
|
||||
.name("Global Automata Text Injection")
|
||||
.with_keys(&attribute_set)?
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
virtual_device: Mutex::new(uinput_device),
|
||||
})
|
||||
}
|
||||
|
||||
fn char_to_keycode_static(c: char) -> Option<(KeyCode, bool)> {
|
||||
let (key, shift) = match c {
|
||||
'a' => (KeyCode::KEY_A, false),
|
||||
'b' => (KeyCode::KEY_B, false),
|
||||
'c' => (KeyCode::KEY_C, false),
|
||||
'd' => (KeyCode::KEY_D, false),
|
||||
'e' => (KeyCode::KEY_E, false),
|
||||
'f' => (KeyCode::KEY_F, false),
|
||||
'g' => (KeyCode::KEY_G, false),
|
||||
'h' => (KeyCode::KEY_H, false),
|
||||
'i' => (KeyCode::KEY_I, false),
|
||||
'j' => (KeyCode::KEY_J, false),
|
||||
'k' => (KeyCode::KEY_K, false),
|
||||
'l' => (KeyCode::KEY_L, false),
|
||||
'm' => (KeyCode::KEY_M, false),
|
||||
'n' => (KeyCode::KEY_N, false),
|
||||
'o' => (KeyCode::KEY_O, false),
|
||||
'p' => (KeyCode::KEY_P, false),
|
||||
'q' => (KeyCode::KEY_Q, false),
|
||||
'r' => (KeyCode::KEY_R, false),
|
||||
's' => (KeyCode::KEY_S, false),
|
||||
't' => (KeyCode::KEY_T, false),
|
||||
'u' => (KeyCode::KEY_U, false),
|
||||
'v' => (KeyCode::KEY_V, false),
|
||||
'w' => (KeyCode::KEY_W, false),
|
||||
'x' => (KeyCode::KEY_X, false),
|
||||
'y' => (KeyCode::KEY_Y, false),
|
||||
'z' => (KeyCode::KEY_Z, false),
|
||||
'A' => (KeyCode::KEY_A, true),
|
||||
'B' => (KeyCode::KEY_B, true),
|
||||
'C' => (KeyCode::KEY_C, true),
|
||||
'D' => (KeyCode::KEY_D, true),
|
||||
'E' => (KeyCode::KEY_E, true),
|
||||
'F' => (KeyCode::KEY_F, true),
|
||||
'G' => (KeyCode::KEY_G, true),
|
||||
'H' => (KeyCode::KEY_H, true),
|
||||
'I' => (KeyCode::KEY_I, true),
|
||||
'J' => (KeyCode::KEY_J, true),
|
||||
'K' => (KeyCode::KEY_K, true),
|
||||
'L' => (KeyCode::KEY_L, true),
|
||||
'M' => (KeyCode::KEY_M, true),
|
||||
'N' => (KeyCode::KEY_N, true),
|
||||
'O' => (KeyCode::KEY_O, true),
|
||||
'P' => (KeyCode::KEY_P, true),
|
||||
'Q' => (KeyCode::KEY_Q, true),
|
||||
'R' => (KeyCode::KEY_R, true),
|
||||
'S' => (KeyCode::KEY_S, true),
|
||||
'T' => (KeyCode::KEY_T, true),
|
||||
'U' => (KeyCode::KEY_U, true),
|
||||
'V' => (KeyCode::KEY_V, true),
|
||||
'W' => (KeyCode::KEY_W, true),
|
||||
'X' => (KeyCode::KEY_X, true),
|
||||
'Y' => (KeyCode::KEY_Y, true),
|
||||
'Z' => (KeyCode::KEY_Z, true),
|
||||
'1' => (KeyCode::KEY_1, false),
|
||||
'2' => (KeyCode::KEY_2, false),
|
||||
'3' => (KeyCode::KEY_3, false),
|
||||
'4' => (KeyCode::KEY_4, false),
|
||||
'5' => (KeyCode::KEY_5, false),
|
||||
'6' => (KeyCode::KEY_6, false),
|
||||
'7' => (KeyCode::KEY_7, false),
|
||||
'8' => (KeyCode::KEY_8, false),
|
||||
'9' => (KeyCode::KEY_9, false),
|
||||
'0' => (KeyCode::KEY_0, false),
|
||||
'!' => (KeyCode::KEY_1, true),
|
||||
'@' => (KeyCode::KEY_2, true),
|
||||
'#' => (KeyCode::KEY_3, true),
|
||||
'$' => (KeyCode::KEY_4, true),
|
||||
'%' => (KeyCode::KEY_5, true),
|
||||
'^' => (KeyCode::KEY_6, true),
|
||||
'&' => (KeyCode::KEY_7, true),
|
||||
'*' => (KeyCode::KEY_8, true),
|
||||
'(' => (KeyCode::KEY_9, true),
|
||||
')' => (KeyCode::KEY_0, true),
|
||||
'-' => (KeyCode::KEY_MINUS, false),
|
||||
'_' => (KeyCode::KEY_MINUS, true),
|
||||
'=' => (KeyCode::KEY_EQUAL, false),
|
||||
'+' => (KeyCode::KEY_EQUAL, true),
|
||||
'[' => (KeyCode::KEY_LEFTBRACE, false),
|
||||
'{' => (KeyCode::KEY_LEFTBRACE, true),
|
||||
']' => (KeyCode::KEY_RIGHTBRACE, false),
|
||||
'}' => (KeyCode::KEY_RIGHTBRACE, true),
|
||||
'\\' => (KeyCode::KEY_BACKSLASH, false),
|
||||
'|' => (KeyCode::KEY_BACKSLASH, true),
|
||||
';' => (KeyCode::KEY_SEMICOLON, false),
|
||||
':' => (KeyCode::KEY_SEMICOLON, true),
|
||||
'\'' => (KeyCode::KEY_APOSTROPHE, false),
|
||||
'"' => (KeyCode::KEY_APOSTROPHE, true),
|
||||
',' => (KeyCode::KEY_COMMA, false),
|
||||
'<' => (KeyCode::KEY_COMMA, true),
|
||||
'.' => (KeyCode::KEY_DOT, false),
|
||||
'>' => (KeyCode::KEY_DOT, true),
|
||||
'/' => (KeyCode::KEY_SLASH, false),
|
||||
'?' => (KeyCode::KEY_SLASH, true),
|
||||
'`' => (KeyCode::KEY_GRAVE, false),
|
||||
'~' => (KeyCode::KEY_GRAVE, true),
|
||||
' ' => (KeyCode::KEY_SPACE, false),
|
||||
'\n' => (KeyCode::KEY_ENTER, false),
|
||||
'\t' => (KeyCode::KEY_TAB, false),
|
||||
_ => return None,
|
||||
};
|
||||
Some((key, shift))
|
||||
}
|
||||
|
||||
fn inject_char(&self, device: &mut VirtualDevice, c: char) -> Result<()> {
|
||||
let (key, shift) = match Self::char_to_keycode_static(c) {
|
||||
Some(val) => val,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mut press_events = Vec::new();
|
||||
if shift {
|
||||
press_events.push(evdev::InputEvent::new(
|
||||
evdev::EventType::KEY.0,
|
||||
KeyCode::KEY_LEFTSHIFT.0,
|
||||
1,
|
||||
));
|
||||
}
|
||||
press_events.push(evdev::InputEvent::new(evdev::EventType::KEY.0, key.0, 1));
|
||||
device.emit(&press_events)?;
|
||||
|
||||
let mut release_events = Vec::new();
|
||||
release_events.push(evdev::InputEvent::new(evdev::EventType::KEY.0, key.0, 0));
|
||||
if shift {
|
||||
release_events.push(evdev::InputEvent::new(
|
||||
evdev::EventType::KEY.0,
|
||||
KeyCode::KEY_LEFTSHIFT.0,
|
||||
0,
|
||||
));
|
||||
}
|
||||
device.emit(&release_events)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl InputManager for EvdevInputManager {
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send>) -> Result<()> {
|
||||
fn start_listening(&self, callback: Box<dyn Fn(InputEvent) + Send + Sync>) -> Result<()> {
|
||||
let devices = evdev::enumerate()
|
||||
.map(|t| t.1)
|
||||
.filter(|d| {
|
||||
|
@ -77,20 +271,66 @@ impl InputManager for EvdevInputManager {
|
|||
));
|
||||
}
|
||||
|
||||
let callback = Arc::new(callback);
|
||||
|
||||
for mut device in devices {
|
||||
let callback = Arc::clone(&callback);
|
||||
let device_name = device.name().unwrap_or("Unnamed Device").to_string();
|
||||
|
||||
thread::spawn(move || {
|
||||
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||
let keymap = xkb::Keymap::new_from_names(
|
||||
&context,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
None,
|
||||
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||
)
|
||||
.expect("Failed to create xkb keymap");
|
||||
let mut xkb_state = xkb::State::new(&keymap);
|
||||
|
||||
loop {
|
||||
match device.fetch_events() {
|
||||
Ok(events) => {
|
||||
for ev in events {
|
||||
if ev.event_type() == evdev::EventType::KEY && ev.value() == 1 {
|
||||
println!("[evdev] Raw key press: code={}", ev.code());
|
||||
if ev.event_type() != evdev::EventType::KEY {
|
||||
continue;
|
||||
}
|
||||
|
||||
// evdev keycodes are offset by 8 from X11 keycodes
|
||||
let keycode = ev.code() + 8;
|
||||
let direction = match ev.value() {
|
||||
0 => xkb::KeyDirection::Up,
|
||||
1 => xkb::KeyDirection::Down,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
match direction {
|
||||
xkb::KeyDirection::Down => {
|
||||
xkb_state.update_key(keycode.into(), xkb::KeyDirection::Down);
|
||||
let utf8_str = xkb_state.key_get_utf8(keycode.into());
|
||||
if !utf8_str.is_empty() {
|
||||
for ch in utf8_str.chars() {
|
||||
callback(InputEvent::KeyPress(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
xkb_state.update_key(keycode.into(), direction);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching evdev events: {}", e);
|
||||
break;
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock {
|
||||
eprintln!(
|
||||
"Error fetching evdev events for device \"{}\": {}",
|
||||
device_name, e
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,61 +340,79 @@ impl InputManager for EvdevInputManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn inject_text(&self, _text: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
fn inject_text(&self, text: &str) -> Result<()> {
|
||||
let mut device = self.virtual_device.lock().unwrap();
|
||||
for ch in text.chars() {
|
||||
self.inject_char(&mut *device, ch)?;
|
||||
}
|
||||
let syn_report =
|
||||
evdev::InputEvent::new(evdev::EventType::SYNCHRONIZATION.0, evdev::SynchronizationCode::SYN_REPORT.0, 0);
|
||||
device.emit(&[syn_report])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
pub fn key_to_char(key: &Key, is_shifted: bool) -> Option<char> {
|
||||
if let Key::Backspace = key {
|
||||
return Some('\u{8}');
|
||||
}
|
||||
if let Key::Return | Key::KpReturn = key {
|
||||
return Some('\n');
|
||||
}
|
||||
if let Key::Tab = key {
|
||||
return Some('\t');
|
||||
}
|
||||
|
||||
let s = match key {
|
||||
Key::KeyA => "aA",
|
||||
Key::KeyB => "bB",
|
||||
Key::KeyC => "cC",
|
||||
Key::KeyD => "dD",
|
||||
Key::KeyE => "eE",
|
||||
Key::KeyF => "fF",
|
||||
Key::KeyG => "gG",
|
||||
Key::KeyH => "hH",
|
||||
Key::KeyI => "iI",
|
||||
Key::KeyJ => "jJ",
|
||||
Key::KeyK => "kK",
|
||||
Key::KeyL => "lL",
|
||||
Key::KeyM => "mM",
|
||||
Key::KeyN => "nN",
|
||||
Key::KeyO => "oO",
|
||||
Key::KeyP => "pP",
|
||||
Key::KeyQ => "qQ",
|
||||
Key::KeyR => "rR",
|
||||
Key::KeyS => "sS",
|
||||
Key::KeyT => "tT",
|
||||
Key::KeyU => "uU",
|
||||
Key::KeyV => "vV",
|
||||
Key::KeyW => "wW",
|
||||
Key::KeyX => "xX",
|
||||
Key::KeyY => "yY",
|
||||
Key::KeyZ => "zZ",
|
||||
Key::Num0 => "0)",
|
||||
Key::Num1 => "1!",
|
||||
Key::Num2 => "2@",
|
||||
Key::Num3 => "3#",
|
||||
Key::Num4 => "4$",
|
||||
Key::Num5 => "5%",
|
||||
Key::Num6 => "6^",
|
||||
Key::Num7 => "7&",
|
||||
Key::Num8 => "8*",
|
||||
Key::Num9 => "9(",
|
||||
Key::Space => " ",
|
||||
Key::Slash => "/?",
|
||||
Key::Dot => ".>",
|
||||
Key::Comma => ",<",
|
||||
Key::Minus => "-_",
|
||||
Key::Equal => "=+",
|
||||
Key::LeftBracket => "[{",
|
||||
Key::RightBracket => "]}",
|
||||
Key::BackSlash => "\\|",
|
||||
Key::SemiColon => ";:",
|
||||
Key::Quote => "'\"",
|
||||
Key::BackQuote => "`~",
|
||||
_ => return None,
|
||||
};
|
||||
s.chars().nth(if is_shifted { 1 } else { 0 })
|
||||
}
|
93
src/lib/components/SnippetForm.svelte
Normal file
93
src/lib/components/SnippetForm.svelte
Normal file
|
@ -0,0 +1,93 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { ArrowLeft, Save } from '@lucide/svelte';
|
||||
import { uiStore } from '$lib/ui.svelte';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
onSave: () => void;
|
||||
};
|
||||
|
||||
let { onBack, onSave }: Props = $props();
|
||||
|
||||
let name = $state('');
|
||||
let keyword = $state('');
|
||||
let content = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function handleSave() {
|
||||
if (!name.trim() || !keyword.trim() || !content.trim()) {
|
||||
error = 'All fields are required.';
|
||||
return;
|
||||
}
|
||||
error = '';
|
||||
|
||||
try {
|
||||
await invoke('create_snippet', { name, keyword, content });
|
||||
uiStore.toasts.set(Date.now(), {
|
||||
id: Date.now(),
|
||||
title: 'Snippet Created',
|
||||
style: 'SUCCESS'
|
||||
});
|
||||
onSave();
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
error = errorMessage;
|
||||
console.error('Failed to create snippet:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||
<header class="flex h-12 shrink-0 items-center border-b px-2">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<div class="flex items-center gap-3 px-2">
|
||||
<Icon icon="snippets-16" class="size-6" />
|
||||
<h1 class="text-lg font-medium">Create Snippet</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div class="grow overflow-y-auto p-6">
|
||||
<div class="mx-auto max-w-xl space-y-6">
|
||||
<div class="grid grid-cols-[120px_1fr] items-center gap-4">
|
||||
<label for="name" class="text-right text-sm text-gray-400">Name</label>
|
||||
<Input id="name" placeholder="Snippet name" bind:value={name} />
|
||||
</div>
|
||||
<div class="grid grid-cols-[120px_1fr] items-center gap-4">
|
||||
<label for="keyword" class="text-right text-sm text-gray-400">Keyword</label>
|
||||
<Input id="keyword" placeholder="!email" bind:value={keyword} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[120px_1fr] items-start gap-4">
|
||||
<label for="content" class="pt-2 text-right text-sm text-gray-400">Snippet</label>
|
||||
<Textarea
|
||||
id="content"
|
||||
placeholder="Enter your snippet content here..."
|
||||
bind:value={content}
|
||||
class="min-h-32 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-center text-red-500">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="bg-card flex h-12 shrink-0 items-center justify-end border-t px-4">
|
||||
<Button onclick={handleSave}><Save class="mr-2 size-4" /> Create Snippet</Button>
|
||||
</footer>
|
||||
</main>
|
|
@ -9,7 +9,8 @@ export type ViewState =
|
|||
| 'settings'
|
||||
| 'extensions-store'
|
||||
| 'clipboard-history'
|
||||
| 'quicklink-form';
|
||||
| 'quicklink-form'
|
||||
| 'create-snippet-form';
|
||||
|
||||
type OauthState = {
|
||||
url: string;
|
||||
|
@ -47,6 +48,10 @@ class ViewManager {
|
|||
this.currentView = 'quicklink-form';
|
||||
};
|
||||
|
||||
showCreateSnippetForm = () => {
|
||||
this.currentView = 'create-snippet-form';
|
||||
};
|
||||
|
||||
runPlugin = (plugin: PluginInfo) => {
|
||||
switch (plugin.pluginPath) {
|
||||
case 'builtin:store':
|
||||
|
@ -58,6 +63,9 @@ class ViewManager {
|
|||
case 'builtin:create-quicklink':
|
||||
this.showQuicklinkForm();
|
||||
return;
|
||||
case 'builtin:create-snippet':
|
||||
this.showCreateSnippetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
uiStore.setCurrentRunningPlugin(plugin);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import ClipboardHistoryView from '$lib/components/ClipboardHistoryView.svelte';
|
||||
import QuicklinkForm from '$lib/components/QuicklinkForm.svelte';
|
||||
import { viewManager } from '$lib/viewManager.svelte';
|
||||
import SnippetForm from '$lib/components/SnippetForm.svelte';
|
||||
|
||||
const storePlugin: PluginInfo = {
|
||||
title: 'Discover Extensions',
|
||||
|
@ -50,12 +51,25 @@
|
|||
mode: 'view'
|
||||
};
|
||||
|
||||
const createSnippetPlugin: PluginInfo = {
|
||||
title: 'Create Snippet',
|
||||
description: 'Create a new snippet',
|
||||
pluginTitle: 'Snippets',
|
||||
pluginName: 'Snippets',
|
||||
commandName: 'create-snippet',
|
||||
pluginPath: 'builtin:create-snippet',
|
||||
icon: 'snippets-16',
|
||||
preferences: [],
|
||||
mode: 'view'
|
||||
};
|
||||
|
||||
const { pluginList, currentPreferences } = $derived(uiStore);
|
||||
const allPlugins = $derived([
|
||||
...pluginList,
|
||||
storePlugin,
|
||||
clipboardHistoryPlugin,
|
||||
createQuicklinkPlugin
|
||||
createQuicklinkPlugin,
|
||||
createSnippetPlugin
|
||||
]);
|
||||
|
||||
const { currentView, oauthState, oauthStatus, quicklinkToEdit } = $derived(viewManager);
|
||||
|
@ -164,4 +178,6 @@
|
|||
onBack={viewManager.showCommandPalette}
|
||||
onSave={viewManager.showCommandPalette}
|
||||
/>
|
||||
{:else if currentView === 'create-snippet-form'}
|
||||
<SnippetForm onBack={viewManager.showCommandPalette} onSave={viewManager.showCommandPalette} />
|
||||
{/if}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue