attn 0.1.14

A beautiful markdown viewer that launches from the CLI
use crate::watcher::UserEvent;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{AnyThread, ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{NSApplication, NSImage, NSMenu, NSMenuItem};
use objc2_foundation::{NSData, NSObject, NSObjectProtocol, NSString};
use std::sync::{Mutex, OnceLock};
use tao::event_loop::{EventLoop, EventLoopProxy};

pub struct SystemUiHandles {
    _main_menu: Retained<NSMenu>,
    _app_menu: Retained<NSMenu>,
    _menu_target: Retained<MenuActionTarget>,
}

static MENU_PROXY: OnceLock<Mutex<Option<EventLoopProxy<UserEvent>>>> = OnceLock::new();

fn set_menu_proxy(proxy: EventLoopProxy<UserEvent>) {
    let slot = MENU_PROXY.get_or_init(|| Mutex::new(None));
    if let Ok(mut guard) = slot.lock() {
        *guard = Some(proxy);
    }
}

fn send_user_event(event: UserEvent) {
    let Some(slot) = MENU_PROXY.get() else {
        return;
    };
    let Ok(guard) = slot.lock() else {
        return;
    };
    if let Some(proxy) = guard.as_ref() {
        let _ = proxy.send_event(event);
    }
}

define_class!(
    #[unsafe(super(NSObject))]
    #[thread_kind = MainThreadOnly]
    struct MenuActionTarget;

    impl MenuActionTarget {
        #[unsafe(method(openWindow:))]
        fn open_window(&self, _sender: Option<&AnyObject>) {
            send_user_event(UserEvent::ShowWindow);
        }

        #[unsafe(method(hideWindow:))]
        fn hide_window(&self, _sender: Option<&AnyObject>) {
            send_user_event(UserEvent::HideWindow);
        }

        #[unsafe(method(increaseFontScale:))]
        fn increase_font_scale(&self, _sender: Option<&AnyObject>) {
            send_user_event(UserEvent::FontScaleIncrease);
        }

        #[unsafe(method(decreaseFontScale:))]
        fn decrease_font_scale(&self, _sender: Option<&AnyObject>) {
            send_user_event(UserEvent::FontScaleDecrease);
        }

        #[unsafe(method(resetFontScale:))]
        fn reset_font_scale(&self, _sender: Option<&AnyObject>) {
            send_user_event(UserEvent::FontScaleReset);
        }

        #[unsafe(method(quitApp:))]
        fn quit_app(&self, _sender: Option<&AnyObject>) {
            send_user_event(UserEvent::Quit);
        }
    }

    unsafe impl NSObjectProtocol for MenuActionTarget {}
);

pub fn configure_event_loop(event_loop: &mut EventLoop<UserEvent>) {
    use tao::platform::macos::{ActivationPolicy, EventLoopExtMacOS};

    event_loop.set_activation_policy(ActivationPolicy::Regular);
    set_macos_app_icon();
}

pub fn install_system_ui(proxy: EventLoopProxy<UserEvent>) -> Option<SystemUiHandles> {
    set_menu_proxy(proxy);
    let mtm = MainThreadMarker::new()?;
    let app = NSApplication::sharedApplication(mtm);
    let menu_target: Retained<MenuActionTarget> =
        unsafe { msg_send![MenuActionTarget::class(), new] };

    let main_menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str("Main"));
    let app_menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str("attn"));
    let app_item = unsafe {
        NSMenuItem::initWithTitle_action_keyEquivalent(
            NSMenuItem::alloc(mtm),
            &NSString::from_str("attn"),
            None,
            &NSString::from_str(""),
        )
    };
    main_menu.addItem(&app_item);
    main_menu.setSubmenu_forItem(Some(&app_menu), &app_item);

    app_menu.addItem(&new_action_item(
        mtm,
        "Open attn",
        Some(sel!(openWindow:)),
        "",
        &menu_target,
    ));
    app_menu.addItem(&new_action_item(
        mtm,
        "Hide attn",
        Some(sel!(hideWindow:)),
        "h",
        &menu_target,
    ));
    app_menu.addItem(&new_action_item(
        mtm,
        "Zoom In",
        Some(sel!(increaseFontScale:)),
        "=",
        &menu_target,
    ));
    app_menu.addItem(&new_action_item(
        mtm,
        "Zoom Out",
        Some(sel!(decreaseFontScale:)),
        "-",
        &menu_target,
    ));
    app_menu.addItem(&new_action_item(
        mtm,
        "Actual Size",
        Some(sel!(resetFontScale:)),
        "0",
        &menu_target,
    ));
    app_menu.addItem(&NSMenuItem::separatorItem(mtm));
    app_menu.addItem(&new_action_item(
        mtm,
        "Quit attn",
        Some(sel!(quitApp:)),
        "q",
        &menu_target,
    ));
    app.setMainMenu(Some(&main_menu));

    Some(SystemUiHandles {
        _main_menu: main_menu,
        _app_menu: app_menu,
        _menu_target: menu_target,
    })
}

fn new_action_item(
    mtm: MainThreadMarker,
    title: &str,
    action: Option<objc2::runtime::Sel>,
    key: &str,
    target: &MenuActionTarget,
) -> Retained<NSMenuItem> {
    let item = unsafe {
        NSMenuItem::initWithTitle_action_keyEquivalent(
            NSMenuItem::alloc(mtm),
            &NSString::from_str(title),
            action,
            &NSString::from_str(key),
        )
    };
    unsafe {
        item.setTarget(Some(target.as_ref()));
    }
    item
}

pub fn activate_app() {
    let Some(mtm) = MainThreadMarker::new() else {
        return;
    };
    let app = NSApplication::sharedApplication(mtm);
    app.activate();
}

fn set_macos_app_icon() {
    static ICON_ICNS_BYTES: &[u8] = include_bytes!("../../icons/attn.icns");
    static ICON_PNG_BYTES: &[u8] = include_bytes!("../../icons/attn.png");

    let Some(mtm) = MainThreadMarker::new() else {
        return;
    };

    let Some(icon_image) =
        icon_image_from_bytes(ICON_ICNS_BYTES).or_else(|| icon_image_from_bytes(ICON_PNG_BYTES))
    else {
        return;
    };

    let app = NSApplication::sharedApplication(mtm);
    unsafe {
        app.setApplicationIconImage(Some(&icon_image));
    }
}

fn icon_image_from_bytes(bytes: &[u8]) -> Option<Retained<NSImage>> {
    let icon_data = NSData::with_bytes(bytes);
    NSImage::initWithData(NSImage::alloc(), &icon_data)
}