Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub enum ResetField {
HapticFeedback,
ShowMenubarIcon,
ClipboardHistory,
ClipboardPasteOnSelect,
MainPage,
ShowScrollbar,
ClearOnHide,
Expand Down Expand Up @@ -168,6 +169,7 @@ pub enum Message {
DebouncedSearch(Id),
CheckEventTap,
ThemeModeChanged(bool),
SimulatePaste(i32),
}

#[derive(Debug, Clone)]
Expand All @@ -191,6 +193,7 @@ pub enum SetConfigFields {
DebounceDelay(u64),
SetThemeFields(SetConfigThemeFields),
SetBufferFields(SetConfigBufferFields),
ClipboardPasteOnSelect(bool),
}

#[derive(Debug, Clone)]
Expand Down
21 changes: 21 additions & 0 deletions src/app/pages/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,26 @@ fn general_tab(config: Box<Config>, 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([
Expand Down Expand Up @@ -383,6 +403,7 @@ fn general_tab(config: Box<Config>, theme: crate::config::Theme) -> Column<'stat
haptic,
tray_icon,
clipboard_history,
cbhist_paste_on_select,
auto_suggest,
])
.spacing(10)
Expand Down
30 changes: 29 additions & 1 deletion src/app/tile/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,16 +543,33 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
_ => 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) => {
Expand Down Expand Up @@ -971,6 +988,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
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();
}
Expand Down Expand Up @@ -1024,6 +1044,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
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()
}
Expand Down Expand Up @@ -1078,6 +1101,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
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);
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Shelly>,
pub modes: HashMap<String, String>,
Expand All @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions src/platform/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/electron/electron/blob/e181fd040f72becd135db1fa977622b81da21643/shell/browser/browser_mac.mm#L512C1-L532C2>
Expand Down
5 changes: 5 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading