Skip to main content

native/guppy_nif/src/main_thread_runtime.rs

use crate::bridge_text_input;
use crate::bridge_view::BridgeView;
use crate::ir::IrNode;
use crate::menu::{self, MenuItemSpec, MenuSpec};
use crate::window_options::WindowOptionsConfig;
use async_task::spawn;
#[cfg(target_os = "macos")]
use cocoa::{
    appkit::{NSApp, NSModalResponse, NSOpenPanel, NSSavePanel},
    base::{BOOL, NO, YES, id, nil},
    foundation::{NSArray, NSAutoreleasePool, NSString, NSURL},
};
use gpui::{App, AppContext, Application, AsyncApp, ClipboardItem, PlatformDispatcher};
#[cfg(not(target_os = "macos"))]
use gpui::{PathPromptOptions, SharedString};
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
use std::cell::RefCell;
use std::collections::HashMap;
#[cfg(target_os = "macos")]
use std::ffi::CStr;
#[cfg(target_os = "macos")]
use std::os::raw::c_char;
#[cfg(not(target_os = "macos"))]
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};

thread_local! {
    static APP: RefCell<Option<AsyncApp>> = const { RefCell::new(None) };
    static WINDOWS: RefCell<HashMap<u64, gpui::WindowHandle<BridgeView>>> = RefCell::new(HashMap::new());
}

static REQUEST_TX: OnceLock<Sender<MainThreadRequest>> = OnceLock::new();
static REQUEST_RX: OnceLock<Mutex<Receiver<MainThreadRequest>>> = OnceLock::new();
static MAIN_THREAD_DISPATCHER: OnceLock<Mutex<Option<Arc<dyn PlatformDispatcher>>>> =
    OnceLock::new();
static REQUEST_DRAIN_SCHEDULED: AtomicBool = AtomicBool::new(false);

#[derive(Clone)]
pub(crate) struct RequestDeadline {
    started_at: Instant,
    timeout: Duration,
    canceled: Arc<AtomicBool>,
}

impl RequestDeadline {
    pub(crate) fn after(timeout: Duration) -> Self {
        Self {
            started_at: Instant::now(),
            timeout,
            canceled: Arc::new(AtomicBool::new(false)),
        }
    }

    fn cancel(&self) {
        self.canceled.store(true, Ordering::Release);
    }

    fn expired(&self) -> bool {
        self.canceled.load(Ordering::Acquire) || self.started_at.elapsed() >= self.timeout
    }
}

pub(crate) enum MainThreadRequest {
    OpenWindow {
        deadline: RequestDeadline,
        view_id: u64,
        ir: IrNode,
        options: WindowOptionsConfig,
        reply: Sender<i32>,
    },
    SetIr {
        deadline: RequestDeadline,
        view_id: u64,
        ir: IrNode,
        reply: Sender<i32>,
    },
    CloseWindow {
        deadline: RequestDeadline,
        view_id: u64,
        reply: Sender<i32>,
    },
    FocusWindow {
        deadline: RequestDeadline,
        view_id: u64,
        reply: Sender<i32>,
    },
    CloseAll {
        deadline: RequestDeadline,
        reply: Sender<i32>,
    },
    SetMenus {
        deadline: RequestDeadline,
        menus: Vec<MenuSpec>,
        reply: Sender<i32>,
    },
    SetDockMenu {
        deadline: RequestDeadline,
        items: Vec<MenuItemSpec>,
        reply: Sender<i32>,
    },
    SetAppBadge {
        deadline: RequestDeadline,
        label: Option<String>,
        reply: Sender<i32>,
    },
    OpenFileDialog {
        deadline: RequestDeadline,
        files: bool,
        directories: bool,
        multiple: bool,
        prompt: Option<String>,
        directory: Option<String>,
        filters: Vec<String>,
        owner_view_id: Option<u64>,
        reply: Sender<Result<Option<Vec<String>>, ()>>,
    },
    SaveFileDialog {
        deadline: RequestDeadline,
        directory: Option<String>,
        default_name: Option<String>,
        filters: Vec<String>,
        owner_view_id: Option<u64>,
        reply: Sender<Result<Option<String>, ()>>,
    },
    ReadClipboardText {
        deadline: RequestDeadline,
        reply: Sender<Result<Option<String>, ()>>,
    },
    WriteClipboardText {
        deadline: RequestDeadline,
        text: String,
        reply: Sender<i32>,
    },
    CloseAllNoReply,
    ViewCount {
        deadline: RequestDeadline,
        reply: Sender<u64>,
    },
}

impl MainThreadRequest {
    fn deadline(&self) -> Option<&RequestDeadline> {
        match self {
            MainThreadRequest::OpenWindow { deadline, .. }
            | MainThreadRequest::SetIr { deadline, .. }
            | MainThreadRequest::CloseWindow { deadline, .. }
            | MainThreadRequest::FocusWindow { deadline, .. }
            | MainThreadRequest::CloseAll { deadline, .. }
            | MainThreadRequest::SetMenus { deadline, .. }
            | MainThreadRequest::SetDockMenu { deadline, .. }
            | MainThreadRequest::SetAppBadge { deadline, .. }
            | MainThreadRequest::OpenFileDialog { deadline, .. }
            | MainThreadRequest::SaveFileDialog { deadline, .. }
            | MainThreadRequest::ReadClipboardText { deadline, .. }
            | MainThreadRequest::WriteClipboardText { deadline, .. }
            | MainThreadRequest::ViewCount { deadline, .. } => Some(deadline),
            MainThreadRequest::CloseAllNoReply => None,
        }
    }
}

pub fn run_app() {
    init_request_queue();

    Application::new().run(move |cx: &mut App| {
        APP.with(|app| {
            *app.borrow_mut() = Some(cx.to_async());
        });

        bridge_text_input::bind_keys(cx);
        crate::bridge_view::bind_focus_keys(cx);
        menu::bind_menu_action(cx);
        register_main_thread_dispatcher(cx);

        crate::notify_gui_started(1);
    });
}

pub(crate) fn enqueue_request(request: MainThreadRequest) -> Result<(), ()> {
    let cancel_deadline = request.deadline().cloned();
    let sender = REQUEST_TX.get().ok_or(())?;
    sender.send(request).map_err(|_| ())?;

    if schedule_request_drain().is_err() {
        if let Some(deadline) = cancel_deadline {
            deadline.cancel();
        }

        return Err(());
    }

    Ok(())
}

pub fn open_window(view_id: u64, ir: IrNode, options: WindowOptionsConfig) -> i32 {
    if WINDOWS.with(|windows| windows.borrow().contains_key(&view_id)) {
        return 0;
    }

    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(mut app) = app else {
            return -1;
        };

        let should_focus = options.focus.unwrap_or(true);
        let Ok(gpui_options) = app.update(|cx| options.into_gpui(cx)) else {
            return -1;
        };

        let result = app.open_window(gpui_options, move |_, cx| {
            cx.new(|_| BridgeView {
                view_id,
                ir,
                retained: Default::default(),
            })
        });

        match result {
            Ok(handle) => {
                if should_focus {
                    let _ = app.update(|cx| {
                        cx.activate(true);
                    });
                }

                let _ = handle.update(&mut app, |_, window, cx| {
                    if should_focus {
                        window.activate_window();
                    }

                    window.on_window_should_close(cx, move |_window, _cx| {
                        let _ = crate::send_window_close_requested_event(view_id);

                        WINDOWS.with(|windows| {
                            windows.borrow_mut().remove(&view_id);
                        });

                        let _ = crate::send_window_closed_event(view_id);
                        true
                    });
                });

                WINDOWS.with(|windows| {
                    windows.borrow_mut().insert(view_id, handle);
                });
                1
            }
            Err(_) => -1,
        }
    })
}

pub fn close_all_windows() -> i32 {
    let view_ids = WINDOWS.with(|windows| windows.borrow().keys().copied().collect::<Vec<_>>());

    for view_id in view_ids {
        let _ = close_window(view_id);
    }

    1
}

pub fn close_window(view_id: u64) -> i32 {
    let handle = WINDOWS.with(|windows| windows.borrow_mut().remove(&view_id));

    let Some(handle) = handle else {
        return 0;
    };

    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(mut app) = app else {
            return -1;
        };

        match handle.update(&mut app, |_, window, _| window.remove_window()) {
            Ok(_) => 1,
            Err(_) => -1,
        }
    })
}

pub fn update_ir(view_id: u64, ir: IrNode) -> i32 {
    let handle = WINDOWS.with(|windows| windows.borrow().get(&view_id).cloned());

    let Some(handle) = handle else {
        return 0;
    };

    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(mut app) = app else {
            return -1;
        };

        match handle.update(&mut app, |view, _window, cx| {
            view.ir = ir;
            cx.notify();
        }) {
            Ok(_) => 1,
            Err(_) => -1,
        }
    })
}

pub fn focus_window(view_id: u64) -> i32 {
    let handle = WINDOWS.with(|windows| windows.borrow().get(&view_id).cloned());

    let Some(handle) = handle else {
        return 0;
    };

    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(mut app) = app else {
            return -1;
        };

        match handle.update(&mut app, |_, window, _| window.activate_window()) {
            Ok(_) => 1,
            Err(_) => -1,
        }
    })
}

pub fn view_count() -> u64 {
    WINDOWS.with(|windows| windows.borrow().len() as u64)
}

pub fn set_menus(menus: Vec<MenuSpec>) -> i32 {
    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(app) = app else {
            return -1;
        };

        let result = app.update(move |cx| menu::install_menus(cx, menus));

        match result {
            Ok(_) => 1,
            Err(_) => -1,
        }
    })
}

pub fn set_dock_menu(items: Vec<MenuItemSpec>) -> i32 {
    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(app) = app else {
            return -1;
        };

        let result = app.update(move |cx| cx.set_dock_menu(menu::to_gpui_dock_menu_items(items)));

        match result {
            Ok(_) => 1,
            Err(_) => -1,
        }
    })
}

pub fn set_app_badge(label: Option<String>) -> i32 {
    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(app) = app else {
            return -1;
        };

        app.update(move |_| set_platform_app_badge(label))
            .unwrap_or(-1)
    })
}

#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
fn set_platform_app_badge(label: Option<String>) -> i32 {
    unsafe {
        let app: id = NSApp();
        if app == nil {
            return -1;
        }

        let dock_tile: id = msg_send![app, dockTile];
        if dock_tile == nil {
            return -1;
        }

        let badge_label = label
            .as_deref()
            .map(|label| NSString::alloc(nil).init_str(label).autorelease())
            .unwrap_or(nil);

        let _: () = msg_send![dock_tile, setBadgeLabel: badge_label];
        let _: () = msg_send![dock_tile, display];
        1
    }
}

#[cfg(not(target_os = "macos"))]
fn set_platform_app_badge(_label: Option<String>) -> i32 {
    -1
}

#[allow(clippy::too_many_arguments)]
pub fn open_file_dialog(
    deadline: RequestDeadline,
    files: bool,
    directories: bool,
    multiple: bool,
    prompt: Option<String>,
    directory: Option<String>,
    filters: Vec<String>,
    owner_view_id: Option<u64>,
    reply: Sender<Result<Option<Vec<String>>, ()>>,
) {
    APP.with(|app| {
        let Some(app) = app.borrow().as_ref().cloned() else {
            let _ = reply.send(Err(()));
            return;
        };

        if !owner_view_id_exists(owner_view_id) {
            let _ = reply.send(Err(()));
            return;
        }

        #[cfg(target_os = "macos")]
        let _ = app;

        #[cfg(target_os = "macos")]
        {
            let result = run_open_file_dialog_macos(
                files,
                directories,
                multiple,
                prompt,
                directory,
                filters,
            );

            if !deadline.expired() {
                let _ = reply.send(result);
            }
        }

        #[cfg(not(target_os = "macos"))]
        {
            if directory.is_some() || !filters.is_empty() {
                let _ = reply.send(Err(()));
                return;
            }

            let options = PathPromptOptions {
                files,
                directories,
                multiple,
                prompt: prompt.map(SharedString::from),
            };

            let receiver = match app.update(move |cx| cx.prompt_for_paths(options)) {
                Ok(receiver) => receiver,
                Err(_) => {
                    let _ = reply.send(Err(()));
                    return;
                }
            };

            let executor = app.background_executor().clone();
            executor
                .spawn(async move {
                    let result = match receiver.await {
                        Ok(Ok(Some(paths))) => {
                            Ok(Some(paths.into_iter().map(path_to_string).collect()))
                        }
                        Ok(Ok(None)) => Ok(None),
                        Ok(Err(_)) | Err(_) => Err(()),
                    };

                    if !deadline.expired() {
                        let _ = reply.send(result);
                    }
                })
                .detach();
        }
    });
}

pub fn save_file_dialog(
    deadline: RequestDeadline,
    directory: Option<String>,
    default_name: Option<String>,
    filters: Vec<String>,
    owner_view_id: Option<u64>,
    reply: Sender<Result<Option<String>, ()>>,
) {
    APP.with(|app| {
        let Some(app) = app.borrow().as_ref().cloned() else {
            let _ = reply.send(Err(()));
            return;
        };

        if !owner_view_id_exists(owner_view_id) {
            let _ = reply.send(Err(()));
            return;
        }

        #[cfg(target_os = "macos")]
        let _ = app;

        #[cfg(target_os = "macos")]
        {
            let result = run_save_file_dialog_macos(directory, default_name, filters);

            if !deadline.expired() {
                let _ = reply.send(result);
            }
        }

        #[cfg(not(target_os = "macos"))]
        {
            if !filters.is_empty() {
                let _ = reply.send(Err(()));
                return;
            }

            let directory = directory
                .map(PathBuf::from)
                .or_else(|| std::env::current_dir().ok())
                .unwrap_or_else(|| PathBuf::from("."));

            let receiver = match app.update(move |cx| {
                cx.prompt_for_new_path(Path::new(&directory), default_name.as_deref())
            }) {
                Ok(receiver) => receiver,
                Err(_) => {
                    let _ = reply.send(Err(()));
                    return;
                }
            };

            let executor = app.background_executor().clone();
            executor
                .spawn(async move {
                    let result = match receiver.await {
                        Ok(Ok(Some(path))) => Ok(Some(path_to_string(path))),
                        Ok(Ok(None)) => Ok(None),
                        Ok(Err(_)) | Err(_) => Err(()),
                    };

                    if !deadline.expired() {
                        let _ = reply.send(result);
                    }
                })
                .detach();
        }
    });
}

#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
fn run_open_file_dialog_macos(
    files: bool,
    directories: bool,
    multiple: bool,
    prompt: Option<String>,
    directory: Option<String>,
    filters: Vec<String>,
) -> Result<Option<Vec<String>>, ()> {
    unsafe {
        let panel = NSOpenPanel::openPanel(nil);
        panel.setCanChooseDirectories_(objc_bool(directories));
        panel.setCanChooseFiles_(objc_bool(files));
        panel.setAllowsMultipleSelection_(objc_bool(multiple));
        panel.setCanCreateDirectories(objc_bool(true));
        panel.setResolvesAliases_(objc_bool(false));

        if let Some(prompt) = prompt.as_deref() {
            let _: () = msg_send![panel, setPrompt: ns_string(prompt)];
        }

        set_panel_directory(panel, directory.as_deref());
        set_allowed_file_types(panel, &filters);

        let response: NSModalResponse = msg_send![panel, runModal];
        if response != NSModalResponse::NSModalResponseOk {
            return Ok(None);
        }

        let urls = panel.URLs();
        let mut paths = Vec::new();
        for index in 0..urls.count() {
            let url = urls.objectAtIndex(index);
            if let Some(path) = ns_url_to_path_string(url) {
                paths.push(path);
            }
        }

        Ok(Some(paths))
    }
}

#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
fn run_save_file_dialog_macos(
    directory: Option<String>,
    default_name: Option<String>,
    filters: Vec<String>,
) -> Result<Option<String>, ()> {
    unsafe {
        let panel = NSSavePanel::savePanel(nil);
        set_panel_directory(panel, directory.as_deref());
        set_allowed_file_types(panel, &filters);

        if let Some(default_name) = default_name.as_deref() {
            let _: () = msg_send![panel, setNameFieldStringValue: ns_string(default_name)];
        }

        let response: NSModalResponse = msg_send![panel, runModal];
        if response != NSModalResponse::NSModalResponseOk {
            return Ok(None);
        }

        let url: id = msg_send![panel, URL];
        Ok(ns_url_to_path_string(url))
    }
}

#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
unsafe fn set_panel_directory(panel: id, directory: Option<&str>) {
    if let Some(directory) = directory {
        unsafe {
            let path = ns_string(directory);
            let url = NSURL::fileURLWithPath_isDirectory_(nil, path, objc_bool(true));
            let _: () = msg_send![panel, setDirectoryURL: url];
        }
    }
}

#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
unsafe fn set_allowed_file_types(panel: id, filters: &[String]) {
    if filters.is_empty() {
        return;
    }

    unsafe {
        let objects = filters
            .iter()
            .map(|filter| ns_string(filter))
            .collect::<Vec<_>>();
        let file_types = NSArray::arrayWithObjects(nil, &objects);
        let _: () = msg_send![panel, setAllowedFileTypes: file_types];
    }
}

#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
unsafe fn ns_url_to_path_string(url: id) -> Option<String> {
    if url == nil {
        return None;
    }

    unsafe {
        let path: id = msg_send![url, path];
        if path == nil {
            return None;
        }

        let bytes: *const c_char = msg_send![path, UTF8String];
        if bytes.is_null() {
            return None;
        }

        Some(CStr::from_ptr(bytes).to_string_lossy().into_owned())
    }
}

#[cfg(target_os = "macos")]
unsafe fn ns_string(string: &str) -> id {
    unsafe { NSString::alloc(nil).init_str(string).autorelease() }
}

#[cfg(target_os = "macos")]
fn objc_bool(value: bool) -> BOOL {
    if value { YES } else { NO }
}

fn owner_view_id_exists(owner_view_id: Option<u64>) -> bool {
    match owner_view_id {
        Some(view_id) => WINDOWS.with(|windows| windows.borrow().contains_key(&view_id)),
        None => true,
    }
}

#[cfg(not(target_os = "macos"))]
fn path_to_string(path: PathBuf) -> String {
    path.to_string_lossy().into_owned()
}

pub fn read_clipboard_text() -> Result<Option<String>, ()> {
    APP.with(|app| {
        let app = app.borrow().as_ref().cloned().ok_or(())?;

        app.update(|cx| cx.read_from_clipboard().and_then(|item| item.text()))
            .map_err(|_| ())
    })
}

pub fn write_clipboard_text(text: String) -> i32 {
    APP.with(|app| {
        let app = app.borrow().as_ref().cloned();

        let Some(app) = app else {
            return -1;
        };

        let result = app.update(move |cx| cx.write_to_clipboard(ClipboardItem::new_string(text)));

        match result {
            Ok(_) => 1,
            Err(_) => -1,
        }
    })
}

pub(crate) fn init_request_queue() {
    if REQUEST_TX.get().is_none() {
        let (tx, rx) = mpsc::channel();
        let _ = REQUEST_TX.set(tx);
        let _ = REQUEST_RX.set(Mutex::new(rx));
    }

    let _ = MAIN_THREAD_DISPATCHER.set(Mutex::new(None));
}

fn register_main_thread_dispatcher(cx: &mut App) {
    let dispatcher = cx.foreground_executor().dispatcher.clone();

    let Some(slot) = MAIN_THREAD_DISPATCHER.get() else {
        return;
    };

    let Ok(mut slot) = slot.lock() else {
        return;
    };

    *slot = Some(dispatcher);
}

fn schedule_request_drain() -> Result<(), ()> {
    if REQUEST_DRAIN_SCHEDULED.swap(true, Ordering::AcqRel) {
        return Ok(());
    }

    dispatch_request_drain().inspect_err(|_| {
        REQUEST_DRAIN_SCHEDULED.store(false, Ordering::Release);
    })
}

fn dispatch_request_drain() -> Result<(), ()> {
    let dispatcher = {
        let slot = MAIN_THREAD_DISPATCHER.get().ok_or(())?;
        let slot = slot.lock().map_err(|_| ())?;
        slot.clone().ok_or(())?
    };

    let (runnable, task) = spawn(async move { drain_requests() }, move |runnable| {
        dispatcher.dispatch_on_main_thread(runnable);
    });

    runnable.schedule();
    task.detach();
    Ok(())
}

fn drain_requests() {
    loop {
        while let Some(request) = try_next_request() {
            handle_request(request);
        }

        REQUEST_DRAIN_SCHEDULED.store(false, Ordering::Release);

        let Some(request) = try_next_request() else {
            break;
        };

        let _ = REQUEST_DRAIN_SCHEDULED.swap(true, Ordering::AcqRel);
        handle_request(request);
    }
}

fn try_next_request() -> Option<MainThreadRequest> {
    let receiver = REQUEST_RX.get()?;
    let guard = receiver.lock().ok()?;
    guard.try_recv().ok()
}

fn handle_request(request: MainThreadRequest) {
    match request {
        MainThreadRequest::OpenWindow {
            deadline,
            view_id,
            ir,
            options,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(open_window(view_id, ir, options));
            }
        }
        MainThreadRequest::SetIr {
            deadline,
            view_id,
            ir,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(update_ir(view_id, ir));
            }
        }
        MainThreadRequest::CloseWindow {
            deadline,
            view_id,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(close_window(view_id));
            }
        }
        MainThreadRequest::FocusWindow {
            deadline,
            view_id,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(focus_window(view_id));
            }
        }
        MainThreadRequest::CloseAll { deadline, reply } => {
            if !deadline.expired() {
                let _ = reply.send(close_all_windows());
            }
        }
        MainThreadRequest::SetMenus {
            deadline,
            menus,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(set_menus(menus));
            }
        }
        MainThreadRequest::SetDockMenu {
            deadline,
            items,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(set_dock_menu(items));
            }
        }
        MainThreadRequest::SetAppBadge {
            deadline,
            label,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(set_app_badge(label));
            }
        }
        MainThreadRequest::OpenFileDialog {
            deadline,
            files,
            directories,
            multiple,
            prompt,
            directory,
            filters,
            owner_view_id,
            reply,
        } => {
            if !deadline.expired() {
                open_file_dialog(
                    deadline,
                    files,
                    directories,
                    multiple,
                    prompt,
                    directory,
                    filters,
                    owner_view_id,
                    reply,
                );
            }
        }
        MainThreadRequest::SaveFileDialog {
            deadline,
            directory,
            default_name,
            filters,
            owner_view_id,
            reply,
        } => {
            if !deadline.expired() {
                save_file_dialog(
                    deadline,
                    directory,
                    default_name,
                    filters,
                    owner_view_id,
                    reply,
                );
            }
        }
        MainThreadRequest::ReadClipboardText { deadline, reply } => {
            if !deadline.expired() {
                let _ = reply.send(read_clipboard_text());
            }
        }
        MainThreadRequest::WriteClipboardText {
            deadline,
            text,
            reply,
        } => {
            if !deadline.expired() {
                let _ = reply.send(write_clipboard_text(text));
            }
        }
        MainThreadRequest::CloseAllNoReply => {
            let _ = set_menus(Vec::new());
            let _ = set_dock_menu(Vec::new());
            let _ = set_app_badge(None);
            let _ = close_all_windows();
        }
        MainThreadRequest::ViewCount { deadline, reply } => {
            if !deadline.expired() {
                let _ = reply.send(view_count());
            }
        }
    }
}

#[cfg(test)]
#[path = "main_thread_runtime_tests.rs"]
mod tests;