diff --git a/Cargo.toml b/Cargo.toml index c539c91..ea0f493 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ objc2 = "0.6.3" objc2-app-kit = { version = "0.3.2", features = ["NSImage"] } objc2-application-services = { version = "0.3.2", default-features = false, features = ["HIServices", "Processes"] } objc2-core-foundation = "0.3.2" -objc2-core-graphics = { version = "0.3.2", features = ["CGEvent"] } +objc2-core-graphics = { version = "0.3.2", features = ["CGEvent", "CGEventTypes", "CGRemoteOperation", "CGEventSource", "libc"] } objc2-event-kit = "0.3.2" objc2-foundation = { version = "0.3.2", features = ["NSDateFormatter", "NSFormatter", "NSString"] } objc2-service-management = "0.3.2" diff --git a/src/app.rs b/src/app.rs index 6dc963b..76358a7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -70,6 +70,7 @@ pub enum ResetField { HapticFeedback, ShowMenubarIcon, ClipboardHistory, + ClipboardPasteOnSelect, MainPage, ShowScrollbar, ClearOnHide, @@ -168,6 +169,7 @@ pub enum Message { DebouncedSearch(Id), CheckEventTap, ThemeModeChanged(bool), + SimulatePaste(i32), } #[derive(Debug, Clone)] @@ -191,6 +193,7 @@ pub enum SetConfigFields { DebounceDelay(u64), SetThemeFields(SetConfigThemeFields), SetBufferFields(SetConfigBufferFields), + ClipboardPasteOnSelect(bool), } #[derive(Debug, Clone)] diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 1929b2a..58d25d8 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -323,6 +323,26 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat theme.clone(), ); + let theme_clone = theme.clone(); + let cbhist_paste_on_select = settings_row_with_reset( + Row::from_iter([ + settings_hint_text(theme.clone(), "Paste on select"), + checkbox(config.clone().cbhist_paste_on_select) + .style(move |_, _| settings_checkbox_style(&theme_clone)) + .on_toggle(|input| { + Message::SetConfig(SetConfigFields::ClipboardPasteOnSelect(input)) + }) + .into(), + notice_item(theme.clone(), "Auto-paste clipboard item after selecting"), + ]) + .align_y(Alignment::Center) + .spacing(SETTINGS_ITEM_COL_SPACING * 2) + .padding(SETTINGS_ITEM_PADDING) + .height(SETTINGS_ITEM_HEIGHT), + ResetField::ClipboardPasteOnSelect, + theme.clone(), + ); + let theme_clone = theme.clone(); let auto_suggest = settings_row_with_reset( settings_item_column([ @@ -383,6 +403,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat haptic, tray_icon, clipboard_history, + cbhist_paste_on_select, auto_suggest, ]) .spacing(10) diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 9f96a47..01283ba 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -543,16 +543,33 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { _ => Task::done(Message::ReturnFocus), }; - if !tile.config.buffer_rules.clear_on_enter || !tile.visible { + let paste_on_select_active = tile.config.cbhist_paste_on_select + && tile.page == Page::ClipboardHistory + && matches!(command, Function::CopyToClipboard(_)); + + if (!tile.config.buffer_rules.clear_on_enter && !paste_on_select_active) + || !tile.visible + { return Task::none(); } + let paste_task = if paste_on_select_active { + tile.frontmost + .as_ref() + .map(|app| app.processIdentifier()) + .map(|pid| Task::done(Message::SimulatePaste(pid))) + .unwrap_or_else(Task::none) + } else { + Task::none() + }; + window::latest() .map(|x| x.unwrap()) .map(Message::HideWindow) .chain(page_task) .chain(Task::done(Message::ClearSearchQuery)) .chain(return_focus_task) + .chain(paste_task) } Message::HideWindow(a) => { @@ -971,6 +988,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { SetConfigFields::SetBufferFields(SetConfigBufferFields::ClearOnEnter(clear)) => { final_config.buffer_rules.clear_on_enter = clear } + SetConfigFields::ClipboardPasteOnSelect(v) => { + final_config.cbhist_paste_on_select = v + } SetConfigFields::ToDefault => { final_config = Config::default(); } @@ -1024,6 +1044,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ResetField::Modes => tile.config.modes = default.modes, ResetField::SearchDirs => tile.config.search_dirs = default.search_dirs, ResetField::ShellCommands => tile.config.shells = default.shells, + ResetField::ClipboardPasteOnSelect => { + tile.config.cbhist_paste_on_select = default.cbhist_paste_on_select + } } Task::none() } @@ -1078,6 +1101,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } + Message::SimulatePaste(pid) => { + crate::platform::simulate_paste(pid); + Task::none() + } + Message::ThemeModeChanged(is_dark) => { if tile.config.theme.theme_mode == ThemeMode::System { let (text, bg) = ThemeMode::System.presets(is_dark); diff --git a/src/config.rs b/src/config.rs index 366c80f..d255806 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,6 +28,7 @@ pub struct Config { pub search_url: String, pub haptic_feedback: bool, pub cbhist: bool, + pub cbhist_paste_on_select: bool, pub show_trayicon: bool, pub shells: Vec, pub modes: HashMap, @@ -51,6 +52,7 @@ impl Default for Config { placeholder: String::from("Time to be productive!"), search_url: "https://duckduckgo.com/search?q=%s".to_string(), cbhist: true, + cbhist_paste_on_select: false, haptic_feedback: false, auto_update: true, show_trayicon: true, diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 4bb7684..ef6a8ed 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -103,6 +103,25 @@ pub fn is_dark_mode() -> bool { .unwrap_or(false) } +/// Simulates Cmd+V (paste) targeted at the given process by PID. +/// Uses CGEventPostToPid so focus transfer timing is irrelevant. +pub fn simulate_paste(pid: libc::pid_t) { + use objc2_core_graphics::{CGEvent, CGEventFlags, CGEventSource, CGEventSourceStateID}; + + let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState); + let source_ref = source.as_deref(); + let v_keycode: u16 = 9; // kVK_ANSI_V + + if let Some(keydown) = CGEvent::new_keyboard_event(source_ref, v_keycode, true) { + CGEvent::set_flags(Some(&keydown), CGEventFlags::MaskCommand); + CGEvent::post_to_pid(pid, Some(&keydown)); + } + if let Some(keyup) = CGEvent::new_keyboard_event(source_ref, v_keycode, false) { + CGEvent::set_flags(Some(&keyup), CGEventFlags::MaskCommand); + CGEvent::post_to_pid(pid, Some(&keyup)); + } +} + /// This is the function that transforms the process to a UI element, and hides the dock icon /// /// see mostly diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 38de61e..25a390c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -23,6 +23,11 @@ pub fn focus_this_app() { self::macos::focus_this_app(); } +pub fn simulate_paste(pid: i32) { + #[cfg(target_os = "macos")] + self::macos::simulate_paste(pid); +} + pub fn transform_process_to_ui_element() { #[cfg(target_os = "macos")] self::macos::transform_process_to_ui_element();