#![cfg(target_os = "macos")]
#![allow(unexpected_cfgs, deprecated)]
use cocoa::appkit::NSPasteboard;
use cocoa::appkit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem};
use cocoa::base::{id, nil};
use cocoa::foundation::{NSInteger, NSString};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use std::ffi::CStr;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
static CMDV_EVENT: AtomicBool = AtomicBool::new(false);
#[inline]
fn ns_app() -> id {
unsafe { NSApplication::sharedApplication(nil) }
}
#[inline]
fn ns_menu_new() -> id {
unsafe { NSMenu::new(nil) }
}
#[inline]
fn ns_menu_item_new() -> id {
unsafe { NSMenuItem::new(nil) }
}
#[inline]
fn nsstring(s: &str) -> id {
unsafe { NSString::alloc(nil).init_str(s) }
}
#[inline]
fn menu_number_of_items(menu: id) -> NSInteger {
unsafe { msg_send![menu, numberOfItems] }
}
#[inline]
fn menu_item_at(menu: id, idx: NSInteger) -> id {
unsafe { msg_send![menu, itemAtIndex: idx] }
}
#[inline]
fn menu_add_item(menu: id, item: id) {
unsafe {
let _: () = msg_send![menu, addItem: item];
}
}
#[inline]
fn menu_item_submenu(item: id) -> id {
unsafe { msg_send![item, submenu] }
}
#[inline]
fn menu_item_set_title(item: id, title: &str) {
unsafe {
let _: () = msg_send![item, setTitle: nsstring(title)];
}
}
#[inline]
fn menu_item_set_submenu(item: id, submenu: id) {
unsafe {
let _: () = msg_send![item, setSubmenu: submenu];
}
}
#[inline]
fn menu_item_key_equivalent(item: id) -> String {
unsafe {
let key: id = msg_send![item, keyEquivalent];
let c: *const std::os::raw::c_char = msg_send![key, UTF8String];
if c.is_null() {
String::new()
} else {
CStr::from_ptr(c)
.to_string_lossy()
.into_owned()
}
}
}
#[inline]
fn menu_item_modifier_mask(item: id) -> NSEventModifierFlags {
unsafe { msg_send![item, keyEquivalentModifierMask] }
}
#[inline]
fn menu_item_set_action(item: id, action: Sel) {
unsafe {
let _: () = msg_send![item, setAction: action];
}
}
#[inline]
fn menu_item_set_target(item: id, target: id) {
unsafe {
let _: () = msg_send![item, setTarget: target];
}
}
#[inline]
fn menu_item_with_title_action_key(title: &str, action: Sel, key: &str) -> id {
unsafe { NSMenuItem::alloc(nil).initWithTitle_action_keyEquivalent_(nsstring(title), action, nsstring(key)) }
}
#[inline]
fn menu_item_set_key_mask(item: id, mask: NSEventModifierFlags) {
unsafe {
let _: () = msg_send![item, setKeyEquivalentModifierMask: mask];
}
}
#[inline]
fn app_send_paste() {
unsafe {
let _: bool = msg_send![ns_app(), sendAction: sel!(paste:) to: nil from: nil];
}
}
#[inline]
fn class_get_or_register() -> Option<&'static Class> {
unsafe {
if let Some(cls) = Class::get("RustPasteTarget") {
return Some(cls);
}
let superclass = class!(NSObject);
let mut decl = ClassDecl::new("RustPasteTarget", superclass)?;
decl.add_method(sel!(pasteCmdV:), paste_cmdv as extern "C" fn(&Object, Sel, id));
decl.add_method(sel!(validateMenuItem:), validate_menu_item as extern "C" fn(&Object, Sel, id) -> bool);
Some(decl.register())
}
}
#[inline]
fn app_main_menu() -> id {
unsafe { msg_send![ns_app(), mainMenu] }
}
#[inline]
pub fn pasteboard_file_path() -> Option<PathBuf> {
unsafe {
let pb: id = NSPasteboard::generalPasteboard(nil);
if pb == nil {
return None;
}
let url: id = msg_send![class!(NSURL), URLFromPasteboard: pb];
if url != nil {
let is_file: bool = msg_send![url, isFileURL];
if is_file {
let ns_path: id = msg_send![url, path];
if ns_path != nil {
let c: *const std::os::raw::c_char = msg_send![ns_path, UTF8String];
if !c.is_null() {
let s = CStr::from_ptr(c)
.to_string_lossy()
.into_owned();
return Some(PathBuf::from(s));
}
}
}
}
let nsurl_class: id = msg_send![class!(NSURL), class];
let classes: id = msg_send![class!(NSArray), arrayWithObject: nsurl_class];
let urls: id = msg_send![pb, readObjectsForClasses: classes options: nil];
if urls != nil {
let count: isize = msg_send![urls, count];
if count > 0 {
let url: id = msg_send![urls, objectAtIndex: 0];
let is_file: bool = msg_send![url, isFileURL];
if is_file {
let ns_path: id = msg_send![url, path];
if ns_path != nil {
let c: *const std::os::raw::c_char = msg_send![ns_path, UTF8String];
if !c.is_null() {
let s = CStr::from_ptr(c)
.to_string_lossy()
.into_owned();
return Some(PathBuf::from(s));
}
}
}
}
}
let ty = nsstring("NSFilenamesPboardType");
let arr: id = msg_send![pb, propertyListForType: ty];
if arr != nil {
let count: isize = msg_send![arr, count];
if count > 0 {
let s: id = msg_send![arr, objectAtIndex: 0];
if s != nil {
let c: *const std::os::raw::c_char = msg_send![s, UTF8String];
if !c.is_null() {
let s = CStr::from_ptr(c)
.to_string_lossy()
.into_owned();
return Some(PathBuf::from(s));
}
}
}
}
None
}
}
extern "C" fn paste_cmdv(_this: &Object, _cmd: Sel, _sender: id) {
CMDV_EVENT.store(true, Ordering::SeqCst);
app_send_paste();
}
extern "C" fn validate_menu_item(_this: &Object, _cmd: Sel, _item: id) -> bool {
true
}
fn ensure_target_class() -> Option<&'static Class> {
class_get_or_register()
}
unsafe fn hook_or_create_paste_item() -> bool {
let main: id = app_main_menu();
if main == nil {
return false;
}
let top_n = menu_number_of_items(main);
for i in 0..top_n {
let top_item = menu_item_at(main, i);
if top_item == nil {
continue;
}
let submenu = menu_item_submenu(top_item);
if submenu == nil {
continue;
}
let sub_n = menu_number_of_items(submenu);
for j in 0..sub_n {
let it = menu_item_at(submenu, j);
if it == nil {
continue;
}
let eq = menu_item_key_equivalent(it);
let mask = menu_item_modifier_mask(it);
if eq == "v" && mask.contains(NSEventModifierFlags::NSCommandKeyMask) {
let Some(cls) = ensure_target_class() else {
return false;
};
let target: id = msg_send![cls, new];
menu_item_set_action(it, sel!(pasteCmdV:));
menu_item_set_target(it, target);
return true;
}
}
}
let mut edit_sub: id = nil;
'outer: for i in 0..top_n {
let ti = menu_item_at(main, i);
if ti == nil {
continue;
}
let sm = menu_item_submenu(ti);
if sm == nil {
continue;
}
let sub_n = menu_number_of_items(sm);
let mut has_cut = false;
let mut has_copy = false;
for j in 0..sub_n {
let it = menu_item_at(sm, j);
if it == nil {
continue;
}
let eq = menu_item_key_equivalent(it);
let mask = menu_item_modifier_mask(it);
if eq == "x" && mask.contains(NSEventModifierFlags::NSCommandKeyMask) {
has_cut = true;
}
if eq == "c" && mask.contains(NSEventModifierFlags::NSCommandKeyMask) {
has_copy = true;
}
if has_cut && has_copy {
edit_sub = sm;
break 'outer;
}
}
}
if edit_sub == nil {
let edit_item = ns_menu_item_new();
menu_item_set_title(edit_item, "Edit");
menu_add_item(main, edit_item);
let menu = ns_menu_new();
menu_item_set_submenu(edit_item, menu);
edit_sub = menu;
}
let paste = menu_item_with_title_action_key("Paste", sel!(pasteCmdV:), "v");
menu_item_set_key_mask(paste, NSEventModifierFlags::NSCommandKeyMask);
let Some(cls) = ensure_target_class() else {
return false;
};
let target: id = msg_send![cls, new];
menu_item_set_target(paste, target);
menu_add_item(edit_sub, paste);
true
}
pub fn tick_ensure_cmdv_event() {
static DONE: AtomicBool = AtomicBool::new(false);
if DONE.load(Ordering::Relaxed) {
return;
}
unsafe {
if hook_or_create_paste_item() {
DONE.store(true, Ordering::Relaxed);
}
}
}
pub fn take_cmdv_event() -> bool {
CMDV_EVENT.swap(false, Ordering::SeqCst)
}